> ## Documentation Index
> Fetch the complete documentation index at: https://docs.now-os.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Signed events for table lifecycle, spend, and reservation outcomes.

NowOS POSTs every event to the webhook URL you registered, as one
envelope:

```json theme={null}
{
  "id": "evt_01HX…",
  "type": "table.seated",
  "sequence": 4812,
  "createdAt": "2026-06-12T20:05:12Z",
  "data": { … }
}
```

## Verifying the signature

Each delivery carries:

```http theme={null}
X-NowOS-Event-Id: evt_01HX…
X-NowOS-Event-Type: table.seated
X-NowOS-Signature: t=1781380201,v1=5f8a…
```

`v1` is `HMAC-SHA256(webhookSecret, "<t>.<rawBody>")` in lowercase hex.
Recompute it over the **raw body bytes** (never a re-serialization) and
compare in constant time. Reject timestamps older than \~5 minutes to
kill replays.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(signature: string, rawBody: string, secret: string): boolean {
  const parts = Object.fromEntries(
    signature.split(",").map((p) => p.split("=") as [string, string]),
  );
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`, "utf8")
    .digest("hex");
  return timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
}
```

## Delivery and ordering

* Respond **2xx within 10 seconds** to acknowledge. Anything else is
  retried at 1 m, 5 m, 30 m, 2 h, 12 h, then parked.
* Delivery order is **not guaranteed** (retries interleave). `sequence`
  is monotonic per location and order events carry `orderVersion` —
  drop anything older than what you already applied. Each event is
  self-sufficient; you never need the previous one.
* `id` is your idempotency key: the same event re-delivered carries the
  same id and the exact same body.

## Events

| Type                         | Fired when                                                                               | Notes                                                                                                                                                                                                                                                                                                                                                       |
| ---------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `floorplan.published`        | the operator publishes the plan                                                          | Thin event — re-fetch `GET /floorplan` and re-check your table mapping.                                                                                                                                                                                                                                                                                     |
| `table.seated`               | an order gains tables                                                                    | `tableIds` is the order's full current set. `reservationExternalId` is YOUR id when the party came from a reservation; null for walk-ins. Also the confirmation of a seat you initiated.                                                                                                                                                                    |
| `order.updated`              | a seated order's items are **sent to the kitchen** (a Send, or a fired-line void)        | Fired total in `totalMinor` — the gross of lines sent to the kitchen, *not* the live cart. Not emitted on unsent edits or covers changes. Throttled to ≥30 s per order, trailing-edge: the last state always ships.                                                                                                                                         |
| `check.paid`                 | a check is paid                                                                          | One event per check — split bills produce several. `tenders[]` includes `provider_prepaid` consumption; `lines[]` is the itemized summary.                                                                                                                                                                                                                  |
| `table.freed`                | order paid/cancelled, or tables removed                                                  | `reason`: `paid`, `cancelled`, or `moved`. A re-seat is `table.freed(moved)` + `table.seated`.                                                                                                                                                                                                                                                              |
| `reservation.status_changed` | staff marks arrived / no-show / cancelled on the till                                    | Forwarded gestures — update your reservation accordingly.                                                                                                                                                                                                                                                                                                   |
| `reservation.tables_changed` | the floor re-plated a booking from the till                                              | Apply it, then **echo the result back** with a normal reservation write. `previousTableIds` lets you recognise your own echo and skip it.                                                                                                                                                                                                                   |
| `reservation.covers_changed` | the party sat a different number than booked, and staff corrected it on the seated order | Apply it, then **echo the result back**. Not optional: covers also travel down (booking → order), so a booking left on the old number reinstates it on the till at your next write. `previousCovers` identifies your own echo.                                                                                                                              |
| `reservation.note_changed`   | staff rewrote the internal note on the seated order                                      | Apply it to the booking's note, then **echo the result back**. Same reasoning as covers: the note also travels down (booking → order, where the kitchen ticket prints it), so a booking left on the old text reinstates it at your next write. `note: null` means the note was CLEARED — an edit, not "no change". `previousNote` identifies your own echo. |
| `reservation.seat_rejected`  | the till refused your seat instruction                                                   | The target table got occupied in the race window. Re-seat the party elsewhere; the instruction will not retry until you change the reservation.                                                                                                                                                                                                             |

Order-derived events fire only for orders **with tables** — you never
see takeaway tickets.
