Skip to content

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 orderId Cata returned at dispatch Plain (this page)POST /api/v1/orders/{orderId}/status with Cata-standard body, partner-side transformation This page
Your POS only emits its native webhook shape and you can't transform on your side (e.g. Revel's order.finalized payload) Built-in transformerPOST /api/v1/inbound/{provider}/order-status with vendor-native body, Cata transforms via JS sandbox Order 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_IN orders follow the same shape as pickup — no driver step; READY → COMPLETED happens 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 PROGRESS and DRIVER PICKED UP are stored exactly as written. Sending IN_PROGRESS or in progress will be rejected with 400 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 → READY is not allowed. From PAID you may only update to ACCEPTED, COMPLETED, or CANCELLED. Trying to skip ahead returns 409 with details: "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. READYREADY), 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 desired differs from where the winner moved the order → loser gets 409 with error.message: "concurrent modification" and details: "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:

  1. Storing the dispatch response's orderId somewhere other than alongside your POS-side order record. The status update endpoint requires Cata's orderId in the URL path. If you only saved your own POS order ID, you cannot push status back; do PATCH /your-pos/orders/{posOrderId} first to attach Cata's orderId.
  2. Sending IN_PROGRESS (with underscore) instead of IN PROGRESS (with space). Same for DRIVER PICKED UP. The strings are exact-match; underscores will be rejected with 400 unsupported status.
  3. Pushing PAID → READY because your POS doesn't have an explicit "accepted" step. From Cata's state machine, READY is only reachable from ACCEPTED/IN PROGRESS. If your POS conceptually moves orders straight from new → preparing, send ACCEPTED first as a synthetic transition, then READY.
  4. Treating 409 as fatal. A 409 concurrent modification is recoverable — re-read and retry. Only 409 invalid status transition is "your input was wrong, don't retry."

See also