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 peticionesGET.
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ólocationIdy no corresponde a ningún local activo del workspace (inexistente o desactivado).405 Method Not Allowed: se intentó un método distinto deGET.500 Internal Server Error: error no controlado en el servidor.
Operaciones — /api/restaurante
| Operación | Método y ruta | Descripción |
|---|---|---|
| Carta completa | GET /api/restaurante | Locales 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 (locationIdsvacío = sirve a todos). Sin este parámetro, cada plato lleva su precio general ysoldOutestruesi está agotado en cualquier local. Si el id no corresponde a un local activo del workspace (inexistente o desactivado), la respuesta es404— nunca se calcula un precio o un agotado para un local que no aparece enlocations.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). ConincludeUnavailable=truese devuelven todas las cartas publicadas (que cumplan los demás filtros), cada una con su banderaavailablereal.
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).
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)
curl "https://cms.begraffic.com/api/restaurante?locationId=local-centro&includeUnavailable=true" \
-H "Authorization: Bearer TU_TOKEN"Respuesta
{
"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:
currencyes 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) ypriceMinor(dentro devariants/extras) van en unidad menor de la moneda (1250 = 12,50 € sicurrencyno tiene decimales cero; nunca dividas por 100 a mano — usafromMinorUnitsi consumes este API desde otro proyecto de Be Graffic, o el equivalente en tu stack).soldOutya viene calculado como booleano para el local pedido (o para "cualquier local" sinlocationId); el API nunca expone el mapa internolocationId → fecha de caducidadcon el que se guarda internamente.locationsincluye todos los locales activos del workspace, se filtre o no porlocationId— úsalo para pintar dirección, teléfono o el horario de apertura del local.locations[].coverImagees la portada del local para su cabecera pública, con la misma forma que las entradas demenus[].dishes[].images(variantesoriginal/medium/small/thumbnail). Esnullcuando el local no tiene portada configurada — pinta una cabecera sólida en ese caso, no un hueco roto.menus[].coverImageymenus[].categories[].coverImageson las imágenes de referencia de la carta y de cada categoría, con exactamente la misma forma quelocations[].coverImage— pensadas para pintar cartas y categorías como tarjetas con foto en la web del cliente. También sonnullcuando no hay imagen: pinta una tarjeta con fondo sólido, no un hueco roto.menusincluye solo cartas constatus: published; los platos de cada carta, solo constatus: published.- Las descripciones (
menus[].description,menus[].categories[].description,menus[].dishes[].description) llegan como HTML ya saneado (sin<script>, sin manejadoreson*). - Las URLs dentro de
imagesycoverImageapuntan 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,pricees el precio general (puede no ser el que cobra ese local);soldOutse activa si está agotado en cualquier local del workspace, no necesariamente el tuyo; yavailablese 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:availablees optimista (evita esconder la carta entera de la marca solo porque un local está cerrado ahora mismo) ysoldOutes pesimista (evita vender un plato que no queda en la mesa). SinlocationIdninguno 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
menuIdcuando 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 demenusfuera de esa franja — no llega conavailable: false, no llega. UsaincludeUnavailable=truesi 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:
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: publisheden cartas y platos,active: trueen locales) — ambas superficies leen por el mismo sitio, así que no pueden divergir. - El
slugdel 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
- 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.
- 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.
- 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.
- 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.
- 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.