Saltar al contenido

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:

Terminal
curl https://mirestaurancito.com.mx/api/v1/me \
  -H "Authorization: Bearer sk_live_TU_LLAVE"
Respuesta 200
{
  "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.

PermisoQué permite
readLectura (menú, órdenes, clientes, inventario, reportes, sucursales)
orders:writeCrear órdenes (canales externos, app propia)
menu:writeCambiar precio y disponibilidad de productos
customers:writeCrear clientes
inventory:writeRegistrar ajustes y mermas de inventario
syncServidor 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 filtros from/to aceptan YYYY-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) y cursor = el nextCursor de la página anterior. Cuando hasMore es false no hay más.
  • Sucursal: los recursos operativos aceptan branchId; si lo omites se usa la primera sucursal activa (consulta GET /branches).

Errores

Todos los errores tienen el mismo formato y un código estable que puedes usar en tu código:

Respuesta 422
{ "error": { "code": "product_unavailable", "message": "items[0]: Taco al pastor está agotado." } }
400validation_error, invalid_json, invalid_date, invalid_cursor, invalid_status, invalid_source, invalid_range
401unauthorized — falta la llave, es inválida o fue revocada
403insufficient_scope — la llave no tiene el permiso; module_disabled — el plan no incluye la API
404not_found, branch_not_found
409customer_exists
422product_not_found, product_unavailable, invalid_modifier, order_rejected
429rate_limited — revisa el encabezado Retry-After
500internal_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

GET/orderspermiso read

Más recientes primero. Filtros: from, to, status (OPEN | PAID | CANCELLED), source (POS, WAITER, QR, ONLINE, KIOSK, RAPPI, UBER, DIDI, WHATSAPP), branchId, limit, cursor.

Petición
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_…"
GET/orders/:idpermiso read
Respuesta 200
{
  "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"
  }
}
POST/orderspermiso orders:write

Crea 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 con 200 y "created": false — reintentar es seguro.
  • type: TAKEOUT (defecto) | DELIVERY (requiere deliveryAddress) | 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.
Petición
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" }
  }'
Respuesta 201
{ "data": { "id": "cm_order", "number": 1042, "status": "PAID", "total": 14500, "currency": "MXN", … }, "created": true }

Clientes

GET/customerspermiso read

Más recientes primero. Filtros: q (nombre o correo), phone (10 dígitos), limit, cursor.

POST/customerspermiso customers:write

Campos: 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.

Respuesta 201
{
  "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

GET/inventorypermiso read

Insumos 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.

Respuesta 200
{ "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 }
POST/inventory/adjustmentspermiso inventory:write

Registra un ajuste (type: "ADJUST", qty con signo) o una merma (type: "WASTE", siempre resta) en unidades base. Campos: itemId, qty, type, branchId, reason.

Respuesta 201
{ "data": { "itemId": "cm…", "name": "Tortilla", "branchId": "cm…", "type": "WASTE", "qty": -5, "unit": "pz", "stock": 35 } }

Reportes y sucursales

GET/reports/salespermiso read

Ventas 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).

Respuesta 200
{
  "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 }]
  }
}
GET/branchespermiso read

Sucursales activas con dirección, teléfono, si aceptan pedidos en línea y domicilio, costo de envío y pedido mínimo (centavos).

GET/mepermiso read

Organizació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:

Cuerpo
{
  "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.createdOrden creada
order.updatedOrden actualizada
order.paidOrden pagada
order.cancelledOrden cancelada
customer.createdCliente creado
stock.lowInventario bajo
reservation.createdReservación creada
invoice.stampedFactura timbrada
giftcard.issuedTarjeta de regalo emitida
giftcard.redeemedTarjeta de regalo redimida
webhook.testSolo 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) y X-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)

server.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);