Skip to content

How Order Dispatch Works

TL;DR

Cata runs one dispatch endpoint:

POST /api/v1/orders/dispatch

When a customer pays an order in Cata, that endpoint persists it and forwards it to your POS. Two delivery shapes share the endpoint — pick the one that matches how your POS integrates:

Path You provide Cata sends
Standard webhook A callback URL you control The Cata Order JSON, byte-for-byte, signed with HMAC-SHA256
Cata-built transformer POS API credentials (per outlet) A request rewritten into your POS's native API shape, POSTed directly to your POS

Same DTO, same dispatch engine, same response. The request body's builtInTransformer field decides which path runs:

POST /api/v1/orders/dispatch
{
  "outletId": "...",
  "event":    "order.paid",
  "order":    { ... },
  "builtInTransformer": "revel"   // ← present = transformer path; omitted = standard webhook
}

If builtInTransformer is omitted (the default), Cata runs the standard webhook flow. Existing partners are unaffected — the field is purely additive.

Which path is for you?

Are you a vendor / partner integrating your own POS into Cata?
├── Yes → does your POS have a public order-creation API and is Cata
│         building the integration end-to-end?
│         ├── No  → Path 1: Standard webhook (you write the receiver)
│         └── Yes → Path 2: Cata-built transformer (you provide creds)
└── No (you're an operator using Cata + a Cata-supported POS)
        → Path 2: Cata-built transformer (operator config only, no code)

If you're not sure, default to Path 1. It's the standard partner integration; Path 2 only applies when Cata has built a per-provider adapter script for your POS (Revel today; Lightspeed and Atlas Kitchen on the roadmap).


Path 1 — Standard webhook flow

You build a small HTTP receiver. Cata POSTs to it with the order.

Cata → POST {your registered URL}
       Body:    { ...Cata Order JSON, byte-for-byte... }
       Headers: X-Cata-Signature: sha256=<hmac>
                X-Cata-Event:     order.paid
                X-Cata-Delivery-ID: <uuid>
                X-Cata-Timestamp: <unix>
                Content-Type: application/json

What you do:

  1. Register your webhook URL once via POST /api/v1/webhooks/register — Cata returns a secret you save and use for HMAC verification.
  2. On each request, verify the HMAC over the raw body bytes using your saved secret.
  3. Parse the Cata Order JSON, push it into your POS's API in whatever shape it needs.
  4. Reply 2xx — Cata marks the order dispatched. Optionally include {"externalOrderId":"..."} in the response body so Cata can store your POS's order ID for reconciliation.

What's nice about this path:

  • Byte-for-byte payload — Cata never re-marshals the order. The signature you receive is over the exact bytes on the wire, so HMAC stays valid even when intermediate proxies are involved.
  • Stable schema — every partner gets the same Cata Order JSON. Schema changes are additive (new optional fields, never breaking).
  • You own the integration — no Cata-side code change needed when your POS evolves.

Hands-on: Vendor Integration Walkthrough


Path 2 — Cata-built transformer (no receiver needed)

Cata maintains a per-provider script that converts the Cata order into your POS's native shape and POSTs directly to your POS.

Cata → POST {your-pos-api-host}/whatever-endpoint
       Body:    { ...rewritten into your POS's native shape... }
       Headers: { ...auth your POS expects... }
       (no HMAC headers — auth is whatever the script returned)

What an operator does:

  1. Tell Cata which POS provider this outlet uses.
  2. Hand Cata your POS API credentials (and any per-establishment IDs the POS needs — e.g. establishmentId, dining-option IDs). Cata stores them per-outlet, never per-tenant, so each location can use its own credentials.
  3. That's it. The internal caller (kds-management-service) sets builtInTransformer:"<provider>" on the dispatch request and Cata's transformer takes over.

What's nice about this path:

  • No webhook receiver to host. Many small operators don't have engineering capacity for that.
  • Cata maintains the integration. When the POS API changes or a new provider quirk surfaces, Cata updates the per-provider script — no operator-side change needed.
  • Coexists with Path 1. If a partner registers a webhook AND uses a transformer for a specific outlet, both work — builtInTransformer on the request picks which one runs for that dispatch.

Hands-on: Built-In Transformer Walkthrough

Currently supported providers (transformer scripts shipped):

Provider Status
Revel 🟢 Sandbox-validated
Atlas Kitchen 🟡 To do
Lightspeed 🟡 To do

What both paths share

Regardless of which path runs, every dispatch goes through the same engine and gets the same guarantees:

  • One endpoint. POST /api/v1/orders/dispatch is the only thing internal callers ever call.
  • Validation up front. The order JSON is validated for required fields before anything else — bad payloads return HTTP 400 before any external call.
  • Persistence first. Cata writes the order to its own DB before attempting to dispatch. If the external call fails, the row stays as evidence and the response carries dispatched: false with a reason.
  • Idempotency-ready. Each dispatch attempt has a unique X-Cata-Delivery-ID (standard path) — partners can dedupe at the receiver. The transformer path equivalent is the upstream POS's own idempotency (typically transaction_id).
  • External-order-id reconciliation. When the destination returns a POS-side order ID in the response (externalOrderId, orderId, or id), Cata surfaces it as externalOrderId in the dispatch response — useful for cross-referencing later.
  • Body-level error detection. Some POS APIs return HTTP 200 with status:"ERROR" in the body (Revel does this). Cata's pipeline catches that and reports dispatched: false with the error message — no false positives.

End-to-end flow

The same dispatch endpoint serves both paths. The diagram below shows what happens from the moment a customer pays in Cata to the moment the order lands in your POS.

flowchart TD
    A[Customer pays in Cata] --> B[POST /api/v1/orders/dispatch]
    B --> C{Validate order JSON<br/>persist it Cata-side}
    C --> D{Did the request include<br/>builtInTransformer?}
    D -- No --> E[Path 1: Standard webhook<br/>POST your registered callback URL<br/>with Cata Order JSON + HMAC headers]
    D -- Yes --> F[Path 2: Cata-built transformer<br/>POST your POS API directly<br/>with the POS-native shape + auth]
    E --> G{Receiver returns 2xx?}
    F --> G
    G -- Yes --> H[Cata returns dispatched: true<br/>+ externalOrderId from your response<br/>to the caller]
    G -- No --> I[Cata returns dispatched: false<br/>+ reason for the failure<br/>order persisted as failed]

Whichever path runs, you get the same response shape from /api/v1/orders/dispatch:

{
  "code":            200,
  "isSuccess":       true,
  "message":         "order dispatched",
  "orderId":         "<cata-uuid>",        // Cata's internal order ID
  "externalOrderId": "987654",             // your POS's order ID (when your response returned one)
  "dispatched":      true,
  "reason":          ""                    // populated only when dispatched: false
}

externalOrderId is your reconciliation handle — store it on your side if you need to cross-reference Cata's view of the order with your POS's view.

When dispatch fails

If your receiver / your POS rejects the request, Cata sets dispatched: false and reports a reason:

reason What it means
"no dispatch route configured for this outlet/provider" Outlet has no registered webhook AND no builtInTransformer was supplied. Configure one.
"callback returned HTTP <code>" Path 1: your webhook returned non-2xx. Check your receiver's logs.
"POS returned HTTP <code>: <body>" Path 2: your POS's API returned non-2xx. The truncated body is in the reason for diagnostics.
"callback returned 2xx but body indicated error: <message>" Path 1: your webhook returned HTTP 2xx with a body-level error (e.g. {"status":"ERROR","error":"..."}). Cata reads the body and treats this as a failed dispatch rather than a success.
"POS returned 2xx but body indicated error: <message>" Path 2: your POS returned HTTP 2xx with a body-level error (e.g. Revel cart-submit's {"status":"ERROR","error":{...}}). Same detection — the embedded message comes from the response body's error.message / error.details.message field.
"http request failed: ..." Transport-level failure (DNS, TCP, TLS, timeout). Usually means your receiver is unreachable or your POS API host is wrong.

The order is always persisted Cata-side regardless of outcome — failed dispatches stay in the DB so you can investigate or replay later.


You want to … Read this
Wire up a webhook receiver as a vendor Vendor Integration Walkthrough
Use a Cata-built transformer (Revel today) as an operator Built-In Transformer Walkthrough
Push order status updates back to Cata Order Status Updates
Full request / response schema API Reference — Partner H2H tag