API v1
Conecta tu restaurante con lo que ya usas.
API REST en JSON y webhooks firmados para llevar tus órdenes, menú, clientes, inventario y ventas a tu contabilidad, BI, app propia o canal de delivery. Disponible con el módulo «API y webhooks» (plan Cadena).
Inicio rápido
1. En Integraciones y API → Llaves de API crea una llave y elige sus permisos. La llave completa (sk_live_…) se muestra una sola vez: guárdala en un servidor, nunca en una app móvil o página pública.
2. Haz tu primera llamada:
curl https://mirestaurancito.com.mx/api/v1/me \
-H "Authorization: Bearer sk_live_TU_LLAVE"{
"data": {
"organization": { "id": "cm…", "name": "La Casona", "slug": "la-casona", "currency": "MXN", "timezone": "America/Mexico_City" },
"scopes": ["read", "orders:write"],
"branches": 2
}
}La URL base es la de tu instalación seguida de /api/v1. En los ejemplos usamos https://mirestaurancito.com.mx/api/v1.
Autenticación y permisos
Envía la llave en el encabezado Authorization: Bearer <llave> en cada petición. Guardamos solo un hash SHA-256 de la llave; si la pierdes, revócala y crea otra. Una llave revocada responde 401 de inmediato.
| Permiso | Qué permite |
|---|---|
read | Lectura (menú, órdenes, clientes, inventario, reportes, sucursales) |
orders:write | Crear órdenes (canales externos, app propia) |
menu:write | Cambiar precio y disponibilidad de productos |
customers:write | Crear clientes |
inventory:write | Registrar ajustes y mermas de inventario |
sync | Servidor local: sincronizar ventas y bajar configuración (modo sin internet) |
Los permisos de escritura no incluyen lectura: agrega read si tu integración también consulta datos.
Convenciones
- Dinero en centavos (enteros) junto a un campo
currency(ej."MXN").14500= $145.00. - Fechas ISO 8601 en UTC (
2026-10-22T18:30:00.000Z). Los filtrosfrom/toaceptanYYYY-MM-DD(día completo en la zona horaria del negocio) o una fecha ISO. - Respuestas: un recurso viene en
{ "data": {…} }; una lista en{ "data": [...], "hasMore": true, "nextCursor": "…" }. - Paginación por cursor:
limit(1–100, por defecto 50) ycursor= elnextCursorde la página anterior. CuandohasMoreesfalseno hay más. - Sucursal: los recursos operativos aceptan
branchId; si lo omites se usa la primera sucursal activa (consultaGET /branches).
Errores
Todos los errores tienen el mismo formato y un código estable que puedes usar en tu código:
{ "error": { "code": "product_unavailable", "message": "items[0]: Taco al pastor está agotado." } }| 400 | validation_error, invalid_json, invalid_date, invalid_cursor, invalid_status, invalid_source, invalid_range |
| 401 | unauthorized — falta la llave, es inválida o fue revocada |
| 403 | insufficient_scope — la llave no tiene el permiso; module_disabled — el plan no incluye la API |
| 404 | not_found, branch_not_found |
| 409 | customer_exists |
| 422 | product_not_found, product_unavailable, invalid_modifier, order_rejected |
| 429 | rate_limited — revisa el encabezado Retry-After |
| 500 | internal_error |
Límites de uso
Cada llave tiene una cubeta de 120 peticiones que se rellena a 120 por minuto. Cada respuesta incluye X-RateLimit-Limit y X-RateLimit-Remaining; al excederlo recibes 429 con Retry-After (segundos). Si necesitas más, usa webhooks en lugar de consultar en bucle.
Órdenes
/orderspermiso readMás recientes primero. Filtros: from, to, status (OPEN | PAID | CANCELLED), source (POS, WAITER, QR, ONLINE, KIOSK, RAPPI, UBER, DIDI, WHATSAPP), branchId, limit, cursor.
curl "https://mirestaurancito.com.mx/api/v1/orders?from=2026-10-01&to=2026-10-31&status=PAID&limit=100" \
-H "Authorization: Bearer sk_live_…"/orders/:idpermiso read{
"data": {
"id": "cm_order", "number": 1042, "externalId": "RAPPI-88123", "branchId": "cm…",
"type": "DELIVERY", "source": "RAPPI", "status": "PAID", "fulfillment": "RECEIVED",
"customerId": "cm…", "customerName": "Ana López", "customerPhone": "5512345678",
"deliveryAddress": "Durango 210, Roma Nte.", "currency": "MXN",
"subtotal": 11000, "discount": 0, "tax": 1517, "serviceCharge": 0, "deliveryFee": 3500, "tip": 0,
"total": 14500, "paidAmount": 14500,
"items": [{ "id": "cm…", "productId": "cm_prod", "name": "Taco al pastor", "qty": 2, "unitPrice": 4000, "total": 8000,
"modifiers": [{ "id": "cm_mod", "name": "Extra queso", "priceDelta": 1500 }], "notes": null, "status": "SENT" }],
"payments": [{ "id": "cm…", "method": "ONLINE", "provider": "RAPPI", "amount": 14500, "tip": 0, "status": "COMPLETED", "createdAt": "…" }],
"createdAt": "2026-10-22T18:30:00.000Z", "closedAt": "2026-10-22T18:30:01.000Z"
}
}/orderspermiso orders:writeCrea una orden desde un canal externo (agregador, app propia, kiosco de terceros). Los platillos se validan en el servidor: producto activo y disponible en la sucursal, modificadores válidos dentro de su mínimo/máximo, y precios siempre de Mi Restaurancito (los que envíes se ignoran). La orden se manda directo a cocina.
externalId(opcional, recomendado): tu folio. Si repites el mismo, regresamos la orden existente con200y"created": false— reintentar es seguro.type:TAKEOUT(defecto) |DELIVERY(requieredeliveryAddress) |COUNTER.source:ONLINE(defecto) |KIOSK|RAPPI|UBER|DIDI|WHATSAPP.customer{name, phone?, email?}: con teléfono de 10 dígitos se vincula al cliente existente o se crea uno nuevo.deliveryFee(centavos; por defecto el de la sucursal),aggregatorFeePct(0–0.6, comisión del canal para la rentabilidad),scheduledFor(ISO),notes.prepaid{provider?, reference?}: si el canal ya cobró, registramos un pago en línea por el total y la orden queda pagada.
curl -X POST https://mirestaurancito.com.mx/api/v1/orders \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{
"externalId": "RAPPI-88123",
"source": "RAPPI",
"type": "DELIVERY",
"deliveryAddress": "Durango 210, Roma Nte., CDMX",
"aggregatorFeePct": 0.25,
"customer": { "name": "Ana López", "phone": "55 1234 5678" },
"items": [
{ "productId": "cm_prod", "qty": 2, "modifierIds": ["cm_mod"] },
{ "productId": "cm_prod2", "qty": 1, "notes": "sin hielo" }
],
"prepaid": { "reference": "R-88123" }
}'{ "data": { "id": "cm_order", "number": 1042, "status": "PAID", "total": 14500, "currency": "MXN", … }, "created": true }Clientes
/customerspermiso readMás recientes primero. Filtros: q (nombre o correo), phone (10 dígitos), limit, cursor.
/customerspermiso customers:writeCampos: name (obligatorio), phone, email, birthday (YYYY-MM-DD), allergies y tags (arreglos), notes, marketingOptIn. Responde 409 customer_exists si el teléfono ya está registrado.
{
"data": { "id": "cm…", "name": "Luis Pérez", "phone": "5598765432", "email": "luis@ejemplo.com", "birthday": "1990-05-04",
"points": 0, "tier": "BRONZE", "totalSpent": 0, "visits": 0, "lastVisitAt": null,
"allergies": [], "tags": ["VIP"], "notes": null, "marketingOptIn": true, "createdAt": "…" }
}Inventario
/inventorypermiso readInsumos activos con existencia en la sucursal (unidad base), punto de reorden y costo por unidad en centavos. Parámetros: branchId, lowStock=true (solo los que están en o bajo su punto de reorden; sin paginar), limit, cursor.
{ "data": [{ "id": "cm…", "name": "Tortilla", "unit": "pz", "stock": 35, "reorderPoint": 50, "parLevel": 0,
"lowStock": true, "costPerUnit": 80, "currency": "MXN", "branchId": "cm…", … }], "hasMore": false, "nextCursor": null }/inventory/adjustmentspermiso inventory:writeRegistra un ajuste (type: "ADJUST", qty con signo) o una merma (type: "WASTE", siempre resta) en unidades base. Campos: itemId, qty, type, branchId, reason.
{ "data": { "itemId": "cm…", "name": "Tortilla", "branchId": "cm…", "type": "WASTE", "qty": -5, "unit": "pz", "stock": 35 } }Reportes y sucursales
/reports/salespermiso readVentas pagadas del periodo según la fecha de creación de la orden. from/to (por defecto los últimos 7 días, máximo 366), branchId opcional (si no, todas las sucursales).
{
"data": {
"from": "2026-10-22T06:00:00.000Z", "to": "2026-10-23T06:00:00.000Z", "timezone": "America/Mexico_City",
"branchId": null, "currency": "MXN", "orders": 1, "cancelledOrders": 0,
"total": 14500, "subtotal": 11000, "discount": 0, "tax": 1517, "serviceCharge": 0, "deliveryFee": 3500, "tips": 0,
"averageTicket": 14500,
"bySource": [{ "source": "RAPPI", "orders": 1, "total": 14500 }],
"byType": [{ "type": "DELIVERY", "orders": 1, "total": 14500 }],
"byPaymentMethod": [{ "method": "ONLINE", "payments": 1, "amount": 14500, "tips": 0 }],
"byDay": [{ "date": "2026-10-22", "orders": 1, "total": 14500 }]
}
}/branchespermiso readSucursales activas con dirección, teléfono, si aceptan pedidos en línea y domicilio, costo de envío y pedido mínimo (centavos).
/mepermiso readOrganización, permisos de la llave y número de sucursales. Útil para probar la conexión.
Webhooks
Configúralos en Integraciones y API → Webhooks: URL (https en producción), eventos (o * para todos) y un secreto whsec_… que se muestra al crear o rotar. Cada evento llega como POST con JSON:
{
"id": "evt_9HXbIXfGgw8nPPsyj4ANezEx",
"event": "order.paid",
"createdAt": "2026-10-22T18:30:01.000Z",
"data": { "id": "cm_order", "number": 1042, "source": "RAPPI", "status": "PAID", "total": 14500, … }
}order.created | Orden creada |
order.updated | Orden actualizada |
order.paid | Orden pagada |
order.cancelled | Orden cancelada |
customer.created | Cliente creado |
stock.low | Inventario bajo |
reservation.created | Reservación creada |
invoice.stamped | Factura timbrada |
giftcard.issued | Tarjeta de regalo emitida |
giftcard.redeemed | Tarjeta de regalo redimida |
webhook.test | Solo con el botón «Enviar evento de prueba» |
Los montos de data van en centavos. El contenido de data depende del evento; trátalo como extensible (podemos agregar campos).
Encabezados
X-Sazon-Signature: t=<unix>,v1=<hex>— HMAC-SHA256 con tu secreto sobre`${t}.${cuerpo}`.X-Sazon-Event(nombre del evento) yX-Sazon-Delivery(id de la entrega).
Reintentos
Una entrega es exitosa si respondes 2xx en menos de 10 s. Si no, reintentamos hasta 3 intentos en total (inmediato, +5 s y +30 s). No seguimos redirecciones. Cada entrega, con su respuesta, queda en el historial del webhook. Un evento puede llegar más de una vez: usa id para no procesarlo dos veces.
Verificar la firma (Node.js)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.RESTAURANCITO_WEBHOOK_SECRET; // whsec_…
// Importante: usa el cuerpo CRUDO, tal cual llegó (no lo vuelvas a serializar).
app.post("/webhooks/mi-restaurancito", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Sazon-Signature") ?? "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
const body = req.body.toString("utf8");
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${body}`).digest("hex");
const valid =
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - t) < 300; // tolerancia de 5 minutos
if (!valid || !fresh) return res.status(400).send("firma inválida");
const event = JSON.parse(body);
// Idempotencia: el mismo event.id puede llegar más de una vez si reintentamos.
console.log(event.event, event.id, event.data);
res.sendStatus(200); // responde 2xx rápido; procesa en segundo plano
});
app.listen(3001);