API de FlavosoGuíasIntegración con caja

Integración con caja

Conectar una caja a Flavoso de principio a fin: conexión, sincronización de la carta, hacerse cargo de los pedidos y comunicar su estado, pausa, horarios de apertura y conciliación diaria.

El objetivo

Al terminar esta guía, la caja gestiona la carta y Flavoso es un canal de pedidos más: los pedidos online llegan a la caja como ticket, los estados vuelven al cliente y por la noche las ventas cuadran.

Caja

lleva artículos, precios, tickets y el cierre del día

Flavoso

página de pedidos, QR de mesa, app móvil

Caja → Flavoso

  • PUT /menuSincronizar la carta
  • POST /menu/availabilityComunicar platos agotados
  • POST /orders/{id}/acknowledgeHacerse cargo del pedido, con número de ticket
  • POST /orders/{id}/statusComunicar el estado: aceptado, listo
  • POST /store/pausePausa, horarios de apertura, días especiales

Flavoso → Caja

  • order.createdPedido nuevo, por webhook o consulta
  • order.cancelledPedido cancelado
  • menu.* · store.updatedCarta u horarios cambiados en el panel
  • GET /reports/salesVentas del día para conciliar

Los permisos necesarios: orders:write, menu:write, store:write y reports:read; con reservas, además reservations:write.

1. Conectar

En tu caja hay un botón «Conectar con Flavoso». Inicia el flujo de OAuth; el propietario inicia sesión, elige su negocio y da su consentimiento. Tu caja guarda cifrados los tokens de cada conexión, junto con tenant.id y connection_id.

Después llama una vez a GET /me y guarda la zona horaria, la moneda, el país, livemode y el created_at de la conexión. Tu webhook recibe app.connected.

Una caja que funciona en un dispositivo del local suele ser un cliente público sin secret. Si tu caja funciona en la nube, usa un cliente confidencial e intercambia los tokens en el servidor. Un cliente público puede volver por loopback o un esquema propio; uno confidencial solo conecta negocios reales a través de https.

Primera conexión

Un negocio que conecta tu caja suele tener ya una carta, pedidos anteriores y eventos. Tres pasos antes de que la caja sincronice y recoja algo por primera vez, para que nada se cree por duplicado ni se registre a destiempo:

  1. 1

    Adoptar la carta existente

    Una sincronización solo reconoce los objetos que llevan un identificador de tu app; por eso, un primer PUT /menu crea la carta existente por segunda vez. Léela antes con GET /menu, asocia los objetos a tus artículos y fija el external_id de cada objeto con PATCH. En el negocio de prueba lo hace «Asignar identificadores», en la pestaña Carta de la consola. Una simulación muestra los duplicados en summary.*.created.

  2. 2

    Dejar fuera los pedidos anteriores

    GET /orders?acknowledged=false no tiene límite de tiempo. Consulta con created_since y el created_at de la conexión de GET /me, y hazte cargo de cada pedido que trates, incluso de uno cancelado que no registres.

  3. 3

    Eventos a partir de ahora

    Sin after, GET /events devuelve los últimos 30 días, incluidos los eventos anteriores a la conexión. La primera vez, consulta con after=latest y guarda next_after para esta conexión.

# 1. The connection itself: remember created_at, it bounds the order backlog
curl https://api.flavoso.com/v1/me -H "Authorization: Bearer $ACCESS_TOKEN"

# 2. Take over the menu that is already there: read it, map it to your articles and
#    attach your article number to every object. Categories, option groups and options
#    work the same way with their own PATCH route.
curl https://api.flavoso.com/v1/menu -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X PATCH https://api.flavoso.com/v1/menu/products/PRODUCT_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "ART-1001"}'
# Then PUT /menu with "dry_run": true: summary.*.created only counts what is really new.

# 3. Only the orders since the connection, never the whole history
curl "https://api.flavoso.com/v1/orders?acknowledged=false&created_since=2026-10-04T17:40:00.000Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 4. Events from now on: store next_after for this connection
curl "https://api.flavoso.com/v1/events?after=latest" -H "Authorization: Bearer $ACCESS_TOKEN"

3. Agotado

Cuando la caja marca algo como agotado, envías POST /menu/availability con hasta 500 entradas a la vez, por external_id o por identificador. available es siempre el estado de destino, nunca un interruptor: dos veces «agotado» sigue siendo agotado.

curl -X POST https://api.flavoso.com/v1/menu/availability \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "items": [
    {
      "type": "product",
      "external_id": "ART-1001",
      "available": false
    },
    {
      "type": "option",
      "external_id": "OPT-SALAMI",
      "available": true
    },
    {
      "type": "product",
      "id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
      "available": true
    }
  ]
}'

4. Recibir pedidos

Un pedido nuevo llega como order.created. Con pago en el local, de inmediato; con pago online, en cuanto se confirma el pago. Un pedido online sin pagar no lo ves nunca.

  • Caja en la nube: por webhook, en pocos segundos.
  • Caja en el local sin dirección pública: con GET /events, cada pocos segundos, types=order.*,menu.*,store.*,app.*, con un after por conexión. Así no puede leer app.disconnected, porque con la desconexión sus tokens dejan de ser válidos: reconoce una desconexión por un 401 seguido de invalid_grant al renovar.
  • En ambos casos, como red de seguridad: GET /orders?acknowledged=false&created_since=… con el created_at de la conexión, en cada arranque y periódicamente. Ahí aparecen los pedidos de los que aún no te has hecho cargo.
En los eventos, external_id y acknowledged_at aparecen siempre como null, aunque tu app ya se haya hecho cargo del pedido: el mismo sobre va a todos los destinatarios. Tu sistema decide por el identificador del pedido si ya está registrado, o lo consultas con GET /orders/{id}.
// The queue worker behind your webhook endpoint (or behind pollEvents for a local POS)
export async function handleEvent(event) {
  const conn = await connectionFor(event.connection_id); // your stored tokens for this restaurant
  if (!conn) return; // not (or no longer) connected

  // external_id and acknowledged_at are always null in events (one envelope for every
  // recipient): your own records, keyed by the order id, say what is booked already.
  switch (event.type) {
    case "order.created": {
      const order = event.data.object;
      const receipt = await pos.book(order, { testOnly: !event.livemode }); // idempotent per order.id
      await flavoso(conn, "POST", `/orders/${order.id}/acknowledge`, {
        body: { external_id: receipt.number },
        idempotencyKey: `ack-${order.id}`,
      });
      break;
    }
    case "order.cancelled":
      await pos.voidReceiptForOrder(event.data.object.id);
      break;
    case "app.disconnected":
      // Webhook only: GET /events never delivers it, a polling POS sees 401 and invalid_grant
      await forgetConnection(event.connection_id); // no token of it works any more
      break;
  }
}

5. Hacerse cargo con un número de ticket propio

Cuando el pedido está registrado en la caja, confirmas con POST /orders/{id}/acknowledge que te has hecho cargo de él, con tu número de ticket como external_id. Con ello, Flavoso fija acknowledged_at (la primera vez) y external_id, y el pedido sale de GET /orders?acknowledged=false.

  • Idempotente: hacerse cargo dos veces no cambia nada. Un external_id distinto sustituye al anterior.
  • Si tu número de ticket ya está asociado a otro pedido de tu app, Flavoso responde con 409 external_id_taken.
  • Hazte cargo también de un pedido que no registres, por ejemplo uno que ya estaba cancelado, si hace falta sin external_id: si no, se queda en GET /orders?acknowledged=false.
  • Hacerse cargo no es aceptar. Si la cocina acepta el pedido, lo comunicas por separado (paso siguiente). Muchas cajas hacen las dos cosas una detrás de otra.
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/acknowledge \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ack-ORDER_ID" \
  -d '{"external_id": "R-2026-000123"}'

6. Aceptar y hacer avanzar

Cuando la caja acepta, llamas a POST /orders/{id}/accept. El pedido pasa al siguiente estado que usa el negocio, normalmente confirmed (si ha desactivado confirmed, preparing y ready, completed); un pedido que ya ha pasado de pending vuelve sin cambios, uno terminado o cancelado responde con 409. Después avanzas con POST /orders/{id}/status: preparing, ready, completed. El cliente recibe su mensaje, y la pantalla de cocina y el panel muestran el estado al momento, como con un clic en el panel.

Puedes saltar hacia delante, por ejemplo de confirmed directamente a ready. Si el negocio no usa un estado, Flavoso responde con 409 stage_not_used; entonces te lo saltas. Rechazar (reject) solo es posible mientras el pedido esté en pending; cancelar (cancel), desde cualquier estado abierto. El flujo en detalle está en Flujo de pedidos.

# The cashier accepted the order in the POS
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/accept \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Idempotency-Key: accept-ORDER_ID"

# The kitchen printer printed the ticket / the food is ready / handed over
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/status \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ready-ORDER_ID" \
  -d '{"status": "ready"}'

Líneas, opciones e impuestos

Cada línea lleva product_id. Con los identificadores de la sincronización encuentras tu artículo; GET /menu/products/{id} muestra además el external_id de tu app. Las opciones aparecen en la línea con su nombre, su suplemento, su id y el group_id y el group_name de su grupo, para que encuentres tu propio modificador (null en pedidos anteriores al 8 de octubre de 2026); unit_price ya las incluye.

tax.lines desglosa el pedido por tipo impositivo, con las líneas del pedido, los gastos de envío y la tarifa de servicio, con el mismo cálculo que el ticket y la exportación DATEV. La propina (amounts.tip) no cuenta como venta y no aparece en ninguna línea de impuestos. Una parte sin tipo guardado lleva rate_bps: null; consulta Líneas de impuestos.

payment.method indica si se paga online (online) o en el local (on_site), y payment.status, si ya está pagado. Un pedido online llega siempre pagado; un pedido con pago en el local sigue en open, porque Flavoso no registra los pagos en el local, y no comunica order.paid.

7. Pausa, horarios de apertura, días especiales

Cuando la cocina no da abasto, la caja pausa los pedidos online: POST /store/pause con minutes (de 1 a 720) o until (con zona horaria) y, opcionalmente, una note de hasta 140 caracteres; sin minutes ni until, la pausa dura 30 minutos. Termina siempre sola, como máximo a las doce horas; POST /store/resume la termina antes.

Si la caja lleva los horarios de apertura, los fija con PUT /store/hours: como máximo dos franjas por día de la semana, que no se solapen, y un día que no aparezca se considera cerrado. Los días especiales los fija con POST /store/closures. Los tiempos de preparación y los tipos de pedido los cambia PATCH /store, con las mismas comprobaciones que en el panel.

Si el hostelero cambia algo de esto en el panel, recibes store.updated con changed.

# Kitchen at capacity: no new online orders for 30 minutes
curl -X POST https://api.flavoso.com/v1/store/pause \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"minutes": 30, "note": "Kitchen at capacity"}'

# Open again before the time is up
curl -X POST https://api.flavoso.com/v1/store/resume -H "Authorization: Bearer $ACCESS_TOKEN"

# Opening hours from the POS: weekday 0 = Monday, at most two windows per weekday
# that do not overlap; a weekday that is missing is closed
curl -X PUT https://api.flavoso.com/v1/store/hours \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "opening_hours": [
    {
      "weekday": 0,
      "open": "11:30",
      "close": "14:30"
    },
    {
      "weekday": 0,
      "open": "17:30",
      "close": "22:00"
    },
    {
      "weekday": 5,
      "open": "12:00",
      "close": "23:00"
    }
  ],
  "delivery_hours": []
}'

# Closed on Christmas Eve, shorter on New Year's Eve
curl -X POST https://api.flavoso.com/v1/store/closures \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"date": "2026-12-24", "closed": true, "note": "Christmas Eve"}'
curl -X POST https://api.flavoso.com/v1/store/closures \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"date": "2026-12-31", "hours": [{"open": "11:30", "close": "16:00"}]}'

# Lead times and order types
curl -X PATCH https://api.flavoso.com/v1/store \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"lead_times": {"pickup_minutes": 25, "delivery_minutes": 50}}'

8. Conciliar las ventas del día

Después de medianoche en la zona horaria del negocio, la caja obtiene el día anterior: GET /reports/sales?from=…&to=…&group_by=day. Bruto, neto, impuestos por tipo, propinas, tarifas, descuentos, por método de pago y tipo de pedido, con las mismas reglas que las cifras del panel. Si algo difiere de tu cierre para el canal Flavoso, muéstralo. Más en Informes.

# Yesterday, as one bucket per day, in the restaurant's local time
curl "https://api.flavoso.com/v1/reports/sales?from=2026-10-03&to=2026-10-03&group_by=day" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Casos de error y reintentos

CasoQué hace la caja
La caja estaba apagadaAl arrancar, GET /events desde el after guardado para esta conexión, y GET /orders?acknowledged=false&created_since=…. Los eventos se conservan 30 días, y los webhooks se reintentan durante casi dos días. Si tu after ya no se conserva, sigue por el evento conservado más antiguo y la respuesta indica gap: true: entonces recupera además con GET /orders?updated_since=…. Una conexión nueva empieza con after=latest.
Sin respuesta a un POSTLa misma solicitud otra vez con la misma Idempotency-Key.
401 unauthorizedRenovar el token y repetir una vez. Si la renovación falla con invalid_grant: mostrar «Conexión con Flavoso interrumpida» y ofrecer «Volver a conectar». Con unauthorized_client, la app está bloqueada: conservar los tokens, mostrar «bloqueada temporalmente» y volver a intentarlo más tarde.
409 invalid_transitionEl pedido se ha cambiado en otro sitio, por ejemplo se ha cancelado en el panel. Volver a leerlo y ajustar el ticket.
409 external_id_takenEl número de ticket ya está en uso. Revisar la numeración de la caja.
429 rate_limitedEsperar Retry-After. El límite se aplica por negocio.
app.disconnectedBorrar los tokens de esta conexión, no enviar nada más e informar al propietario en la caja. Una caja sin webhook se entera de la desconexión por un 401 seguido de invalid_grant al renovar.
order.cancelledAnular el ticket e informar a la cocina.

Lista de comprobación

  • Tokens guardados cifrados por conexión, renovación en un único sitio.
  • En la primera conexión, carta existente adoptada antes del primer PUT /menu.
  • Firma de cada webhook comprobada, duplicados reconocidos por el id, o GET /events con un after guardado por conexión.
  • GET /orders?acknowledged=false con created_since como red de seguridad al arrancar y periódicamente; tu caja se hace cargo de cada pedido tratado, también de los cancelados.
  • Cada POST con Idempotency-Key, derivada del identificador del pedido y del paso.
  • livemode: false nunca se registra en la caja real.
  • stage_not_used, invalid_transition, app.disconnected y una app bloqueada (unauthorized_client) tratados.
  • Probado de principio a fin con el negocio de prueba: pedido de prueba, hacerse cargo, aceptar, listo, cancelación en la consola, pausa, conciliación.

¿Algo no ha quedado claro? Escribe a support@flavoso.com e indica el identificador de X-Request-Id. Así podremos encontrar tu solicitud en el registro.