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

# KDS order status

> Inbound endpoint your KDS POSTs to when a kitchen order changes state (preparing, ready, dispatched). Fire logs the event, deduplicates it, and enforces anti-regression so the order state never moves backwards.

This endpoint is **inbound** — your Kitchen Display System (KDS) POSTs to it whenever an order advances in the kitchen: the kitchen started **preparing** it, it became **ready** for hand-off, or it was **dispatched** (delivered / picked up). Fire authenticates the request, correlates it to the order, applies idempotency and an anti-regression gate, and records the event in its KDS event log for observability.

<Note>
  **This endpoint is asynchronous.** Fire authenticates, runs the source-of-truth + tenant guard, deduplicates, and **enqueues** the event — then returns **`202 Accepted`** with a `webhookEventId` (typically under 100 ms). The event is recorded a moment later by a background worker (usually within \~2 seconds). To check the outcome, poll [`GET /v1/webhooks/events/{webhookEventId}`](#checking-the-outcome). Client-fixable problems (bad payload, wrong `eventId`, wrong tenant) are rejected **synchronously** with `4xx` **before** the `202`.
</Note>

<Note>
  **One dispatch event, several status reports.** Unlike the fiscal callback — where each action carries its own `eventId` — the KDS reports the whole journey (`preparing` → `ready` → `dispatched`) against **one** `eventId`: the id Fire emitted when it dispatched the order to your device. They are distinguished by `eventType`, not by `eventId`. See [Idempotency & the journey](#idempotency--the-journey).
</Note>

## Event types

The KDS lifecycle has a strict order — an order is **prepared** before it is **ready**, and **ready** before it is **dispatched**:

| `eventType`        | Meaning                                                        | Rank |
| ------------------ | -------------------------------------------------------------- | ---- |
| `order.preparing`  | The kitchen started working on the order                       | 1    |
| `order.ready`      | The order is prepared and ready for hand-off / pickup          | 2    |
| `order.dispatched` | The order left the kitchen (delivered or picked up) — terminal | 3    |

Values are **lowercase, dotted** (`order.preparing`, not `ORDER_PREPARING`).

## Authentication

This endpoint requires a **vendor-scoped API key with the `webhooks:kds` scope** (account + vendor binding). Fire enforces that the order belongs to that account and vendor. Keys without the scope, or without a vendor binding, are rejected with `403 Forbidden`.

<ParamField header="x-api-key" type="string" required>
  Your Fire vendor-scoped API key with scope `webhooks:kds`. Generate one from **Developers → API Management** for the account/vendor whose orders this key may report on.
</ParamField>

<ParamField header="Authorization" type="string">
  Optional `Bearer <token>` — accepted as a legacy alternative to `x-api-key`. Send one or the other.
</ParamField>

## Request body

<ParamField body="eventType" type="string" required>
  The KDS lifecycle event. One of `order.preparing`, `order.ready`, or `order.dispatched` (lowercase, dotted).
</ParamField>

<ParamField body="providerEventId" type="string" required>
  Your KDS's own id for this delivery. Stored for audit/forensics — **it is not the idempotency key**. Fire deduplicates on `(orderId, eventId, eventType)`, so you may send a fresh `providerEventId` on every retry. Use your KDS-native event id if available; otherwise a UUID.
</ParamField>

<ParamField body="occurredAt" type="string" required>
  ISO 8601 UTC timestamp of when the event happened in the KDS — not when it was sent.
</ParamField>

<ParamField body="orderId" type="string" required>
  UUID of the order in Fire. Matches `data.orderId` in the order events. Fire correlates the event to this order; it must already exist.
</ParamField>

<ParamField body="eventId" type="string" required>
  Correlation UUID — the `event.id` of the envelope Fire emitted when it **dispatched the order to your device**. Echo it exactly; never invent it.

  * **Source-of-truth check.** An `eventId` that does not reference a Fire event for that order is rejected with `400` before the `202`.
  * **One eventId for the whole journey.** Send the **same** `eventId` for `preparing`, `ready`, and `dispatched` of that dispatch — they are told apart by `eventType`. (Each dispatch to a **different device** carries its own `eventId`, so two devices never collide.)
</ParamField>

<ParamField body="stationName" type="string">
  Optional originating KDS station (e.g. `Hot line`, `Despacho 1`). Stored for observability.
</ParamField>

<ParamField body="metadata" type="object">
  Optional free-form bag of extra fields. Stored as-is, not validated.
</ParamField>

### Examples

The three reports of the same dispatch share **one `eventId`** (the dispatch's) and differ only by `eventType` and `providerEventId`:

```json order.preparing theme={null}
{
  "eventType": "order.preparing",
  "providerEventId": "evt_2026-05-26_000122",
  "occurredAt": "2026-05-26T18:28:00.000Z",
  "orderId": "9f1c0e8a-1234-4abc-9def-0123456789ab",
  "eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "stationName": "Hot line"
}
```

```json order.ready theme={null}
{
  "eventType": "order.ready",
  "providerEventId": "evt_2026-05-26_000123",
  "occurredAt": "2026-05-26T18:30:00.000Z",
  "orderId": "9f1c0e8a-1234-4abc-9def-0123456789ab",
  "eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "stationName": "Hot line"
}
```

```json order.dispatched theme={null}
{
  "eventType": "order.dispatched",
  "providerEventId": "evt_2026-05-26_000124",
  "occurredAt": "2026-05-26T18:42:11.000Z",
  "orderId": "9f1c0e8a-1234-4abc-9def-0123456789ab",
  "eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "stationName": "Hot line"
}
```

## Response

On success the endpoint returns **`202 Accepted`** — the event was authenticated, validated, deduplicated, and **enqueued**. A `202` does **not** mean the event was recorded yet; that happens asynchronously. Use the [status endpoint](#checking-the-outcome) to confirm.

The body carries **two distinct ids**: `eventId` is the id **you** sent (echoed), `webhookEventId` is **Fire's** id for the queued record. Same shape as the [fiscal callback](/en/api-reference/fiscal-callback#response).

<ResponseField name="received" type="boolean">
  Always `true` when the request was accepted and enqueued.
</ResponseField>

<ResponseField name="duplicate" type="boolean">
  `true` when this exact `(orderId, eventId, eventType)` was already ingested — the existing record is returned and nothing is re-enqueued. `false` for a fresh report (including a different `eventType` of the same dispatch — that's a new step, not a duplicate).
</ResponseField>

<ResponseField name="eventId" type="string">
  Echo of the `eventId` you sent (the dispatch's `event.id`).
</ResponseField>

<ResponseField name="webhookEventId" type="string">
  Fire's id for the queued record. Pass it to `GET /v1/webhooks/events/{webhookEventId}` to poll the outcome. On a duplicate it is the **same** id returned the first time.
</ResponseField>

<ResponseField name="status" type="string">
  Current queue status — `queued` → `processing` → `processed` (and `retry` / `failed` / `dead` / `ignored`).
</ResponseField>

<ResponseField name="firstReceivedAt" type="string">
  ISO 8601 UTC timestamp of when Fire **first** received this report. Stable across retries.
</ResponseField>

<ResponseField name="message" type="string">
  Human-readable summary.
</ResponseField>

<ResponseExample>
  ```json 202 — accepted (new step, queued) theme={null}
  {
    "received": true,
    "duplicate": false,
    "eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "webhookEventId": "9e6c8af8-af80-4967-9422-c096ab43c0e7",
    "status": "queued",
    "firstReceivedAt": "2026-05-26T18:30:00.512Z",
    "message": "Event accepted and queued for processing."
  }
  ```

  ```json 202 — duplicate (same orderId + eventId + eventType) theme={null}
  {
    "received": true,
    "duplicate": true,
    "eventId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "webhookEventId": "9e6c8af8-af80-4967-9422-c096ab43c0e7",
    "status": "processed",
    "firstReceivedAt": "2026-05-26T18:30:00.512Z",
    "message": "Event already received; no action needed."
  }
  ```

  ```json 400 — wrong eventId / validation error theme={null}
  {
    "success": false,
    "error": "VALIDATION_ERROR",
    "message": "eventId does not reference an event emitted by Fire for this orderId. Echo the event.id from a V4 envelope you received for this order."
  }
  ```

  ```json 401 — missing or invalid API key theme={null}
  {
    "success": false,
    "error": "UNAUTHORIZED",
    "message": "API key required. Use x-api-key: pk_live_... header"
  }
  ```

  ```json 403 — missing scope / not your tenant theme={null}
  {
    "success": false,
    "error": "FORBIDDEN",
    "message": "webhooks:kds requires a vendor-scoped API key (account + vendor binding). Generate one from /developers/firepos-api-management."
  }
  ```

  ```json 503 — auth store temporarily unreachable (retry) theme={null}
  {
    "success": false,
    "error": "SERVICE_UNAVAILABLE",
    "message": "API key verification is temporarily unavailable (auth store unreachable). Retry the request."
  }
  ```
</ResponseExample>

## Checking the outcome

Because processing is asynchronous, the `202` only confirms the event was **queued**. To see whether it was recorded, poll the companion status endpoint with the `webhookEventId` returned by the `202`:

```
GET https://app.fire.rest/api/v1/webhooks/events/{webhookEventId}
x-api-key: <your webhooks:kds key>
```

<ResponseField name="status" type="string">
  Queue lifecycle: `queued` → `processing` → `processed` (done) · `failed` / `dead` (gave up after retries) · `retry` (waiting for the next attempt) · `ignored` (handled, no action needed — e.g. a duplicate or non-advancing event).
</ResponseField>

<ResponseField name="attempts" type="number">Processing attempts so far.</ResponseField>
<ResponseField name="result" type="object | null">On success, the worker outcome — e.g. `{ "kind": "recorded" }` (advanced the order) or `{ "kind": "ignored" }` (non-advancing).</ResponseField>
<ResponseField name="error" type="object | null">`{ "message": "…" }` when the last attempt failed; `null` otherwise.</ResponseField>

A `404` is returned for unknown ids — or ids that belong to another tenant — with no existence leak. Authenticate with the same `webhooks:kds` key you used for the event.

## Idempotency & the journey

Fire deduplicates on the triple **`(orderId, eventId, eventType)`** — *not* on `providerEventId` (which you may regenerate freely). This is what makes the journey work:

| Scenario                                                                                 | Result                                                                                       |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `preparing`, then `ready`, then `dispatched` — **same `eventId`**, different `eventType` | each is a **new step** → `202` `duplicate:false`. Same `eventId` is expected, not a conflict |
| The **same** report resent — same `(orderId, eventId, eventType)`                        | `202` `duplicate:true` — existing record returned, not re-processed                          |
| `eventId` not emitted by Fire for this `orderId`                                         | `400`                                                                                        |
| Order/event not under your API key's account + vendor                                    | `403`                                                                                        |
| Auth store momentarily unreachable                                                       | `503` — transient, **retry**                                                                 |

<Note>
  **This is the key difference from the fiscal callback.** There, one `eventId` carries exactly **one** action, so reusing it for a different `eventType` is a conflict (`409`). Here, one dispatch `eventId` legitimately carries the **whole journey** (`preparing` → `ready` → `dispatched`) — the `eventType` is what tells the steps apart. Different devices get different dispatch `eventId`s, so their reports never collide.
</Note>

## Anti-regression

The order state must never move backwards. Fire tracks the **highest stage reached** by the order (`order.dispatched` > `order.ready` > `order.preparing`) and compares each incoming event against it:

* An event that **advances** the order (e.g. `order.ready` after `order.preparing`) is recorded as the new state.
* An event that **does not advance** — a regression (e.g. `order.preparing` arriving after `order.ready`) or a same-state repeat — is still **recorded for observability**, but flagged as non-advancing with a reason, and it does **not** move the order back.

This makes the endpoint safe against out-of-order or late deliveries: send events in any order and Fire keeps the order at its most advanced stage.

## Related

<CardGroup cols={2}>
  <Card title="Inject order" icon="paper-plane" href="/en/api-reference/orders">
    The order injection endpoint that creates the order this event references.
  </Card>

  <Card title="Fiscal callback" icon="receipt" href="/en/api-reference/fiscal-callback">
    The sibling inbound webhook — same async + idempotency + correlation model.
  </Card>

  <Card title="Authentication" icon="lock" href="/en/authentication">
    How API keys, scopes, partner and vendor binding work.
  </Card>
</CardGroup>
