openapi: 3.1.0
info:
  title: Tavoloo Integrations API
  version: 1.0.0
  summary: REST API and outbound webhooks that connect a restaurant's POS (point of sale) to Tavoloo.
  description: |
    Tavoloo is the service channel of a restaurant: menu, table ordering, waiter calls and the AI concierge.
    The restaurant's POS stays the system of record for the bill (total, closing, tax documents such as NFC-e in
    Brazil or sales tax in the US). This API lets a POS:

    * receive the orders, waiter calls and table sessions created in Tavoloo, by **webhook** (push) or by
      **polling** (`GET /events` + `POST /events/ack`);
    * send back the order status, the payment and the **table bill** (`PUT /tables/{tableNumber}/tab`). Closing the
      bill frees the table in Tavoloo;
    * read the menu, the tables and the open orders to reconcile state.

    ## Base URL

    `https://tavoloo.app/api/integrations/v1` is the stable URL. The raw function URL
    (`https://jprbxjjkzjpbwkypcbip.supabase.co/functions/v1/integrations-api/v1`) serves the same API and is a fallback.

    ## Authentication

    Create a connection in the Tavoloo panel (`/gestione/{slug}/settings/integracoes`). The panel shows the API key
    (`tvl_live_` followed by 48 hex characters) and the webhook secret (`whsec_` followed by 64 hex characters) **once**.
    Send the key on every call as `Authorization: Bearer tvl_live_...`. The tenant (the restaurant) is always the one the
    key belongs to; it is never read from the request. A revoked or rotated key stops working immediately.

    There is no CORS: this is a server-to-server API and a browser must never hold the key.

    ## Delivery model

    One queue, two delivery modes per connection: `webhook` or `polling`.

    * **At-least-once.** The same event can be delivered more than once. De-duplicate on the event `id`.
    * **Ordering.** The event `id` grows with insertion order. Polling returns events in ascending `id`. A webhook retry
      can arrive after a newer event, so order events by `id` (or `updated_at` inside the data) and drop stale ones.
    * **No history for a new connection.** Only events created after the connection became active are queued. Use
      `GET /orders`, `GET /tables` and `GET /menu` to load the current state.
    * **Reconciliation.** `GET /orders?status=open` is the source of truth. Run it on start-up and every few minutes.
    * **Retention.** Events and deliveries are deleted after 30 days.
    * **No echo for the POS bill.** The mirror order created from `PUT /tables/{tableNumber}/tab` never produces an event.
      Status changes you make to app orders do come back as `order.updated`; treat them as idempotent.

    ## Order status vocabulary

    Events and `GET /orders` use the stored statuses `pending`, `confirmed`, `preparing`, `out_for_delivery`,
    `delivered` and `canceled`. `POST /orders/{orderId}/status` accepts `confirmed`, `preparing`, `ready`, `delivered`
    and `canceled`, where `ready` is stored as `out_for_delivery`.

    ## Limits

    * 600 requests per minute per connection (fixed 1 minute window). Over the limit: `429` with `Retry-After`.
    * Request body up to 256 KiB (`413` above).
    * `limit` on list endpoints: default 50, capped at 100.
    * `POST /events/ack`: 1 to 500 ids. `PUT /tables/{tableNumber}/tab`: at most 500 lines and 50 payments.

    ## Errors

    Every error is `{"error": {"code": "...", "message": "..."}}`. Messages are generic; the code is the contract.

    ## Webhook signature

    Each webhook request carries `Tavoloo-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is
    `hex(HMAC_SHA256(secret, t + "." + rawBody))`. The key is the whole secret string including the `whsec_` prefix,
    used as UTF-8 text. Verify against the raw body bytes before parsing, compare in constant time and reject a `t` more
    than 300 seconds from your clock. More than one `v1` may be present: accept the request if any matches.

    Test vector:

    ```
    secret: whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
    t:      1760000000
    body:   {"id":"4821","type":"integration.test","created_at":"2026-10-06T18:00:00.000Z","pub_id":"3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10","data":{"message":"test"}}
    v1:     525b55aec595eae900be9d91a7c7830d7fde8f2634ee64aad9081d224f9f38fe
    ```

    ## Webhook delivery rules

    Reply `2xx` within 10 seconds. Redirects are not followed. `410` stops the delivery for good. Any other status,
    a timeout or a network error is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours; the 6th
    failure ends the delivery (`dead`). The webhook URL must be `https`, with a public host name or address: private,
    loopback, link-local and internal targets are refused on every attempt and end the delivery immediately.
  contact:
    name: Tavoloo
    url: https://tavoloo.app
servers:
  - url: https://tavoloo.app/api/integrations/v1
    description: Production, stable URL (Vercel rewrite)
  - url: https://jprbxjjkzjpbwkypcbip.supabase.co/functions/v1/integrations-api/v1
    description: Production, direct function URL (fallback)
tags:
  - name: Connection
    description: Check the key.
  - name: Events
    description: Polling feed of the event queue.
  - name: Orders
    description: Orders placed through Tavoloo.
  - name: Tables
    description: Tables and the table bill published by the POS.
  - name: Waiter calls
    description: Calls from the table (service, order, bill).
  - name: Menu
    description: Menu with the POS item codes.
  - name: Webhooks
    description: Events Tavoloo sends to the webhook URL of a connection in webhook mode.
security:
  - bearerAuth: []
paths:
  /ping:
    post:
      tags: [Connection]
      operationId: ping
      summary: Check the API key
      description: Returns the connection and the restaurant the key belongs to. Use it to test a key.
      responses:
        '200':
          description: The key is valid.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PingResponse'
              example:
                ok: true
                pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
                connection_id: 8d2a64f0-17c3-4e59-b0a1-5f9e3c7d2b86
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: Poll pending events
      description: |
        Returns the events still `pending` for this connection, oldest first, in the same envelope the webhook sends.
        The call does not consume anything: an event keeps coming back until you acknowledge it with
        `POST /events/ack`. Only connections in `polling` mode have a feed; a `webhook` connection always receives an
        empty list (use `GET /orders` to reconcile it).
      parameters:
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Pending events, ascending by `id`.
          content:
            application/json:
              schema:
                type: object
                required: [events]
                properties:
                  events:
                    type: array
                    items:
                      $ref: '#/components/schemas/Event'
              example:
                events:
                  - id: '4821'
                    type: waiter_call.created
                    created_at: '2026-10-06T19:31:44.020Z'
                    pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
                    data:
                      id: e9c04d7b-1f36-4a85-b7d2-6a3f8c1e0954
                      reason: bill
                      status: pending
                      table:
                        id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                        number: '12'
                      created_at: '2026-10-06T19:31:44.020Z'
                      acknowledged_at: null
                      resolved_at: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /events/ack:
    post:
      tags: [Events]
      operationId: acknowledgeEvents
      summary: Acknowledge events
      description: |
        Marks the events as delivered so they stop appearing in `GET /events`. Idempotent: ids already acknowledged, or
        that do not belong to this connection, are not counted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AckRequest'
            example:
              ids: [4821, 4822]
      responses:
        '200':
          description: Number of events newly marked as delivered.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AckResponse'
              example:
                acknowledged: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders:
    get:
      tags: [Orders]
      operationId: listOrders
      summary: List orders placed through Tavoloo
      description: |
        Orders created in the Tavoloo app or by the concierge, most recently updated first. Orders that mirror the POS
        bill are not listed. Use it to reconcile after downtime.
      parameters:
        - name: status
          in: query
          description: '`open` (default) excludes delivered and canceled orders. `all` returns every status.'
          schema:
            type: string
            enum: [open, all]
            default: open
        - name: since
          in: query
          description: Only orders updated at or after this instant (ISO 8601).
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Orders.
          content:
            application/json:
              schema:
                type: object
                required: [orders]
                properties:
                  orders:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders/{orderId}:
    get:
      tags: [Orders]
      operationId: getOrder
      summary: Get one order
      parameters:
        - $ref: '#/components/parameters/OrderId'
      responses:
        '200':
          description: The order.
          content:
            application/json:
              schema:
                type: object
                required: [order]
                properties:
                  order:
                    $ref: '#/components/schemas/Order'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders/{orderId}/status:
    post:
      tags: [Orders]
      operationId: setOrderStatus
      summary: Change the status of an order
      description: |
        The transition only moves forward (`pending`, `confirmed`, `preparing`, `out_for_delivery`, `delivered`) or
        cancels an order that has not finished. Repeating the current status changes nothing and returns
        `changed: false`. A delivered or canceled order cannot change. A real change also posts the staff bell
        notification the panel posts, and comes back as an `order.updated` event.
      parameters:
        - $ref: '#/components/parameters/OrderId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderStatusRequest'
            example:
              status: preparing
      responses:
        '200':
          description: The resulting status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderStatusResponse'
              example:
                ok: true
                status: preparing
                changed: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /orders/{orderId}/payment:
    post:
      tags: [Orders]
      operationId: setOrderPayment
      summary: Mark an order as paid
      description: |
        Records how the order was paid. This is an operational record, not a fiscal document. A `method` that is not one
        of the restaurant's enabled payment methods (Brazil defaults: `pix`, `credit`, `debit`, `cash`, `meal_voucher`)
        is stored as null and the order is marked paid anyway. A `paid_at` in the future is cut to now. A canceled
        order returns `422`.
      parameters:
        - $ref: '#/components/parameters/OrderId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderPaymentRequest'
            example:
              method: pix
              paid_at: '2026-10-06T20:01:10Z'
      responses:
        '200':
          description: Recorded.
          content:
            application/json:
              schema:
                type: object
                required: [ok]
                properties:
                  ok:
                    type: boolean
                    const: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /tables:
    get:
      tags: [Tables]
      operationId: listTables
      summary: List tables
      description: Every table of the restaurant with its room and the open occupation, if any. The `number` is the key used by `PUT /tables/{tableNumber}/tab`.
      responses:
        '200':
          description: Tables.
          content:
            application/json:
              schema:
                type: object
                required: [tables]
                properties:
                  tables:
                    type: array
                    items:
                      $ref: '#/components/schemas/Table'
              example:
                tables:
                  - id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                    number: '12'
                    room: Salao principal
                    is_active: true
                    session:
                      id: 5e2f8b14-7a90-4c3d-a1b6-e08d94c2f7a3
                      started_at: '2026-10-06T18:40:51.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /tables/{tableNumber}/tab:
    put:
      tags: [Tables]
      operationId: putTableTab
      summary: Publish the table bill
      description: |
        Sends the **complete snapshot** of the bill of a table (not a delta). Only a connection with "POS owns the bill"
        enabled can call it; any other gets `403`.

        * `version` is a required, monotonic integer. A version lower than or equal to the stored one is ignored and
          answered with `applied: false`, so retries and out-of-order sends are safe.
        * Lines with `tavoloo_order_id` came from the Tavoloo app and are kept in the bill only. Lines without it are
          the waiter's and are mirrored into a single order with `source = pos`, one per bill, which feeds the
          restaurant's reports and the customer's table bill. Send a stable `lines[].id` for every line: the customer's
          split marks reference it, and without it the line is identified by its position.
        * `external_ref` identifies the bill in the POS. A different `external_ref` arriving while another bill is open
          on the same table ends the previous one.
        * Service charge and tip are kept in the bill and never added to the order total.
        * `status: closed` closes the bill: the mirror order becomes delivered and paid (dominant payment method by
          amount), the Tavoloo orders of the occupation are marked paid and delivered, the table's pending waiter
          calls are resolved, the occupation ends with `end_source = bill`, and the linked reservation is checked out.
          The table is then free. A closed bill cannot be reopened (`422 invalid_transition`).
      parameters:
        - name: tableNumber
          in: path
          required: true
          description: Table number exactly as returned by `GET /tables` (`number`). URL-encode it. Must match exactly one table of the restaurant.
          schema:
            type: string
            minLength: 1
            maxLength: 40
          example: '12'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TabRequest'
            examples:
              open:
                summary: Open bill with one waiter line and one app line
                value:
                  version: 17
                  external_ref: pos-tab-889
                  status: open
                  currency: BRL
                  lines:
                    - id: ln-3
                      name: Chopp 300ml
                      quantity: 2
                      unit_price: 12.9
                      line_total: 25.8
                      external_code: '501'
                      tavoloo_order_id: null
                    - id: ln-1
                      name: Pizza Margherita G
                      quantity: 2
                      unit_price: 39.4
                      line_total: 78.8
                      external_code: '112'
                      tavoloo_order_id: 0b8f6a52-6c0d-4d9a-8f43-2a9d1c7e5b31
                  subtotal: 104.6
                  discount: 0
                  service_charge: 10.46
                  tip: 0
                  tax: 0
                  total: 115.06
                  paid: 0
                  payments: []
              closed:
                summary: Closing snapshot, paid with Pix and card
                value:
                  version: 19
                  external_ref: pos-tab-889
                  status: closed
                  currency: BRL
                  lines:
                    - id: ln-3
                      name: Chopp 300ml
                      quantity: 2
                      unit_price: 12.9
                      line_total: 25.8
                      external_code: '501'
                      tavoloo_order_id: null
                    - id: ln-1
                      name: Pizza Margherita G
                      quantity: 2
                      unit_price: 39.4
                      line_total: 78.8
                      external_code: '112'
                      tavoloo_order_id: 0b8f6a52-6c0d-4d9a-8f43-2a9d1c7e5b31
                  subtotal: 104.6
                  discount: 0
                  service_charge: 10.46
                  tip: 0
                  tax: 0
                  total: 115.06
                  paid: 115.06
                  payments:
                    - method: pix
                      amount: 65.06
                      tip: 0
                    - method: credit
                      amount: 50
                      tip: 0
      responses:
        '200':
          description: 'Applied, or ignored because the version is not newer (`applied: false`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TabResponse'
              examples:
                applied:
                  value:
                    ok: true
                    applied: true
                    tab_id: 71b3e0a9-2c58-4d17-9f64-c5a8d1e0b293
                    status: closed
                    session_closed: true
                ignored:
                  value:
                    ok: true
                    applied: false
                    tab_id: 71b3e0a9-2c58-4d17-9f64-c5a8d1e0b293
                    status: open
                    session_closed: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /waiter-calls/{callId}/ack:
    post:
      tags: [Waiter calls]
      operationId: acknowledgeWaiterCall
      summary: Acknowledge a waiter call
      description: Moves a `pending` call to `acknowledged`. Repeating it is harmless. A cancelled call returns `422`.
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200':
          description: The resulting status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaiterCallActionResponse'
              example:
                ok: true
                status: acknowledged
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /waiter-calls/{callId}/resolve:
    post:
      tags: [Waiter calls]
      operationId: resolveWaiterCall
      summary: Resolve a waiter call
      description: |
        Moves a `pending` or `acknowledged` call to `resolved`. Resolving a bill call (`reason: bill`) ends the table
        occupation. A cancelled call returns `422`.
      parameters:
        - $ref: '#/components/parameters/CallId'
      responses:
        '200':
          description: The resulting status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WaiterCallActionResponse'
              example:
                ok: true
                status: resolved
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/Unprocessable'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /menu:
    get:
      tags: [Menu]
      operationId: getMenu
      summary: Get the menu
      description: |
        Every menu item with its price, availability and the POS item code (`external_code`) the restaurant mapped.
        Names follow the registration language of the restaurant.
      responses:
        '200':
          description: Menu items, ordered by category and position.
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/MenuItem'
              example:
                items:
                  - id: 7a1e5c33-90d4-4b28-a6f1-3c8e2d9b0a75
                    name: Pizza Margherita G
                    category: Pizzas
                    external_code: '112'
                    price: 39.4
                    is_available: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
webhooks:
  order.created:
    post:
      tags: [Webhooks]
      operationId: webhookOrderCreated
      summary: A customer placed an order
      description: |
        Sent when an order with `source = tavoloo` is committed, with its items. Orders that mirror the POS bill never
        produce this event.
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreatedEvent'
            example:
              id: '4821'
              type: order.created
              created_at: '2026-10-06T18:42:07.311Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: 0b8f6a52-6c0d-4d9a-8f43-2a9d1c7e5b31
                order_code: K7M2QX
                status: confirmed
                delivery_type: dine_in
                source: tavoloo
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                table_session_id: 5e2f8b14-7a90-4c3d-a1b6-e08d94c2f7a3
                currency: BRL
                subtotal: 78.8
                discount: 0
                delivery_fee: 0
                total: 78.8
                payment:
                  status: pending
                  method_intent: pix
                  method_confirmed: null
                  paid_at: null
                notes: Sem cebola na burrata
                customer: null
                items:
                  - id: c3d7e9a1-48b2-4f60-9e15-7b2a6d0c8f44
                    menu_item_id: 7a1e5c33-90d4-4b28-a6f1-3c8e2d9b0a75
                    external_code: '112'
                    name: Pizza Margherita G
                    quantity: 2
                    unit_price: 39.4
                    line_total: 78.8
                    notes: null
                created_at: '2026-10-06T18:42:06.980Z'
                updated_at: '2026-10-06T18:42:06.980Z'
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  order.updated:
    post:
      tags: [Webhooks]
      operationId: webhookOrderUpdated
      summary: An order changed status, payment or total
      description: |
        Sent when `status`, `payment.status`, `payment.method_confirmed` or `total` of an order with
        `source = tavoloo` changes, whoever changed it (staff panel, this API, or closing the table bill). The data is the
        full order snapshot.
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpdatedEvent'
            example:
              id: '4830'
              type: order.updated
              created_at: '2026-10-06T18:55:20.004Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: 0b8f6a52-6c0d-4d9a-8f43-2a9d1c7e5b31
                order_code: K7M2QX
                status: preparing
                delivery_type: dine_in
                source: tavoloo
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                table_session_id: 5e2f8b14-7a90-4c3d-a1b6-e08d94c2f7a3
                currency: BRL
                subtotal: 78.8
                discount: 0
                delivery_fee: 0
                total: 78.8
                payment:
                  status: pending
                  method_intent: pix
                  method_confirmed: null
                  paid_at: null
                notes: Sem cebola na burrata
                customer: null
                items:
                  - id: c3d7e9a1-48b2-4f60-9e15-7b2a6d0c8f44
                    menu_item_id: 7a1e5c33-90d4-4b28-a6f1-3c8e2d9b0a75
                    external_code: '112'
                    name: Pizza Margherita G
                    quantity: 2
                    unit_price: 39.4
                    line_total: 78.8
                    notes: null
                created_at: '2026-10-06T18:42:06.980Z'
                updated_at: '2026-10-06T18:55:20.004Z'
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  waiter_call.created:
    post:
      tags: [Webhooks]
      operationId: webhookWaiterCallCreated
      summary: A customer called the waiter
      description: 'Sent when a table calls the waiter. `reason: bill` is the "ask for the bill" button.'
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WaiterCallCreatedEvent'
            example:
              id: '4850'
              type: waiter_call.created
              created_at: '2026-10-06T19:31:44.020Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: e9c04d7b-1f36-4a85-b7d2-6a3f8c1e0954
                reason: bill
                status: pending
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                created_at: '2026-10-06T19:31:44.020Z'
                acknowledged_at: null
                resolved_at: null
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  waiter_call.updated:
    post:
      tags: [Webhooks]
      operationId: webhookWaiterCallUpdated
      summary: A waiter call changed status
      description: Sent when the `status` of a call changes (acknowledged, resolved or cancelled).
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WaiterCallUpdatedEvent'
            example:
              id: '4853'
              type: waiter_call.updated
              created_at: '2026-10-06T19:32:10.552Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: e9c04d7b-1f36-4a85-b7d2-6a3f8c1e0954
                reason: bill
                status: acknowledged
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                created_at: '2026-10-06T19:31:44.020Z'
                acknowledged_at: '2026-10-06T19:32:10.552Z'
                resolved_at: null
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  table_session.opened:
    post:
      tags: [Webhooks]
      operationId: webhookTableSessionOpened
      summary: A table was occupied
      description: Sent when a table occupation starts. `data.start_source` says what started it.
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TableSessionOpenedEvent'
            example:
              id: '4815'
              type: table_session.opened
              created_at: '2026-10-06T18:40:51.004Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: 5e2f8b14-7a90-4c3d-a1b6-e08d94c2f7a3
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                started_at: '2026-10-06T18:40:51.000Z'
                start_source: dine_in
                ended_at: null
                end_source: null
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  table_session.closed:
    post:
      tags: [Webhooks]
      operationId: webhookTableSessionClosed
      summary: A table was freed
      description: |
        Sent when an occupation ends. `end_source` says why: `bill` (bill call resolved or bill closed by the POS),
        `checkout`, `manual` or `timeout`.
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TableSessionClosedEvent'
            example:
              id: '4871'
              type: table_session.closed
              created_at: '2026-10-06T20:05:12.480Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                id: 5e2f8b14-7a90-4c3d-a1b6-e08d94c2f7a3
                table:
                  id: a41c9d70-35be-4f1e-b2a8-90e6d3f1c5a7
                  number: '12'
                started_at: '2026-10-06T18:40:51.000Z'
                start_source: dine_in
                ended_at: '2026-10-06T20:05:12.480Z'
                end_source: bill
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
  integration.test:
    post:
      tags: [Webhooks]
      operationId: webhookIntegrationTest
      summary: Test event
      description: |
        Sent only to the connection that asked for it ("Send test" in the panel), whatever its event filter. The values
        of this example match the signature test vector in the API description.
      security: []
      parameters:
        - $ref: '#/components/parameters/TavolooEvent'
        - $ref: '#/components/parameters/TavolooDelivery'
        - $ref: '#/components/parameters/TavolooSignature'
        - $ref: '#/components/parameters/UserAgent'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestEvent'
            example:
              id: '4821'
              type: integration.test
              created_at: '2026-10-06T18:00:00.000Z'
              pub_id: 3f6d2c1e-8a47-4b9e-9c52-1d7a0e5b6f10
              data:
                message: test
      responses:
        '2XX':
          $ref: '#/components/responses/WebhookAccepted'
        '410':
          $ref: '#/components/responses/WebhookGone'
        default:
          $ref: '#/components/responses/WebhookRetry'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: tvl_live_ followed by 48 hex characters
      description: |
        API key created in the Tavoloo panel and shown once. Only its SHA-256 is stored. The restaurant is the one the key
        belongs to.
  parameters:
    Limit:
      name: limit
      in: query
      description: Page size. Default 50. Values above 100 are capped to 100. Zero or non-numeric values return `400`.
      schema:
        type: integer
        minimum: 1
        default: 50
    OrderId:
      name: orderId
      in: path
      required: true
      description: Order id (`data.id` of the order events).
      schema:
        type: string
        format: uuid
    CallId:
      name: callId
      in: path
      required: true
      description: Waiter call id (`data.id` of the waiter call events).
      schema:
        type: string
        format: uuid
    TavolooEvent:
      name: Tavoloo-Event
      in: header
      required: true
      description: Event type, same as `type` in the body.
      schema:
        type: string
    TavolooDelivery:
      name: Tavoloo-Delivery
      in: header
      required: true
      description: Id of the delivery of this event to this connection. It does not change across retries.
      schema:
        type: string
        pattern: '^[0-9]+$'
    TavolooSignature:
      name: Tavoloo-Signature
      in: header
      required: true
      description: |
        `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" keyed with the webhook secret>`. See "Webhook signature"
        in the API description.
      schema:
        type: string
        pattern: '^t=[0-9]{1,12},v1=[0-9a-f]{64}(,v1=[0-9a-f]{64})*$'
      example: t=1760000000,v1=525b55aec595eae900be9d91a7c7830d7fde8f2634ee64aad9081d224f9f38fe
    UserAgent:
      name: User-Agent
      in: header
      required: false
      description: 'Always `Tavoloo-Webhooks/1`. The body is sent as `Content-Type: application/json`.'
      schema:
        type: string
        const: Tavoloo-Webhooks/1
  headers:
    RetryAfter:
      description: Seconds until the next rate limit window starts.
      schema:
        type: integer
        minimum: 1
        maximum: 60
  responses:
    BadRequest:
      description: Malformed request. Codes `invalid_json`, `invalid_request`, `invalid_id` or `invalid_table` (malformed table number).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_request
              message: ids must hold 1 to 500 event ids
    Unauthorized:
      description: Missing, malformed, revoked or rotated API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unauthorized
              message: Invalid or missing API key
    Forbidden:
      description: The connection is not allowed to do this, for example it does not own the bill.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: forbidden
              message: Forbidden
    NotFound:
      description: Unknown route, or the order or call does not exist for this restaurant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: not_found
              message: Resource not found
    PayloadTooLarge:
      description: Body larger than 256 KiB.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: payload_too_large
              message: Body too large
    Unprocessable:
      description: The request is well formed but the state does not allow it. Codes `invalid_status`, `invalid_transition`, `invalid_payload` or `invalid_table` (no table, or more than one, with that number).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_transition
              message: Status transition not allowed
    RateLimited:
      description: More than 600 requests in the current minute.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Too many requests
    InternalError:
      description: Server error. The body is generic.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: internal_error
              message: Internal error
    WebhookAccepted:
      description: Event accepted. Reply within 10 seconds. The body is ignored.
    WebhookGone:
      description: The endpoint is gone. The delivery stops for good (`dead`) and is not retried.
    WebhookRetry:
      description: |
        Any other status (including `3xx`, because redirects are not followed), a timeout or a network error. The delivery
        is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours; the 6th failure ends it (`dead`).
        Answer with a non-2xx status, for example `401`, when the signature does not verify.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_json
                - invalid_request
                - invalid_id
                - invalid_table
                - unauthorized
                - forbidden
                - not_found
                - method_not_allowed
                - payload_too_large
                - invalid_status
                - invalid_transition
                - invalid_payload
                - rate_limited
                - internal_error
            message:
              type: string
    PingResponse:
      type: object
      required: [ok, pub_id, connection_id]
      properties:
        ok:
          type: boolean
          const: true
        pub_id:
          type: string
          format: uuid
        connection_id:
          type: string
          format: uuid
    AckRequest:
      type: object
      required: [ids]
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 500
          description: Event ids, as integers or as digit strings (the webhook envelope carries the id as a string).
          items:
            oneOf:
              - type: integer
                minimum: 1
              - type: string
                pattern: '^[0-9]{1,15}$'
    AckResponse:
      type: object
      required: [acknowledged]
      properties:
        acknowledged:
          type: integer
          minimum: 0
    TableRef:
      type: object
      required: [id, number]
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
          description: Table number as the restaurant registered it.
    Customer:
      type: [object, 'null']
      description: Only filled for pickup and delivery. It is null for dine-in orders, so no personal data leaves for the salon.
      required: [name, phone]
      properties:
        name:
          type: [string, 'null']
        phone:
          type: [string, 'null']
    OrderItem:
      type: object
      required: [id, menu_item_id, external_code, name, quantity, unit_price, line_total, notes]
      properties:
        id:
          type: string
          format: uuid
        menu_item_id:
          type: [string, 'null']
          format: uuid
        external_code:
          type: [string, 'null']
          description: The POS item code the restaurant mapped on the menu item. Variants and options appear only inside `name`.
        name:
          type: string
        quantity:
          type: integer
        unit_price:
          type: number
        line_total:
          type: number
        notes:
          type: [string, 'null']
    OrderPayment:
      type: object
      required: [status, method_intent, method_confirmed, paid_at]
      properties:
        status:
          type: string
          description: Payment state, for example `pending` or `paid`.
        method_intent:
          type: [string, 'null']
          description: How the customer said they would pay (payment method code).
        method_confirmed:
          type: [string, 'null']
          description: How the order was paid, confirmed by staff or by the POS (payment method code).
        paid_at:
          type: [string, 'null']
          format: date-time
    Order:
      type: object
      required:
        - id
        - order_code
        - status
        - delivery_type
        - source
        - table
        - table_session_id
        - currency
        - subtotal
        - discount
        - delivery_fee
        - total
        - payment
        - notes
        - customer
        - items
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        order_code:
          type: string
        status:
          type: string
          enum: [pending, confirmed, preparing, out_for_delivery, delivered, canceled]
        delivery_type:
          type: string
          enum: [dine_in, pickup, delivery]
        source:
          type: string
          const: tavoloo
        table:
          oneOf:
            - $ref: '#/components/schemas/TableRef'
            - type: 'null'
        table_session_id:
          type: [string, 'null']
          format: uuid
        currency:
          type: string
          description: ISO 4217 code, for example `BRL`.
        subtotal:
          type: number
        discount:
          type: number
        delivery_fee:
          type: number
        total:
          type: number
        payment:
          $ref: '#/components/schemas/OrderPayment'
        notes:
          type: [string, 'null']
        customer:
          $ref: '#/components/schemas/Customer'
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    WaiterCall:
      type: object
      required: [id, reason, status, table, created_at, acknowledged_at, resolved_at]
      properties:
        id:
          type: string
          format: uuid
        reason:
          type: string
          enum: [service, order, bill]
        status:
          type: string
          enum: [pending, acknowledged, resolved, cancelled]
        table:
          $ref: '#/components/schemas/TableRef'
        created_at:
          type: string
          format: date-time
        acknowledged_at:
          type: [string, 'null']
          format: date-time
        resolved_at:
          type: [string, 'null']
          format: date-time
    TableSession:
      type: object
      required: [id, table, started_at, start_source, ended_at, end_source]
      properties:
        id:
          type: string
          format: uuid
        table:
          $ref: '#/components/schemas/TableRef'
        started_at:
          type: string
          format: date-time
        start_source:
          type: string
          enum: [reservation, table_qr, dine_in, manual]
          description: |
            What opened the occupation: a reservation check-in (`reservation`), a table QR scan (`table_qr`), the first
            dine-in order or a bill published by the POS (`dine_in`), or the staff (`manual`).
        ended_at:
          type: [string, 'null']
          format: date-time
        end_source:
          type: [string, 'null']
          enum: [bill, checkout, manual, timeout, null]
          description: |
            Why it ended, null while open. `bill`: the bill call was resolved or the POS closed the bill. `checkout`: the
            staff checked the reservation out. `manual`: the staff freed the table. `timeout`: it ran past the dwell
            time with no sign of life. The first three are real ends; `timeout` is not.
    TestData:
      type: object
      required: [message]
      properties:
        message:
          type: string
          const: test
    EventEnvelope:
      type: object
      description: The webhook body and each item of `GET /events`.
      required: [id, type, created_at, pub_id, data]
      properties:
        id:
          type: string
          pattern: '^[0-9]+$'
          description: Event id as a string. Grows with insertion order. Use it to de-duplicate and order.
        type:
          type: string
          enum:
            - order.created
            - order.updated
            - waiter_call.created
            - waiter_call.updated
            - table_session.opened
            - table_session.closed
            - integration.test
        created_at:
          type: string
          format: date-time
        pub_id:
          type: string
          format: uuid
          description: The restaurant.
        data:
          type: object
    Event:
      description: An event of any type, as returned by `GET /events`. `data` follows `type`.
      oneOf:
        - $ref: '#/components/schemas/OrderCreatedEvent'
        - $ref: '#/components/schemas/OrderUpdatedEvent'
        - $ref: '#/components/schemas/WaiterCallCreatedEvent'
        - $ref: '#/components/schemas/WaiterCallUpdatedEvent'
        - $ref: '#/components/schemas/TableSessionOpenedEvent'
        - $ref: '#/components/schemas/TableSessionClosedEvent'
        - $ref: '#/components/schemas/TestEvent'
      discriminator:
        propertyName: type
        mapping:
          order.created: '#/components/schemas/OrderCreatedEvent'
          order.updated: '#/components/schemas/OrderUpdatedEvent'
          waiter_call.created: '#/components/schemas/WaiterCallCreatedEvent'
          waiter_call.updated: '#/components/schemas/WaiterCallUpdatedEvent'
          table_session.opened: '#/components/schemas/TableSessionOpenedEvent'
          table_session.closed: '#/components/schemas/TableSessionClosedEvent'
          integration.test: '#/components/schemas/TestEvent'
    OrderCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: order.created
        data:
          $ref: '#/components/schemas/Order'
    OrderUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: order.updated
        data:
          $ref: '#/components/schemas/Order'
    WaiterCallCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: waiter_call.created
        data:
          $ref: '#/components/schemas/WaiterCall'
    WaiterCallUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: waiter_call.updated
        data:
          $ref: '#/components/schemas/WaiterCall'
    TableSessionOpenedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: table_session.opened
        data:
          $ref: '#/components/schemas/TableSession'
    TableSessionClosedEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: table_session.closed
        data:
          $ref: '#/components/schemas/TableSession'
    TestEvent:
      allOf:
        - $ref: '#/components/schemas/EventEnvelope'
      properties:
        type:
          const: integration.test
        data:
          $ref: '#/components/schemas/TestData'
    OrderStatusRequest:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [confirmed, preparing, ready, delivered, canceled]
          description: '`ready` is stored as `out_for_delivery`.'
    OrderStatusResponse:
      type: object
      required: [ok, status, changed]
      properties:
        ok:
          type: boolean
          const: true
        status:
          type: string
          enum: [confirmed, preparing, out_for_delivery, delivered, canceled]
          description: The stored status after the call.
        changed:
          type: boolean
          description: False when the order already had this status.
    OrderPaymentRequest:
      type: object
      required: [method]
      properties:
        method:
          type: string
          pattern: '^[A-Za-z0-9_.-]{1,40}$'
          description: Payment method code. One that the restaurant does not accept is stored as null.
        paid_at:
          type: [string, 'null']
          format: date-time
          description: When it was paid. Defaults to now. A future instant is cut to now.
    Table:
      type: object
      required: [id, number, room, is_active, session]
      properties:
        id:
          type: string
          format: uuid
        number:
          type: string
        room:
          type: [string, 'null']
        is_active:
          type: boolean
        session:
          type: [object, 'null']
          description: The open occupation, or null when the table is free.
          required: [id, started_at]
          properties:
            id:
              type: string
              format: uuid
            started_at:
              type: string
              format: date-time
    TabLine:
      type: object
      required: [name, quantity, unit_price, line_total]
      properties:
        id:
          type: string
          minLength: 1
          maxLength: 100
          description: Stable id of the line in the POS. Send it on every snapshot. Without it the line is identified by its position and reordering changes it. Repeated ids among waiter lines are rejected.
        name:
          type: string
          minLength: 1
          maxLength: 200
        quantity:
          type: number
          minimum: 0
          maximum: 1000000000
          description: Fractional quantities (kg, half portion) are mirrored as 1 times the line total.
        unit_price:
          type: number
          minimum: -1000000000
          maximum: 1000000000
        line_total:
          type: number
          minimum: -1000000000
          maximum: 1000000000
        external_code:
          type: [string, 'null']
          maxLength: 64
          description: POS item code. Matches `menu_items.external_code` and fills the menu item of the mirror line.
        tavoloo_order_id:
          type: [string, 'null']
          format: uuid
          description: Set when the line came from a Tavoloo order. Such a line is not mirrored again.
    TabPayment:
      type: object
      required: [method, amount]
      properties:
        method:
          type: string
          pattern: '^[A-Za-z0-9_.-]{1,40}$'
          description: Payment method code, for example `pix`, `credit`, `debit`, `cash`, `meal_voucher`.
        amount:
          type: number
          minimum: 0
          maximum: 1000000000
        tip:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
    TabRequest:
      type: object
      description: Complete snapshot of the table bill. Unknown properties are dropped.
      required: [version, status, lines]
      properties:
        version:
          type: integer
          minimum: 0
          description: 'Monotonic. A value lower than or equal to the stored one is ignored (`applied: false`).'
        external_ref:
          type: [string, 'null']
          maxLength: 120
          description: Id of the bill in the POS.
        status:
          type: string
          enum: [open, closed]
        currency:
          type: string
          pattern: '^[A-Za-z]{3}$'
          description: ISO 4217 code. Defaults to the currency of the restaurant's app orders, or `BRL`.
        lines:
          type: array
          maxItems: 500
          items:
            $ref: '#/components/schemas/TabLine'
        subtotal:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        discount:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        service_charge:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        tip:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        tax:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        total:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        paid:
          type: number
          minimum: 0
          maximum: 1000000000
          default: 0
        payments:
          type: array
          maxItems: 50
          default: []
          items:
            $ref: '#/components/schemas/TabPayment'
    TabResponse:
      type: object
      required: [ok, applied, tab_id, status, session_closed]
      properties:
        ok:
          type: boolean
          const: true
        applied:
          type: boolean
          description: False when the version was not newer than the stored one.
        tab_id:
          type: string
          format: uuid
        status:
          type: string
          enum: [open, closed]
        session_closed:
          type: boolean
          description: True when this call closed the table occupation.
    WaiterCallActionResponse:
      type: object
      required: [ok, status]
      properties:
        ok:
          type: boolean
          const: true
        status:
          type: string
          enum: [pending, acknowledged, resolved]
    MenuItem:
      type: object
      required: [id, name, category, external_code, price, is_available]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        category:
          type: string
        external_code:
          type: [string, 'null']
        price:
          type: number
        is_available:
          type: boolean
