Be CMS · Docs
← Toda la documentación

Manual de uso del API de Restaurante (solo lectura)

El API de restaurante expone un endpoint de solo lectura para consultar la carta publicada (locales, cartas, categorías y platos) de tu workspace. Vive en /api/restaurante y devuelve JSON.

Contenido monolingüe (español). Todos los textos (nombres, descripciones, categorías, alérgenos, etiquetas) se guardan y se sirven en español; el API no traduce ni admite variantes de idioma.

Autenticación

  • Incluye Authorization: Bearer <token> en cada solicitud.
  • El token debe estar activo y tener permisos sobre el recurso que consultas.
  • El workspace se determina siempre desde el token; nunca lo envías en la solicitud.

Permisos

  • read: permite ejecutar peticiones GET.

Si el token está vencido, inactivo o carece del permiso read, la respuesta será 401 o 403.

Códigos de respuesta comunes

  • 200 OK: respuesta exitosa (incluye el caso sin resultados: menus: []).
  • 401 / 403: token ausente, inválido o sin permisos.
  • 404 Not Found: se pasó locationId y no corresponde a ningún local activo del workspace (inexistente o desactivado).
  • 405 Method Not Allowed: se intentó un método distinto de GET.
  • 500 Internal Server Error: error no controlado en el servidor.

Operaciones — /api/restaurante

OperaciónMétodo y rutaDescripción
Carta completaGET /api/restauranteLocales activos y cartas publicadas, con sus categorías y platos.

GET

Query params (todos opcionales)

  • locationId (string): resuelve el precio y el agotado de cada plato para ese local, y limita las cartas a las que sirven a ese local (locationIds vacío = sirve a todos). Sin este parámetro, cada plato lleva su precio general y soldOut es true si está agotado en cualquier local. Si el id no corresponde a un local activo del workspace (inexistente o desactivado), la respuesta es 404 — nunca se calcula un precio o un agotado para un local que no aparece en locations.
  • menuId (string): devuelve solo esa carta (si está publicada).
  • includeUnavailable (true | ausente): por defecto solo se devuelven las cartas dentro de su ventana horaria (available: true). Con includeUnavailable=true se devuelven todas las cartas publicadas (que cumplan los demás filtros), cada una con su bandera available real.

Ejemplo básico (sin locationId: available y soldOut responden con sesgos opuestos — ver "Buenas prácticas" más abajo antes de confiar en ellos para un local concreto).

bash
curl "https://cms.begraffic.com/api/restaurante" \
  -H "Authorization: Bearer TU_TOKEN"

Ejemplo para un local concreto (el uso típico: la web de un local pinta su propia carta)

bash
curl "https://cms.begraffic.com/api/restaurante?locationId=local-centro&includeUnavailable=true" \
  -H "Authorization: Bearer TU_TOKEN"

Respuesta

json
{
  "currency": "eur",
  "locations": [
    {
      "id": "local-centro",
      "name": "Bistró del Centro",
      "slug": "bistro-del-centro",
      "address": "Calle Mayor 12, Madrid",
      "phone": "+34600111222",
      "place": { "lat": 40.4168, "lng": -3.7038, "placeId": "ChIJ..." },
      "coverImage": {
        "id": "img-0",
        "url": "https://cms.begraffic.com/api/storage/proxy?path=tables%2F...",
        "alt": "Fachada del Bistró del Centro",
        "storageBasePath": "tables/.../restaurantLocations/...",
        "originalFileName": "fachada.jpg",
        "variants": {
          "original": { "url": "...", "storagePath": "...", "width": 1600, "height": 1200, "size": 0, "contentType": null },
          "medium": { "url": "...", "storagePath": "...", "width": 800, "height": 600, "size": 0, "contentType": null },
          "small": { "url": "...", "storagePath": "...", "width": 400, "height": 300, "size": 0, "contentType": null },
          "thumbnail": { "url": "...", "storagePath": "...", "width": 160, "height": 120, "size": 0, "contentType": null }
        }
      },
      "timezone": "Europe/Madrid",
      "schedule": [
        { "day": 1, "ranges": [{ "from": "13:00", "to": "16:00" }, { "from": "20:00", "to": "23:30" }] }
      ]
    }
  ],
  "menus": [
    {
      "id": "carta-principal",
      "name": "Carta",
      "slug": "carta",
      "description": "<p>Cocina de mercado.</p>",
      "availability": null,
      "available": true,
      "coverImage": {
        "id": "img-0",
        "url": "https://cms.begraffic.com/api/storage/proxy?path=tables%2F...",
        "alt": "",
        "storageBasePath": "tables/.../restaurantMenus/...",
        "originalFileName": "carta.jpg",
        "variants": {
          "original": { "url": "...", "storagePath": "...", "width": 1600, "height": 1200, "size": 0, "contentType": null },
          "medium": { "url": "...", "storagePath": "...", "width": 800, "height": 600, "size": 0, "contentType": null },
          "small": { "url": "...", "storagePath": "...", "width": 400, "height": 300, "size": 0, "contentType": null },
          "thumbnail": { "url": "...", "storagePath": "...", "width": 160, "height": 120, "size": 0, "contentType": null }
        }
      },
      "categories": [
        {
          "id": "cat-entrantes",
          "name": "Entrantes",
          "description": "",
          "order": 0,
          "coverImage": {
            "id": "img-1",
            "url": "https://cms.begraffic.com/api/storage/proxy?path=tables%2F...",
            "alt": "",
            "storageBasePath": "tables/.../restaurantMenus/...",
            "originalFileName": "entrantes.jpg",
            "variants": {
              "original": { "url": "...", "storagePath": "...", "width": 1600, "height": 1200, "size": 0, "contentType": null },
              "medium": { "url": "...", "storagePath": "...", "width": 800, "height": 600, "size": 0, "contentType": null },
              "small": { "url": "...", "storagePath": "...", "width": 400, "height": 300, "size": 0, "contentType": null },
              "thumbnail": { "url": "...", "storagePath": "...", "width": 160, "height": 120, "size": 0, "contentType": null }
            }
          }
        },
        { "id": "cat-principales", "name": "Principales", "description": "", "order": 1, "coverImage": null }
      ],
      "dishes": [
        {
          "id": "plato-1",
          "categoryId": "cat-entrantes",
          "name": "Croquetas de jamón",
          "description": "<p>Seis unidades, receta casera.</p>",
          "sku": "CROQ-01",
          "price": 950,
          "taxRate": 10,
          "soldOut": false,
          "variants": [],
          "extras": [{ "id": "opt-0-ab12cd", "label": "Ración doble", "priceMinor": 700 }],
          "images": [
            {
              "id": "img-0",
              "url": "https://cms.begraffic.com/api/storage/proxy?path=tables%2F...",
              "alt": "Croquetas de jamón",
              "storageBasePath": "tables/.../restaurantDishes/...",
              "originalFileName": "croquetas.jpg",
              "variants": {
                "original": { "url": "...", "storagePath": "...", "width": 1600, "height": 1200, "size": 0, "contentType": null },
                "medium": { "url": "...", "storagePath": "...", "width": 800, "height": 600, "size": 0, "contentType": null },
                "small": { "url": "...", "storagePath": "...", "width": 400, "height": 300, "size": 0, "contentType": null },
                "thumbnail": { "url": "...", "storagePath": "...", "width": 160, "height": 120, "size": 0, "contentType": null }
              }
            }
          ],
          "allergens": ["gluten", "lacteos"],
          "tags": ["recomendado"],
          "order": 0
        }
      ]
    }
  ]
}

Notas de la respuesta:

  • currency es la moneda propia del módulo Restaurante (código ISO-4217 en minúscula, p. ej. "eur", configurada en los ajustes de la sección — independiente de la moneda de Aportes/pagos del workspace); es la misma para toda la carta.
  • price (por plato) y priceMinor (dentro de variants/extras) van en unidad menor de la moneda (1250 = 12,50 € si currency no tiene decimales cero; nunca dividas por 100 a mano — usa fromMinorUnit si consumes este API desde otro proyecto de Be Graffic, o el equivalente en tu stack).
  • soldOut ya viene calculado como booleano para el local pedido (o para "cualquier local" sin locationId); el API nunca expone el mapa interno locationId → fecha de caducidad con el que se guarda internamente.
  • locations incluye todos los locales activos del workspace, se filtre o no por locationId — úsalo para pintar dirección, teléfono o el horario de apertura del local.
  • locations[].coverImage es la portada del local para su cabecera pública, con la misma forma que las entradas de menus[].dishes[].images (variantes original/medium/small/thumbnail). Es null cuando el local no tiene portada configurada — pinta una cabecera sólida en ese caso, no un hueco roto.
  • menus[].coverImage y menus[].categories[].coverImage son las imágenes de referencia de la carta y de cada categoría, con exactamente la misma forma que locations[].coverImage — pensadas para pintar cartas y categorías como tarjetas con foto en la web del cliente. También son null cuando no hay imagen: pinta una tarjeta con fondo sólido, no un hueco roto.
  • menus incluye solo cartas con status: published; los platos de cada carta, solo con status: published.
  • Las descripciones (menus[].description, menus[].categories[].description, menus[].dishes[].description) llegan como HTML ya saneado (sin <script>, sin manejadores on*).
  • Las URLs dentro de images y coverImage apuntan siempre a /api/storage/proxy, nunca directo a Firebase Storage.

Buenas prácticas

  • Si tu sitio representa un único local, pasa siempre locationId: sin él, price es el precio general (puede no ser el que cobra ese local); soldOut se activa si está agotado en cualquier local del workspace, no necesariamente el tuyo; y available se activa si la carta está en su ventana horaria en cualquiera de sus locales, tampoco necesariamente el tuyo. Estos dos últimos sesgos son opuestos y deliberados, no un descuido: available es optimista (evita esconder la carta entera de la marca solo porque un local está cerrado ahora mismo) y soldOut es pesimista (evita vender un plato que no queda en la mesa). Sin locationId ninguno de los dos te da la respuesta exacta para TU local — pásalo si necesitas precisión, sobre todo antes de mostrar disponibilidad o de dejar pedir un plato.
  • Filtra por menuId cuando solo necesites una carta (p. ej. la del mediodía): evita traer y descartar en el cliente el resto de cartas del workspace.
  • Sin includeUnavailable=true, una carta con ventana horaria (p. ej. el menú del día, de 13:00 a 16:00) simplemente desaparece de menus fuera de esa franja — no llega con available: false, no llega. Usa includeUnavailable=true si quieres mostrarla igualmente con un aviso de "no disponible ahora".
  • Maneja el código 405 (método no permitido) en tus integraciones.

Sin programar: la carta pública y su QR

No hace falta consumir este API para poner la carta delante de un comensal. Be CMS sirve una carta pública ya montada, pensada para leerse en el móvil a distancia de brazo y sin hacer zoom:

código
https://cms.begraffic.com/carta/{pageId}
https://cms.begraffic.com/carta/{pageId}?local={slug-del-local}
  • Sin local, si el workspace tiene varios locales activos, la página pregunta primero en cuál está el comensal. Con un solo local entra directo.
  • Con local, muestra la carta de ese local: sus precios, sus platos agotados y solo las cartas que ese local sirve dentro de su ventana horaria.
  • No requiere token ni sesión: es la página que abre quien escanea el código de la mesa. Aplica exactamente las mismas reglas de visibilidad que este API (status: published en cartas y platos, active: true en locales) — ambas superficies leen por el mismo sitio, así que no pueden divergir.
  • El slug del local se edita en el panel. Cambiarlo invalida los QR ya impresos, porque el enlace deja de resolver: si tienes códigos pegados en las mesas, cámbialo solo con intención.

Qué ve el comensal

  1. Portada del local. Su foto a sangre si la subiste (Restaurante → el local → portada), con el logo de la marca encima, el nombre, la dirección y si está abierto ahora mismo en la zona horaria del local. Sin foto, una cabecera sólida con el nombre en grande: se ve intencionada desde el primer día, así que no hace falta fotografiar nada para publicar.
    • El aviso de «Abierto ahora» solo aparece si el local tiene horario configurado. Sin horario no se afirma nada: decir «abierto» a las 4 de la mañana porque nadie lo rellenó es peor que no decirlo.
  2. Las cartas como tarjetas con su imagen (Restaurante → el local → Carta → editar carta → «Imagen de la carta»), si hay más de una. Con una sola carta se entra directo, sin un toque de más. Las cartas fuera de su ventana horaria aparecen igualmente, marcadas con «Ahora fuera de horario». Navegar entre niveles es instantáneo, sin volver al servidor — la carta se abre desde una mesa, a veces con mala cobertura — y el botón atrás del móvil sube un nivel, no saca al comensal de la carta.
  3. Dentro de una carta, sus categorías como tarjetas con su imagen (cada categoría tiene la suya en el editor de la carta; sin imagen, un fondo sólido intencionado). Al tocar una se ven sus platos. Una carta con una sola categoría enseña los platos directamente.
  4. Fila por plato con nombre, precio, descripción, alérgenos, distintivos y miniatura si tiene foto. Sin foto no se deja hueco gris: un recuadro vacío se lee como algo roto.
  5. Al tocar un plato, un modal con la foto grande, la descripción completa, los alérgenos con su nombre, y las variantes y extras con su precio.
    • Se cierra con el botón atrás del móvil, no solo con la ✕. Es el gesto que hace la gente, y sin recogerlo el navegador sacaría al comensal de la carta entera.
    • Si un plato no tiene alérgenos marcados, esa parte no se muestra. Un «Alérgenos: —» se leería como «no tiene», y eso no lo sabemos. Marcarlos es obligatorio por ley: hazlo desde el editor de plato.

Los platos agotados se ven tachados y con su etiqueta, resueltos para ese local: un plato agotado en el centro sigue disponible en la playa.

Generar el código QR

El panel trae el QR de cada local (Restaurante → el local → «QR de la carta»), con opción de copiar el enlace y descargar el SVG para llevarlo a imprenta.

Por debajo lo sirve una ruta que no es parte del API pública: requiere sesión del panel, exige pertenecer al workspace de esa carta, y solo acepta codificar direcciones del propio dominio. No la llames desde el sitio de un cliente — para eso basta con enlazar la URL de la carta de arriba.