Order Status Updates¶
Push fulfilment progress for an order from your POS back to Cata so the customer sees real-time status in the Cata App. This is the inbound counterpart to Order Dispatch: orders flow Cata → your POS via dispatch, then status updates flow back your POS → Cata via this endpoint.
Two paths exist — pick the one that matches your integration:
Situation Path Doc Your POS can POST a Cata-shape body to a Cata URL with the orderIdCata returned at dispatchPlain (this page) — POST /api/v1/orders/{orderId}/statuswith Cata-standard body, partner-side transformationThis page Your POS only emits its native webhook shape and you can't transform on your side (e.g. Revel's order.finalizedpayload)Built-in transformer — POST /api/v1/inbound/{provider}/order-statuswith vendor-native body, Cata transforms via JS sandboxOrder Status Updates — Built-In Transformer Both paths funnel into the same backend service, so the state machine, transitions, and idempotency guarantees described below apply to both.
POST /api/v1/orders/{orderId}/status
Auth: X-Api-Key (Partner H2H — same key used for dispatch + loyalty)
{orderId}: the order UUID Cata returned to you in the dispatch response
export BASE="https://{tenant}.sgp1.samba-technologies.xyz/service/pos-integration"
export KEY="<your partner API key>"
1. Status transition diagrams¶
The state machine itself is delivery-method-agnostic — the same set of transitions is allowed regardless. In practice though, the path partners walk depends on the order's deliveryMethod (set during dispatch). Two typical flows:
1.1 — Pickup orders (deliveryMethod: PICKUP)¶
The customer collects the order at the counter — no driver step. Most stores spend most of their time in this flow.
stateDiagram-v2
direction TB
[*] --> PAID: dispatched
PAID --> ACCEPTED: you acknowledged it
ACCEPTED --> IN_PROGRESS: "IN PROGRESS"
IN_PROGRESS --> READY: plated / cup in rack
READY --> COMPLETED: customer picked it up
COMPLETED --> [*]
PAID --> CANCELLED: reject (with reason)
ACCEPTED --> CANCELLED
IN_PROGRESS --> CANCELLED
READY --> CANCELLED
COMPLETED --> CANCELLED: late cancel
CANCELLED --> [*]
classDef partner fill:#d4edda,stroke:#28a745,color:#155724
classDef internal fill:#e9ecef,stroke:#6c757d,color:#495057
classDef terminal fill:#f8d7da,stroke:#dc3545,color:#721c24
class ACCEPTED,IN_PROGRESS,READY,COMPLETED partner
class PAID internal
class CANCELLED terminal
DINE_INorders follow the same shape as pickup — no driver step;READY → COMPLETEDhappens when the customer takes the tray.
1.2 — Delivery orders (deliveryMethod: DELIVERY)¶
A driver collects from the kitchen and hands off to the customer. One extra hop (DRIVER PICKED UP) between READY and COMPLETED.
stateDiagram-v2
direction TB
[*] --> PAID: dispatched
PAID --> ACCEPTED: you acknowledged it
ACCEPTED --> IN_PROGRESS: "IN PROGRESS"
IN_PROGRESS --> READY: plated / bagged
READY --> DRIVER_PICKED_UP: "DRIVER PICKED UP"
DRIVER_PICKED_UP --> COMPLETED: customer received
COMPLETED --> [*]
PAID --> CANCELLED: reject (with reason)
ACCEPTED --> CANCELLED
IN_PROGRESS --> CANCELLED
READY --> CANCELLED
DRIVER_PICKED_UP --> CANCELLED
COMPLETED --> CANCELLED: late cancel
CANCELLED --> [*]
classDef partner fill:#d4edda,stroke:#28a745,color:#155724
classDef internal fill:#e9ecef,stroke:#6c757d,color:#495057
classDef terminal fill:#f8d7da,stroke:#dc3545,color:#721c24
class ACCEPTED,IN_PROGRESS,READY,DRIVER_PICKED_UP,COMPLETED partner
class PAID internal
class CANCELLED terminal
1.3 — Full reference: every transition the state machine allows¶
The two diagrams above show the typical path per delivery method. The underlying state machine is more permissive — it doesn't enforce delivery-method semantics. If your operations need to skip steps (e.g. ACCEPTED → COMPLETED for an instant takeaway), it's allowed:
| From | Allowed targets (partner-pushable) |
|---|---|
PAID |
ACCEPTED, COMPLETED, CANCELLED |
ACCEPTED |
IN PROGRESS, READY, DRIVER PICKED UP, COMPLETED, CANCELLED |
IN PROGRESS |
READY, DRIVER PICKED UP, COMPLETED, CANCELLED |
READY |
DRIVER PICKED UP, COMPLETED, CANCELLED |
DRIVER PICKED UP |
COMPLETED, CANCELLED |
COMPLETED |
CANCELLED (late-cancellation only) |
CANCELLED |
— terminal |
UNPAID, REFUNDED |
— internal, not partner-pushable |
Anything not in this table returns 409 Conflict with the canonical message "cannot change order status from <current> to <desired>".
2. Allowed status strings¶
Six values, partner-pushable. Send each exactly as shown — literal spaces, no underscores, no case-folding:
| Value | When |
|---|---|
ACCEPTED |
You acknowledged the order in your POS. |
IN PROGRESS |
Kitchen / barista has started preparing it. |
READY |
Order is plated / cup is in the rack — pickup or hand-off ready. |
DRIVER PICKED UP |
Delivery driver has the bag. |
COMPLETED |
Customer received the order; transaction is done. |
CANCELLED |
Order was rejected, voided, or refunded mid-flow. reason is required. |
Spaces are intentional and persistent. The strings
IN PROGRESSandDRIVER PICKED UPare stored exactly as written. SendingIN_PROGRESSorin progresswill be rejected with400 unsupported status.
3. Request shape¶
{
"status": "ACCEPTED",
"posOrderId": "pos-12345", // optional — your POS-side reference; V1 captures it in service logs, queryable persistence ships in V2
"reason": "kitchen out of buns", // required iff status == "CANCELLED", advisory otherwise
"timestamp": "2026-04-28T10:00:00Z" // optional — when it changed in your system; defaults to server time
}
4. Walkthrough — typical lifecycle¶
After your dispatch receiver got the order and you saved Cata's orderId from the dispatch response:
export ORDER_ID="<orderId-from-dispatch-response>"
4.1 — Acknowledge (PAID → ACCEPTED)¶
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "ACCEPTED", "posOrderId": "pos-12345" }' | jq .
{
"code": 200,
"isSuccess": true,
"message": "order status updated",
"orderId": "...",
"previousStatus": "PAID",
"currentStatus": "ACCEPTED",
"transitionedAt": "2026-04-28T10:00:00Z"
}
PAID → READYis not allowed. FromPAIDyou may only update toACCEPTED,COMPLETED, orCANCELLED. Trying to skip ahead returns409withdetails: "cannot change order status from PAID to READY".
4.2 — Kitchen progress (ACCEPTED → IN PROGRESS → READY)¶
# Started prep
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "IN PROGRESS" }' | jq .
# Plated / ready for pickup
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "READY" }' | jq .
4.3 — Hand-off (READY → DRIVER PICKED UP → COMPLETED)¶
# Delivery: driver took the bag
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "DRIVER PICKED UP" }' | jq .
# Customer received (or, for pickup orders, customer picked it up)
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "COMPLETED" }' | jq .
4.4 — Cancellation (CANCELLED, with reason)¶
# Reason required
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "CANCELLED", "reason": "kitchen out of buns" }' | jq .
Without reason:
curl -sS -X POST "$BASE/api/v1/orders/$ORDER_ID/status" \
-H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "status": "CANCELLED" }' | jq .
# → 400 "reason is required when status is CANCELLED"
5. Idempotency and concurrency¶
Replays of the same status¶
If you push a status the order is already in (e.g. READY → READY), Cata returns 200 with previousStatus == currentStatus and does not modify the stored transitionedAt. The original transition's timestamp wins. This makes status updates safe to retry on network failures without duplicating events.
Concurrent updates from your side¶
Two of your kitchen terminals push READY for the same order at the same instant. Cata uses optimistic concurrency control (UPDATE ... WHERE order_status = <expected_current>) so only one write lands. The loser is told what happened:
- If the loser asked for the same status the winner achieved → loser also gets
200(idempotent — both got what they wanted). - If the loser's
desireddiffers from where the winner moved the order → loser gets409witherror.message: "concurrent modification"anddetails: "another writer changed this order's status concurrently — re-read and try again". Pull the order's current status, decide whether your transition still applies, then retry.
6. Error responses¶
| HTTP | When | details |
|---|---|---|
400 |
Status not in the partner-pushable set (PAID, UNPAID, REFUNDED, garbage, IN_PROGRESS instead of IN PROGRESS, etc.) |
"unsupported status: <value> — partners may only push [ACCEPTED IN PROGRESS READY DRIVER PICKED UP COMPLETED CANCELLED]" |
400 |
Missing reason on a CANCELLED request |
"reason is required when status is CANCELLED" |
400 |
timestamp not ISO-8601 |
"timestamp must be ISO-8601 (RFC 3339), e.g. 2026-04-28T10:00:00Z" |
401 |
Missing / invalid X-Api-Key |
standard auth envelope |
404 |
No order with this orderId exists in your tenant |
"no dispatched order found for the given orderId in this tenant" |
409 |
The state machine rejects the transition | "cannot change order status from <current> to <desired>" |
409 |
Another writer changed the order concurrently | "another writer changed this order's status concurrently — re-read and try again" |
500 |
Cata-side persistence failure | "persist status: ..." |
Recognising 409 in client code¶
Both 409 cases share the HTTP status. Distinguish via error.message:
if (resp.status === 409) {
if (resp.body.error.message === "invalid status transition") {
// State machine rejection — don't retry; you sent something that was never going to land
// (e.g. PAID → READY skip, or CANCELLED → ANYTHING).
} else if (resp.body.error.message === "concurrent modification") {
// Race — re-read the order's current status, decide if your transition still applies, retry
}
}
7. What partners commonly do wrong¶
These are the four most common mistakes during integration:
- Storing the dispatch response's
orderIdsomewhere other than alongside your POS-side order record. The status update endpoint requires Cata'sorderIdin the URL path. If you only saved your own POS order ID, you cannot push status back; doPATCH /your-pos/orders/{posOrderId}first to attach Cata'sorderId. - Sending
IN_PROGRESS(with underscore) instead ofIN PROGRESS(with space). Same forDRIVER PICKED UP. The strings are exact-match; underscores will be rejected with400 unsupported status. - Pushing
PAID → READYbecause your POS doesn't have an explicit "accepted" step. From Cata's state machine,READYis only reachable fromACCEPTED/IN PROGRESS. If your POS conceptually moves orders straight from new → preparing, sendACCEPTEDfirst as a synthetic transition, thenREADY. - Treating
409as fatal. A409 concurrent modificationis recoverable — re-read and retry. Only409 invalid status transitionis "your input was wrong, don't retry."
See also¶
- Order Dispatch — Vendor Integration Walkthrough — the outbound flow that gives you the
orderIdyou use here. - Order Status Updates — Built-In Transformer — the alternate path for vendors whose webhook payloads are non-negotiable (Revel, etc.); Cata transforms in-house.
- Full API reference (request/response schemas, examples, state-machine description): https://apidocs.cata.sg/