Skip to content

Order Dispatch — Built-In Transformer (Cata-built POS adapter)

If you're new to order dispatch, read How Order Dispatch Works first — it explains both delivery paths (webhook vs Cata-built transformer) and when each one applies. This page is the hands-on walkthrough for the transformer path.

When a POS vendor doesn't host a webhook receiver, Cata can build the integration in-house: a per-provider JS transformer rewrites the standard Cata Order JSON into the POS's native API shape and POSTs directly to the POS. The internal caller selects this path with one optional field on the dispatch request:

POST /api/v1/orders/dispatch
{
  "outletId": "...",
  "event":    "order.paid",
  "order":    { ... },
  "builtInTransformer": "revel"   // ← opt-in
}

If builtInTransformer is omitted, the standard webhook flow runs (Cata Order JSON + HMAC headers to the registered callback URL). The two paths are independent and can coexist on the same outlet.

When to use it

Situation Path
Partner runs their own webhook receiver Standard — they consume Cata Order JSON, verify HMAC, transform on their side. See Order Dispatch (Vendor Integration).
POS exposes a public order-creation API but the operator can't / won't host a webhook Built-in transformer — Cata writes the JS, operator just enters credentials. This page.

What lives where

Piece Where Purpose
Standard envelope script central.adapter_scripts row (_standard, order_dispatch) Always runs first. Builds {url, method, headers, body=null} from the registered webhook.
Per-provider transformer central.adapter_scripts row (<provider>, order_dispatch) Runs only when builtInTransformer="<provider>". Rewrites body / headers / URL for the POS's API.
Per-outlet credentials tenantdb.outlet_providers.settings (JSON column) API keys, establishment IDs, dining-option mappings — anything outlet-specific. The transformer reads these via input.outlet.settings.
HMAC headers injected by Go (internal/connectors/dispatch_pipeline.go) Added only on the no-transformer path. Transformer path is responsible for its own auth.

Provider status

Provider Status Script
Revel 🟢 Sandbox-validated (current v1.2.0) — covers simple orders, modifiers, bundles (is_combo + products_sets[]), discounts, fees, all three dining options scripts/adapters/revel/order_dispatch.js
Atlas Kitchen To do
Lightspeed To do
Deliverect N/A — Deliverect is a partner who registers their own webhook (standard path)

The rest of this page walks through Revel as the worked example. New providers follow the same shape: write scripts/adapters/<provider>/order_dispatch.js, deploy, document outlet_providers.settings requirements, sandbox-validate.


Walkthrough — using the Revel built-in transformer

Step 1. Confirm the script is deployed

The transformer script lives in central.adapter_scripts keyed by (provider, topic). Deploy from source:

go run cmd/deploy-scripts/main.go --dry-run        # what would happen
go run cmd/deploy-scripts/main.go                  # write to DB (UPSERT keyed by provider+topic)

You should see at least:

_standard / order_dispatch  v1.0.0  (active)
revel     / order_dispatch  v1.2.0  (active)

Without the revel row, requests that pass builtInTransformer:"revel" get HTTP 400 (unknown built-in transformer "revel").

Step 2. Map the outlet to the provider

Stash credentials in outlet_providers.settings for the outlet. Required fields per the script's contract (see docs/guides/adapters/revel.md for the full list):

curl -sS -X PUT "$BASE/api/v1/outlet-providers/$OUTLET" \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: $TENANT" \
  -d '{
    "providerSlug":  "revel",
    "externalId":    "<revel-establishment-id>",
    "externalName":  "<your outlet>",
    "settings": {
      "baseUrl":              "https://<your-revel>.revelup.com",
      "apiKey":               "<real-key>",
      "apiSecret":            "<real-secret>",
      "establishmentId":      <int>,
      "pickupDiningOption":   <int>,
      "deliveryDiningOption": <int>,
      "eatinDiningOption":    <int>,
      "paymentTypeId":        7,
      "serviceFeeAlias":      "Cata Fee"
    }
  }'

Notes:

  • The script throws if apiKey, apiSecret, establishmentId, or the dining option matching the order's deliveryMethod is missing — the dispatch lands as dispatched: false with the exact reason.
  • baseUrl is what reroutes outbound traffic to real Revel. Leave it unset during local testing if you want the transformer to inherit the registered webhook URL (so the same Hookdeck/webhook.site receiver gets both paths).
  • The secret half of the HMAC is on the webhook record, NOT here. The transformer path doesn't HMAC.

Step 3. Dispatch with the opt-in flag

curl -sS -X POST "$BASE/api/v1/orders/dispatch" \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: $KEY" \
  -d '{
    "outletId": "'"$OUTLET"'",
    "event":    "order.paid",
    "builtInTransformer": "revel",
    "order": { ...same Cata Order JSON as the standard path... }
  }'

A successful run returns:

{
  "code": 200, "isSuccess": true, "message": "order dispatched",
  "orderId": "<uuid>", "dispatched": true, "reason": ""
}

What landed at Revel (or your test receiver):

Item Value
URL <settings.baseUrl>/specialresources/cart/submit (or the registered webhook URL when baseUrl is unset)
Method POST
Content-Type text/plain (legacy Revel parity)
API-AUTHENTICATION <apiKey>:<apiSecret> from settings
X-Cata-Signature / X-Cata-Event / X-Cata-Delivery-ID / X-Cata-Timestamp absent — auth is the script's responsibility on the transformer path
Body Revel cart-submit JSON: {skin:"WebPortal", establishmentId, items[].product, items[].modifieritems, orderInfo, paymentInfo, …} — see docs/guides/adapters/revel.md for the field map

Step 4. Verify locally before pointing at real Revel

There's a one-shot smoke test that exercises both paths against the same Hookdeck receiver so the difference is obvious side-by-side:

export KEY=<partner-api-key>
./script/demo/test-order-dispatch.sh setup       <outletId> https://<receiver>
./script/demo/test-order-dispatch.sh standard    <outletId>     # Cata Order JSON + HMAC
./script/demo/test-order-dispatch.sh transformer <outletId>     # Revel cart-submit, no HMAC
./script/demo/test-order-dispatch.sh unknown     <outletId>     # 400 with "unknown built-in transformer"

A short Hookdeck-receiver smoke recipe is the fastest local end-to-end check before pointing dispatch at real Revel.

Step 5. Cut over to real Revel

Once you've verified locally:

  1. Update outlet_providers.settings.baseUrl to the real Revel base URL (the transformer derives <baseUrl>/specialresources/cart/submit).
  2. Replace stub credentials with the real apiKey, apiSecret, establishmentId, dining-option IDs, etc.
  3. Decide what the standard path should do for this outlet:
    • If you want production calls only via the transformer — delete the webhook (DELETE /api/v1/webhooks/{webhookId}) so a stray request without builtInTransformer doesn't accidentally POST to Hookdeck.
    • If both paths are valid — replace the webhook URL with your real receiver via PUT /api/v1/webhooks/{webhookId}.

Watch the first few real calls for 422 responses — that's the signal we got a field shape wrong. The Revel transformer's "Known uncertainties remaining for sandbox" are listed in docs/guides/adapters/revel.md.


Going further: building a transformer for a new provider

Adding a new POS = writing a transformer script and deploying it. No Go changes are needed for the dispatch flow.

  1. Read the contract. Adapter Script Contracts → order_dispatch describes the input shape (input.order, input.outlet.settings, input.event, input.standard) and the required output shape (url, method, headers, body).
  2. Write scripts/adapters/<provider>/order_dispatch.js with @provider, @topic, @version JSDoc tags. Throw on missing required settings; return the spec for a successful build.
  3. Document settings + field map in docs/guides/adapters/<provider>.md.
  4. Deploy with go run cmd/deploy-scripts/main.go.
  5. Smoke-test locally using the same script/demo/test-order-dispatch.sh transformer flow, just with your provider name.
  6. Sandbox-validate against the real POS API; record open uncertainties under "Known uncertainties remaining for sandbox" in the provider doc.

See also