Integra Menumetrik con tu POS / ERP

Esta guía es para el área técnica del restaurante. Menumetrik empuja cada comanda pagada a tu sistema en tiempo real (webhook firmado) y expone una API de consulta para conciliar. El contrato lo define Menumetrik y está versionado — tu integración no se rompe sin aviso.

Cómo funciona

1 · Webhook (push)

El dueño registra la URL https de tu servidor en su portal (sección Integra tu sistema). Cada vez que una comanda queda pagada, Menumetrik hace un POST JSON firmado a esa URL con el detalle completo: mesa, líneas, totales, pagos por método y propina.

2 · API de consulta (pull)

Con la API key del restaurante puedes pedir las comandas pagadas desde una fecha. Úsala para reconciliar si tu servidor estuvo caído — devuelve exactamente el mismo formato que el webhook.

Las credenciales (signing secret y API key) las genera el dueño de la cuenta en menumetrik.com → Portal → Integra tu sistema. Se muestran una sola vez — pídeselas por un canal seguro.

El webhook, a detalle

Verificar la firma (obligatorio)

Cada POST lleva X-Mmk-Signature: t=<unix>,v1=<hex> donde v1 = HMAC_SHA256(secret, `{t}.{cuerpo_crudo}`). Valida la firma con comparación de tiempo constante y rechaza si t tiene más de 5 minutos (anti-replay).

Node.js / Express

const crypto = require("crypto");

function verifyMmk(req, secret) {
  const sig = req.get("X-Mmk-Signature") || "";          // "t=1722690000,v1=ab12..."
  const t = sig.match(/t=(\d+)/)?.[1];
  const v1 = sig.match(/v1=([a-f0-9]+)/)?.[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;   // 5 min
  const expected = crypto.createHmac("sha256", secret)
    .update(`${t}.${req.rawBody}`)                    // cuerpo CRUDO, sin re-serializar
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Python / Flask · FastAPI

import hmac, hashlib, time

def verify_mmk(signature_header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(secret.encode(),
                        f"{t}.".encode() + raw_body,     # cuerpo CRUDO
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

⚠️ Firma sobre el cuerpo crudo del request. Si tu framework parsea el JSON y lo re-serializa, la firma no va a coincidir — usa el raw body.

El payload (check.paid)

IDs como string, montos con 2 decimales en MXN, fechas en hora de CDMX (America/Mexico_City). El campo api_version te garantiza que el shape no cambia sin aviso.

{
  "id": "evt_ab12cd34ef56...",         // único por entrega — deduplica con esto
  "type": "check.paid",
  "api_version": "2026-08-03",
  "created_at": "2026-08-03 14:22:10",
  "timezone": "America/Mexico_City",
  "currency": "MXN",
  "restaurant_id": "4503599627370497",
  "data": {
    "check": {
      "id": "12",
      "restaurant_id": "4503599627370497",
      "table_id": "3",
      "table_label": "Mesa 4 + 5",      // mesas combinadas incluidas
      "waiter_staff_id": "7",
      "managed_by": "both",             // waiter | diner | both
      "status": "paid",
      "opened_at": "2026-08-03 13:01:44",
      "closed_at": "2026-08-03 14:22:08",
      "members": [                       // comensales de la mesa
        { "member_id": "21", "diner_id": "9", "display_name": "Juan",
          "group_id": "1", "status": "active" }
      ],
      "groups": [                        // cuentas separadas dentro de la mesa
        { "id": "1", "name": "Cuenta de Juan", "total_list": 190.00,
          "total_qtapp": 184.30, "paid_total": 190.00, "pending_total_list": 0.00 }
      ],
      "orders": [                        // líneas con precio CONGELADO al ordenar
        { "id": "55", "name": "Tacos al pastor", "qty": 2,
          "unit_price_list": 95.00, "unit_price_qtapp": 92.15,
          "line_total_list": 190.00, "line_total_qtapp": 184.30,
          "member_id": "21", "member_name": "Juan", "group_id": "1",
          "item_id": "14", "package_id": null, "station_id": "2",
          "notes": null, "status": "delivered", "created_by": "diner" }
      ],
      "totals": {
        "total_list": 190.00,           // total a precio de menú
        "total_qtapp": 184.30,          // total si todo se paga con QTApp
        "qtapp_savings": 5.70,
        "paid_total": 190.00,           // ya cubierto (equivalente lista)
        "pending_total_list": 0.00
      },
      "payments": [                      // SOLO completados
        { "id": "31", "method": "qtapp",       // cash | card | qtapp
          "amount": 184.30, "tip_amount": 27.65,
          "status": "completed", "group_id": "1", "member_id": null,
          "external_ref": "MMK-31",
          "qtapp_transaction_id": "8837...",   // referencia del pago real
          "completed_at": "2026-08-03 14:22:08" }
      ]
    }
  }
}

Notas para tu contabilidad

  • payments[].method: cash/card se cobraron con la terminal del restaurante (Menumetrik solo tipifica); qtapp son operaciones liquidadas al restaurante a través de la infraestructura de QTApp y las instituciones financieras autorizadas que la respaldan.
  • La propina (tip_amount) nunca lleva el descuento QTApp y viaja aparte del consumo.
  • Los pagos qtapp traen qtapp_transaction_id para cruzar contra el estado de cuenta QTApp.
  • Una mesa puede pagar mixto (unos en efectivo, otros por QTApp, por grupo o por la mesa completa).

API de consulta (conciliación)

curl "https://backend.tkm.technology/api/menumetrik/integration/checks?since=2026-08-01&limit=50" \
  -H "X-Mmk-Api-Key: mmk_live_..."

# → { "api_version": "2026-08-03", "count": 2, "checks": [ { ...mismo shape que data.check... } ] }

Buenas prácticas y seguridad

¿Aún no usas Menumetrik en tu restaurante?

Es gratis — solo necesitas tu Cuenta Platino en QTApp.

Conoce Menumetrik para restaurantes