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:
- Register your webhook URL once via
POST /api/v1/webhooks/register— Cata returns a secret you save and use for HMAC verification. - On each request, verify the HMAC over the raw body bytes using your saved secret.
- Parse the Cata Order JSON, push it into your POS's API in whatever shape it needs.
- 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:
- Tell Cata which POS provider this outlet uses.
- 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. - 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 —
builtInTransformeron 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/dispatchis 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: falsewith areason. - 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 (typicallytransaction_id). - External-order-id reconciliation. When the destination returns a POS-side order ID in the response (
externalOrderId,orderId, orid), Cata surfaces it asexternalOrderIdin 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 reportsdispatched: falsewith 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.
Read next¶
| 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 |