Skip to content

POS API Requirements — what your POS needs to provide

Version 1.0 · 4 September 2026 · Audience: POS vendors integrating with Cata via a Cata-built adapter.

This is the standard requirements document. It is the same for every POS vendor — send it as-is at first contact, before any scoping call.


How this integration works

Who builds what

flowchart LR
  subgraph CATA["CATA — we build and run all of this"]
    direction TB
    APP["Cata ordering app<br/>(diner places order)"]
    ADP["POS adapter<br/><b>written by Cata</b>"]
    RCV["Webhook receiver<br/><code>CATA_WEBHOOK_URL</code>"]
    APP --> ADP
  end

  subgraph POSS["YOUR POS — you build and run all of this"]
    direction TB
    API["Your API<br/><code>POS_BASE_URL</code>"]
    TERM["POS terminal / kitchen"]
    API --> TERM
  end

  ADP ==>|"1 connection test · 2 outlets<br/>3 products · 4 create order"| API
  TERM ==>|"5 order status"| RCV

The boundary is the only thing that matters here. Everything in the left box is Cata's — including the adapter that talks to you, which we write and maintain. Everything in the right box is yours.

Four arrows point right: those are the endpoints you host and Cata calls (POS_BASE_URL). One arrow points left: that is the status webhook, the single call you make to us (CATA_WEBHOOK_URL).

You do not build a client against Cata's API, and you do not need Cata credentials for the catalogue side. The one exception is that one arrow back.

So: four endpoints you expose, one webhook you send.

Want the exact spec? Start at Appendix A.

Appendix A — Reference API gives the precise endpoints and payloads Cata's adapter expects. If you can implement it exactly, do — an exact implementation needs almost no mapping and is the fastest path to a working integration.

If your API already exists and differs, that is fine and expected. The sections below describe what the data must contain, independent of shape; we map the rest.

The Cata side of this pattern is already in production with other POS systems, so the runtime, the dispatch pipeline and the status receiver all exist. What is new for each vendor is the adapter itself.


What you need to provide

# Capability Direction Method Path Body What you provide
1 Connection test Cata → You GET {POS_BASE_URL}/v1/health — Any cheap authenticated read
2 Get outlets Cata → You GET {POS_BASE_URL}/v1/outlets — List of your stores / locations
3 Get products Cata → You GET {POS_BASE_URL}/v1/outlets/{outletId}/products — Catalogue including modifiers and bundles
4 Create order Cata → You POST {POS_BASE_URL}/v1/outlets/{outletId}/orders Cata order DTO, or your own format — §4.1 Endpoint that accepts a paid order
5 Order status webhook You → Cata POST {CATA_WEBHOOK_URL} Status JSON Outbound call when an order progresses

The same five, split by who receives the call:

flowchart LR
  ADP["Cata<br/>adapter"]
  RCV["Cata<br/>webhook receiver"]

  subgraph SERVE["4 APIs YOU PROVIDE — Cata calls these"]
    direction TB
    E1["1 · GET /v1/health"]
    E2["2 · GET /v1/outlets"]
    E3["3 · GET /v1/outlets/:outletId/products"]
    E4["4 · POST /v1/outlets/:outletId/orders"]
  end

  subgraph OUTBOUND["1 API YOU CALL — Cata provides this"]
    direction TB
    E5["5 · POST to CATA_WEBHOOK_URL<br/>order status"]
  end

  ADP ==> E1
  ADP ==> E2
  ADP ==> E3
  ADP ==> E4
  E5 ==> RCV

  classDef serve fill:#eef4ff,stroke:#3b6fd4,stroke-width:1px;
  classDef outbound fill:#eefaf0,stroke:#2e8b57,stroke-width:1px;
  class E1,E2,E3,E4 serve;
  class E5 outbound;

Read the Direction column carefully. Rows 1–4 are endpoints you host and Cata calls — all four live under your {POS_BASE_URL}. Row 5 is the opposite: Cata hosts it, you call it. You are not building an endpoint for row 5 — you are building an outbound HTTP call from your POS to a URL we give you.

So the work on your side is four endpoints, plus one outbound call — plus an OAuth token endpoint if you use OAuth (A.1).

Placeholders used in this document

Every URL below is written with one of these. The prefix tells you whose server it is.

Placeholder Whose Meaning
{POS_BASE_URL} Yours The root of your POS API — e.g. https://api.your-pos.com, or https://{merchant}.your-pos.com if each merchant gets their own host. You tell us this value; Cata stores it per merchant.
{CATA_WEBHOOK_URL} Ours Where you POST order status updates. Cata gives you this value, one per merchant. It is the only Cata URL you call.
{access_token} Yours Issued by your auth endpoint (A.1), or a static API key
{outletId} Yours An outlet id from §2

If you see {POS_BASE_URL}, you are building it. If you see {CATA_WEBHOOK_URL}, we are.

Every capability section below opens with its request contract — method, path, headers, query params and body. Paths are the reference shape from Appendix A; mount them wherever suits your API, but keep the methods, parameters and payloads.

The gate — endpoints alone are not enough

Cata does not begin building the adapter for a capability until it has both:

  1. the endpoint, and
  2. a real sample payload for it.

Redact customer data as needed, but send genuine responses. Where vendor documentation and a real payload have disagreed in past integrations, the payload was right. Partial delivery is fine — each capability unblocks independently.


Authentication and connectivity

Authentication

Tell us your auth model and the exact header format. Cata stores your credentials encrypted server-side; they are never exposed to adapter code.

Model What we need
API key Header name and exact value format
OAuth 2.0 Token endpoint, grant type, scopes, token lifetime, refresh behaviour. If the token response also returns a per-merchant base URL, name that field — we support the pattern.
Other Full description plus a working example request

Do you have sandbox credentials? Building against production is possible but slower and riskier for your live merchants.

Connectivity

Question Why we ask
Base URL Global, or per-merchant (e.g. https://{merchant}.example.com)? Both are supported.
Environments Sandbox and production base URLs
Rate limits Requests/minute, burst, and your 429 behaviour. A full catalogue sync can be many calls.
Pagination Exact parameter names, and how we know we have reached the last page.
Error envelope Non-2xx on failure, or HTTP 200 with an error body? Both are workable — we must know which, with an example of each.

Data formats

Field Cata uses Tell us
Prices Decimal (e.g. 35.00) Integer minor units or decimal? The adapter converts — we need certainty, not a particular choice.
IDs String Integers and GUIDs are both fine. Confirm they are stable over time.
Timestamps ISO-8601 / RFC 3339 UTC or local? Format? Timezone source?
Currency ISO 4217 Fixed per outlet, or on the payload?

1. Connection test

The cheapest authenticated read that proves a credential set is valid. Called when an operator saves credentials, and before any sync.

Request contract

Method GET
Path {POS_BASE_URL}/v1/health (any read-only path you prefer)
Headers Authorization: Bearer {access_token}
Query params none
Request body none
Expected status 200 valid · 401/403 invalid credentials

Full response schema: A.2.

  • Must be read-only.
  • Must return a distinguishable 401/403 for bad credentials. If an auth failure comes back as HTTP 200 with an error body, say so — we will handle it, but silently reporting a healthy connection is the failure mode to avoid.
  • "Get merchant info" or "list outlets" is ideal. If your outlets endpoint is cheap, it can double as the connection test.

2. Get outlets

Every location the credentials can access. An operator maps each one to a Cata outlet.

Request contract

Method GET
Path {POS_BASE_URL}/v1/outlets
Headers Authorization: Bearer {access_token}
Query params cursor (optional — from nextCursor), limit (optional)
Request body none
Expected status 200

Full response schema: A.3.

Field Required Notes
Outlet id Required Stable identifier. Becomes the mapping key and is reused when Cata sends you orders — it must not change for the life of the store.
Name Required Shown in the operator's dropdown
Address Optional Secondary text, to disambiguate similar names

Return all locations, paginating if needed. An empty list is valid, not an error.

3. Get products

The menu — and usually the largest piece of work, because Cata needs the full product structure, not just names and prices.

Request contract

Method GET
Path {POS_BASE_URL}/v1/outlets/{outletId}/products
Path params outletId — the id from §2
Headers Authorization: Bearer {access_token}
Query params cursor (optional), limit (optional). Tell us if modifiers or bundles need an extra include-style param — see Pitfall 2.
Request body none
Expected status 200

Full response schema: A.4.

One endpoint or several is fine (products, modifier groups, options separately). Tell us which exist and how they join.

Cata's catalogue is flat. Cata imports a flat list of products — no category or menu tree. Menu structure, sections and ordering are built afterwards in Cata's Menu Manager. Send a category label per product if you have one (we keep it for reference), but do not model the hierarchy — it is discarded.

Per product

Concept Required Notes
Item code / SKU Required Stable key. Cata sends it back on every order line.
Name Required Customer-facing
Base price Required Before modifiers
Description / short description Optional Product page copy
Image URL Optional Publicly reachable; Cata copies it into its own storage
Product kind Optional Simple item, variant, or bundle/combo
Category label Optional Retained for reference only

Modifiers — and the selection rules

Cata models modifiers as groups (e.g. "Choice of milk") holding options (e.g. "Oat milk, +0.80"). Your response must express how many options a customer must and may choose.

Concept Required Meaning
Group id Required Stable identifier
Group name Required Shown to the customer
Required vs optional Required Must the customer choose at least one? Cata stores a minimum-selection count (0 = optional).
Single vs multi-select Required May they choose more than one? Cata stores a maximum-selection count (1 = single).
Min / max selection Required The explicit numbers where you have them (e.g. "pick 2 to 4")
Repeat same option Optional May the same option be taken more than once?
Option id / name / price delta Required Option id is sent back to you on every order
Option is default Optional Pre-selected

Pitfall 1 — selection rules must come from your API

In a previous POS integration the catalogue endpoint did not expose required/optional or min/max at all. Cata had to apply defaults, which produced wrong ordering rules for the merchant, and the integration was not correct until the vendor added those fields — months later.

Please confirm at the outset that your API returns them. If it does not, treat adding them as part of this project's scope, not a later refinement. It is the single most common cause of delay.

Variants

If a product has variants (sizes, styles) with their own SKU and price, tell us how they relate to the parent — typically a shared header or parent id. Cata imports each variant as its own product, so we must be able to name them unambiguously.

Bundles and combos — use this shape

Rather than ask how your POS models combos, here is the shape Cata wants. If you can express your bundles this way, the integration is straightforward.

A bundle is a sellable product whose contents are one or more sections. Each section is a slot in the combo — "Main", "Side", "Drink" — and carries:

Per section Meaning
Section id Stable identifier for the slot
Section name "Main", "Side", "Drink" — shown to the customer
minSelection / maxSelection How many items the customer takes from this slot
Eligible items The SKUs that may fill the slot, each with an optional price surcharge

Fixed combos use the same shape. A combo with no customer choice is simply sections where each has exactly one eligible item and minSelection = maxSelection = 1.

This is the important part. Do not model fixed bundles and choice bundles as two different things. One shape covers both, which means one code path in the adapter, one set of edge cases, and no ambiguity about which kind a given product is. Past integrations that used separate representations cost time in exactly this area.

{
  "itemCode": "COMBO-LUNCH-01",
  "name": "Lunch Combo",
  "basePrice": 49.00,
  "kind": "bundle",
  "bundleSections": [
    {
      "itemCode": "SEC-MAIN",
      "name": "Main",
      "minSelection": 1,
      "maxSelection": 1,
      "items": [
        { "itemCode": "SKU-BURGER-01", "surcharge": 0.00 },
        { "itemCode": "SKU-WRAP-01",   "surcharge": 3.00 }
      ]
    },
    {
      "itemCode": "SEC-DRINK",
      "name": "Drink",
      "minSelection": 1,
      "maxSelection": 1,
      "items": [ { "itemCode": "SKU-COLA-01", "surcharge": 0.00 } ]
    }
  ]
}

The second section above is a fixed component — one eligible item, exactly one taken — and needs no special handling.

If your POS cannot express this, tell us how it does model combos and we will map it. But send us this shape if you can: it is the one Cata's catalogue is built around, and mapping to it is the cheapest path.

What happens to a bundle at order time

By default, Cata decomposes a bundle before sending the order: the bundle SKU is dropped and its chosen components are sent as standard order lines, with the bundle's total redistributed across them. Most POS systems accept this without any bundle-specific support, which is why it is the default.

If your POS has a native combo line type and would rather receive the bundle intact, say so and include the request shape — we can send it that way instead. Be aware that a native combo path is stricter: in a past integration the POS rejected the bundle SKU on every order type, and the native path had to be abandoned in favour of decomposition.

Pitfall 2 — bundle contents that come back empty

In a previous integration, bundle contents were null unless a specific include/expand parameter was passed. It is easy to miss, and it looks like "this merchant has no combos" rather than an error.

If your API requires a parameter to return modifiers, bundle contents, or per-outlet prices, tell us the exact parameter.

4. Create order

When a customer pays in the Cata app, Cata calls your POS. This is the most important endpoint: an order that fails here does not reach the kitchen.

Request contract

Method POST
Path {POS_BASE_URL}/v1/outlets/{outletId}/orders
Path params outletId — the id from §2
Headers Authorization: Bearer {access_token} · Content-Type: application/json · (optional) an idempotency-key header if you support one
Request body JSON. Which schema depends on your route — Cata's order DTO verbatim if you implement A.5, or your own request format if you have an existing order API. See §4.1.
Expected status 200 or 201 accepted · 4xx or accepted:false rejected

Full request/response schema: A.5.

4.1 The order Cata sends

This is Cata's order DTO. Which wire format actually reaches your endpoint depends on which route you take:

Your choice What your endpoint receives
Implement A.5 exactly (the reference API) This DTO, verbatim. No mapping — the adapter forwards it.
Use your own existing order API Your own request format. The adapter maps this DTO into it, so your endpoint never sees the shape below.

Either way you need to read this table: in the first case it is your request schema, and in the second it is the source data we map from.

A field absent here cannot be sourced from the order payload — but it may still be populated from per-outlet configuration. Cata stores per-outlet values for exactly this purpose: payment type ids, dining-option ids, revenue centres, and anything else your create-order call needs that is a property of the outlet rather than the order. So if your request format requires a field you cannot see below, tell us which ones are configuration-derived and we will hold them per outlet — see §4.2.

{
  "uuid": "ord-00000000-0000-0000-0000-000000000000",
  "orderRefNo": "ORD-2026-001",
  "dailyQueueNo": "A12",
  "storeUuid": "00000000-0000-0000-0000-000000000000",
  "deliveryMethod": "DELIVERY",
  "status": "PAID",
  "currency": "SGD",
  "isPreorder": false,
  "customer": {
    "fullName": "Sample Customer",
    "phone": "+6500000000",
    "email": "customer@example.com",
    "address": "88 Sample Road"
  },
  "items": [
    {
      "plu": "SKU-BURGER-01",
      "name": "Classic Burger",
      "quantity": 2,
      "itemOnlyPrice": 35.00,
      "modifierOnlyPrice": 5.00,
      "itemPrice": 40.00,
      "itemSubTotal": 80.00,
      "notes": "No onions",
      "modifiers": [
        {
          "modifierHeaderId": "MG-CHEESE",
          "modifierOptionId": "MO-EXTRA",
          "modifierHeaderName": "Cheese",
          "modifierOptionName": "Extra Cheese",
          "price": 5.00,
          "quantity": 1
        }
      ]
    }
  ],
  "subtotal": 80.00,
  "discountTotal": 0.00,
  "serviceCharge": 0.00,
  "deliveryFee": 5.00,
  "packagingFee": 0.00,
  "totalPay": 85.00,
  "payment": { "amount": 85.00, "tip": 0.00, "method": "CARD" },
  "expectedPickupAt": "2026-09-04T10:30:00Z",
  "tableNo": "",
  "createdAt": "2026-09-04T10:00:00Z"
}
Field Type Always sent Meaning
uuid string yes Cata's order id, and the idempotency key — see §4.3
orderRefNo string yes Human-readable reference, safe to print on a ticket
dailyQueueNo string no Short per-day collection number
storeUuid string yes Cata's outlet id. Your own outlet id is in the URL path.
deliveryMethod enum yes PICKUP · DELIVERY · EATIN
status string yes Always PAID — Cata does not dispatch unpaid orders
currency string yes ISO 4217
isPreorder boolean yes true when scheduled for later; see expectedPickupAt
customer object no Omitted entirely for anonymous orders — do not expect an empty object. See the customer table below.
items[] array yes At least one line — see the item table below
subtotal decimal yes Sum of itemSubTotal before order-level adjustments
discountTotal decimal yes Total discount applied; 0.00 when none
serviceCharge decimal yes 0.00 when none
deliveryFee decimal yes 0.00 for non-delivery
packagingFee decimal yes 0.00 when none
totalPay decimal yes Final amount the customer paid — the authoritative total
payment object yes Always present; Cata dispatches only paid orders. See the payment table below.
expectedPickupAt ISO-8601 no Present for preorders and scheduled collection
tableNo string no Set for EATIN where the merchant uses table numbers
createdAt ISO-8601 yes When the order was placed

customer — absent entirely for anonymous orders.

Field Type Always sent Meaning
fullName string yes when customer present Display name
phone string no Some POS require this to create a customer record — tell us if so
email string no
address string no Present for DELIVERY

items[] — one entry per order line.

Field Type Always sent Meaning
plu string yes Your itemCode from §3
name string yes Product name as shown to the customer at order time. Informational — match on plu, not on this.
quantity integer yes Always ≥ 1
itemOnlyPrice decimal yes Unit price excluding modifiers
modifierOnlyPrice decimal yes Sum of modifier prices for one unit; 0.00 when none
itemPrice decimal yes Per unit, including modifiers
itemSubTotal decimal yes quantity × itemPrice
notes string no Customer's note for the kitchen
modifiers[] array no Omitted or [] when none — see the modifier table below

items[].modifiers[] — one entry per selected option.

Field Type Always sent Meaning
modifierHeaderId string yes Your modifier-group itemCode from §3
modifierOptionId string yes Your option itemCode. This is the match key.
modifierHeaderName string yes Group name, informational
modifierOptionName string yes Option name, informational
price decimal yes Price for one unit of this option; 0.00 when free
quantity integer yes Always ≥ 1. Above 1 only where the group allows repeats.

payment — Cata orders are always already settled.

Field Type Always sent Meaning
amount decimal yes Amount paid. Equals totalPay.
method string yes unless the order is free How the customer paid, e.g. CARD, ONLINE. Map to your prepaid tender type — not to cash. Absent only on a free order (amount and totalPay both 0.00, e.g. a 100% voucher) — record no tender line for it.
tip decimal no 0.00 or absent when none. If your POS has no tip field, tell us and we will drop it.

Bundles: a chosen bundle arrives decomposed into its component lines by default — see what happens to a bundle at order time.

4.2 What we need from you

We need Why
Full request schema Every field, which are required, accepted enum values
A sample request A real create-order call that succeeds
A sample success response See the critical requirement below
A sample error response So we can tell a rejected order from an outage
Required per-outlet configuration Anything beyond the order itself — location id, payment type, dining-option ids, revenue centre. Cata stores these per outlet.
How orders are paid Cata orders are already paid. Tell us how to represent a settled, prepaid order so your POS does not ask staff to take payment again.

Critical — return your order ID

Your success response must include the order identifier your POS assigned. Cata stores it and uses it to match your later status updates back to the right order.

Without a returned order ID, status updates cannot be matched and the integration cannot be completed.

4.3 Duplicate protection

Requirement: de-duplicate on uuid. A repeated create-order carrying a uuid you have already accepted must not create a second ticket. Return the original orderId and the same response you returned the first time, as if the call had just succeeded.

Why this is a requirement and not a nicety. A delivery can repeat — a network timeout leaves the outcome ambiguous, and the safe response to an ambiguous outcome is to deliver again. Cata-side de-duplication on order.uuid is in progress; treat your own as required regardless, since it is what protects the merchant from a duplicate kitchen ticket.

uuid is stable across every delivery of the same order, which is what makes it usable as the key. Treat it as a unique constraint with an atomic insert-or-fetch — not a check-then-insert, which races under exactly the concurrent retries it is meant to guard against.

If you support an idempotency key header, name it and Cata will send uuid in it as well.

If your POS genuinely cannot enforce this, say so early — it is a material limitation and we will need to discuss how to contain it, not discover it in production.

5. Order status webhook

As the order moves through your kitchen, tell Cata. This drives the customer's live order tracking, so latency here is visible to the diner.

Cata provides the destination URL. Cata decides the URL — it is not configurable by you.

Request contract — note the direction reverses here: you make this call.

Method POST
URL {CATA_WEBHOOK_URL} — provided by Cata, per tenant
Headers Content-Type: application/json · X-Signature: sha256={hex} · X-Timestamp: {unix-seconds}
Request body { orderId, status, reason, occurredAt } — JSON
Expected status 200 accepted. Retry only on network errors, 408, 429 and 5xx, with bounded backoff. Treat other 4xx as terminal — see A.6.

Full schema and signing rules: A.6.

What we need:

  • Which events fire, and when.
  • Your signature scheme — algorithm, exactly what is signed (raw body? body plus timestamp?), and which header carries it.
  • A sample body per event.
  • The status mapping table below, completed.
  • Which events Cata must ignore — e.g. status changes on orders rung up in-store that did not come from Cata.
  • Your retry policy on non-2xx.

Pitfall 3 — which field actually means 'the kitchen has progressed'?

In a previous integration the obvious top-level status field read "completed" from the very first event — because the sale was settled up front, which is always true for Cata orders. Mapping off it would have marked every order complete the moment it was created. The real fulfilment progress lived in a different, less obvious field.

Tell us explicitly which field represents kitchen/fulfilment progress, and which fields are about payment or the sale record instead.

Status mapping — please complete

Cata status Meaning Your equivalent
ACCEPTED POS accepted the order
IN PROGRESS Kitchen has started
READY Ready for collection / handover
DRIVER PICKED UP Courier collected
COMPLETED Fulfilled
CANCELLED Cancelled — reason required

Note the literal spaces in IN PROGRESS and DRIVER PICKED UP. Cata enforces a state machine — transitions that move backwards are rejected.


Optional capabilities

Not required for a working integration. Tell us yes/no and we will scope separately.

Capability If yes, we need
Item / modifier snooze Endpoint to mark an item unavailable at one outlet, and how it is restored
Per-outlet pricing Whether prices differ by outlet, and how they are returned
Stock levels Endpoint, and whether counts are per-outlet
Tags / allergens Where they live on the product
In-store order sync Whether till-rung orders should also appear in Cata

Cata-side rules worth knowing

Rule What it means for you
Your POS owns product structure Modifier groups, selection rules and bundle composition come from your catalogue and are trusted as-is. Cata will not override them. Operators edit display fields only.
Cata's menu structure is Cata's Categories, sections and ordering are built in Cata's Menu Manager, not imported from your hierarchy.
Cancel and refund are Cata-owned Customer-side cancellation happens in Cata's back office. You can report a kitchen-side cancellation with a reason.
Orders arrive pre-paid Every order Cata sends is already settled.

What to send back

Items 1–3 let us scope; item 4 lets us build.

  1. API documentation URL, and whether it is public or behind a login.
  2. Auth details and sandbox credentials.
  3. Answers to the connectivity and data-format questions.
  4. Per capability: endpoint + a real sample payload.
  5. The completed status mapping table.
  6. Explicit answers to the three pitfalls: do you return modifier selection rules? does anything need an include parameter? which field is fulfilment progress?
  7. Whether you can implement the reference API in Appendix A as-is, or which parts differ.

Appendix A — Reference API

If you can implement this exactly, do. These are the precise endpoints and payloads Cata's adapter expects. An exact implementation needs almost no mapping, which is the fastest and least error-prone path to a working integration.

If you cannot, that is fine — this is a target, not a mandate. Send us your own shapes per the sections above and we will map them. But diverge deliberately, not by accident: the closer you are to this, the shorter the build.

All examples use placeholder values. Prices are decimal. All ids are strings.

A.1 Authentication

Preferred: OAuth 2.0 client credentials, or a bearer token.

Authorization: Bearer {access_token}

An API key header (X-API-Key: {key}) is equally acceptable. Tell us which, and the exact header name.

If you use OAuth, the token endpoint is yours to host too — it is a fifth endpoint on your side, in addition to the four in the table above:

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id={id}&client_secret={secret}
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600
}

If your token response also returns a per-merchant base URL (some POS platforms do, so that each merchant's API lives on its own host), name that field — we support the pattern and will use it in preference to a configured base URL.

With a plain API key there is no token endpoint: you provide the key out of band and Cata sends it on every request.

A.2 Connection test

GET {POS_BASE_URL}/v1/health
Authorization: Bearer {access_token}

200 OK

{ "ok": true, "merchant": "Example Merchant" }

Return 401 or 403 for invalid credentials. The body is not important; the status code is.

A.3 Outlets

GET {POS_BASE_URL}/v1/outlets
Authorization: Bearer {access_token}

200 OK

{
  "outlets": [
    {
      "id": "OUTLET-001",
      "name": "Example Store — Central",
      "address": "1 Example Street, #01-01",
      "timezone": "Asia/Singapore",
      "active": true
    }
  ],
  "nextCursor": null
}
Field Type Required Notes
id string yes Stable for the life of the store. Reused as the path parameter below.
name string yes
address string no
timezone string no IANA name, if you have it
active boolean no Omit or true if you do not track this
nextCursor string | null yes null on the last page; otherwise pass back as ?cursor=

Pagination — worked example

The same rules apply to products. Cata keeps calling while nextCursor is non-null, passing the previous response's value back verbatim.

Call 1 — no cursor:

GET {POS_BASE_URL}/v1/outlets
Authorization: Bearer {access_token}
{
  "outlets": [ "…200 outlets…" ],
  "nextCursor": "b2Zmc2V0OjIwMA"
}

nextCursor is non-null, so there is more. Cata calls again with it.

Call 2 — cursor echoed back:

GET {POS_BASE_URL}/v1/outlets?cursor=b2Zmc2V0OjIwMA
Authorization: Bearer {access_token}
{
  "outlets": [ "…47 outlets…" ],
  "nextCursor": null
}

nextCursor is null. Cata stops. Total collected: 247.

Single-page case — if everything fits in one response, return "nextCursor": null on the first call. Cata makes exactly one request.

Rules

  • The cursor is opaque to Cata. We never parse it — we echo it back exactly as given. Encode an offset, a row id, a page number, a timestamp, whatever suits your storage.
  • null ends the loop. Send JSON null, not "", not the string "null", and do not omit the field. An absent or empty-string cursor is ambiguous and we will treat it as an error rather than guess.
  • Never return a non-null nextCursor for an empty page. That is the shape that causes an infinite loop. If there is no more data, the cursor is null.
  • A stable ordering matters. If rows can shift between calls, items can be skipped or duplicated across pages. Order by something immutable — a created-at plus id, for instance.
  • Tell us your page size, and whether limit is honoured or ignored.

If your API paginates differently

Offset/limit, page/page_size, or a next URL are all fine — this is a reference shape, not a requirement. Tell us which you use and how the last page is signalled, and the adapter will follow it. What we cannot work with is pagination that is undocumented or has no reliable end condition.

A.4 Products

GET {POS_BASE_URL}/v1/outlets/{outletId}/products?cursor={cursor}&limit=200
Authorization: Bearer {access_token}

200 OK

{
  "products": [
    {
      "itemCode": "SKU-BURGER-01",
      "name": "Classic Burger",
      "description": "Beef patty, lettuce, tomato",
      "basePrice": 35.00,
      "kind": "standard",
      "category": "Mains",
      "imageUrl": "https://cdn.example.com/burger.jpg",
      "available": true,
      "modifierGroups": [
        {
          "itemCode": "MG-CHEESE",
          "name": "Cheese",
          "minSelect": 0,
          "maxSelect": 2,
          "allowMultipleQty": false,
          "options": [
            { "itemCode": "MO-EXTRA", "name": "Extra Cheese", "additionalPrice": 5.00, "isDefault": false },
            { "itemCode": "MO-NONE",  "name": "No Cheese",    "additionalPrice": 0.00, "isDefault": false }
          ]
        }
      ],
      "bundleSections": []
    }
  ],
  "nextCursor": null
}
Field Type Required Notes
itemCode string yes Your SKU. Sent back on every order line.
name string yes
basePrice decimal yes Before modifiers
kind enum yes standard · variant · bundle
parentItemCode string if variant The parent this variant belongs to
description string no
category string no Retained for reference; Cata builds its own menu structure
imageUrl string no Publicly reachable
available boolean no Omit or true if not tracked
modifierGroups array yes [] when the product has none — do not send null
bundleSections array yes when kind is bundle [] otherwise
nextCursor string | null yes null on the last page — see the worked example

Modifier group — minSelect 0 = optional, 1+ = required. maxSelect 1 = single-select, N = multi-select. Both are required; see Pitfall 1.

Bundle section — as specified in Bundles and combos: itemCode, name, minSelection, maxSelection, and items[] of { itemCode, surcharge }.

Return the whole catalogue in one paginated pass. Modifiers, options and bundle sections must be present in this response — not behind a second call per product, and not gated behind an include parameter we have to know about. If they are, say so explicitly (Pitfall 2).

A.5 Create order

POST {POS_BASE_URL}/v1/outlets/{outletId}/orders
Authorization: Bearer {access_token}
Content-Type: application/json

The body is Cata's order DTO, forwarded verbatim — the full schema and field tables are in §4.1.

This is the defining difference between implementing the reference API and using your own: here your endpoint accepts Cata's shape directly and the adapter does no order mapping at all. If you use your own order API instead, the adapter maps the DTO into your format and this section does not apply to you — see the table in §4.1.

200 OK / 201 Created

{
  "orderId": "POS-987654",
  "accepted": true,
  "message": null,
  "estimatedReadyAt": "2026-09-04T10:25:00Z"
}
Field Type Required Notes
orderId string yes Your order id. Cata stores it and matches your status updates against it. Without this the integration cannot be completed.
accepted boolean yes false = you rejected the order
message string | null no Reason when accepted is false
estimatedReadyAt ISO-8601 no Surfaced to the customer if present

Rejection — return the same body with "accepted": false and a message, or a 4xx with an error body. Either works; tell us which you use.

If you support an idempotency key, name the header and Cata will send the order's uuid in it.

A.6 Order status webhook

You send this to a URL Cata provides, whenever an order's fulfilment state changes.

POST {CATA_WEBHOOK_URL}
Content-Type: application/json
X-Signature: sha256={hex}
X-Timestamp: {unix-seconds}

Signature. HMAC-SHA256, hex-encoded, over the string {X-Timestamp}.{raw body} — timestamp, a literal full stop, then the raw bytes you are sending. The key is a shared secret Cata provides per merchant.

The timestamp must be inside the signed message. Signing only the body leaves a valid request replayable forever by anyone who captures it. Cata rejects any request whose X-Timestamp is more than 5 minutes from our clock, which bounds that window — but that check is only meaningful because the timestamp is signed and therefore cannot be rewritten.

Use Unix seconds for X-Timestamp, and keep your clock synchronised (NTP); a POS whose clock has drifted past the window will have every delivery rejected.

If your platform already has a signing scheme, describe it and we will verify against it instead — provided it binds a timestamp or nonce. A body-only signature with no freshness bound is not something we can accept for order state changes.

{
  "orderId": "POS-987654",
  "status": "IN PROGRESS",
  "reason": null,
  "occurredAt": "2026-09-04T10:12:00Z"
}
Field Type Required Notes
orderId string yes The id you returned from A.5
status enum yes Exactly one of the six Cata statuses — note the literal spaces
reason string | null when CANCELLED Required on cancellation
occurredAt ISO-8601 no Defaults to receipt time if absent

Send only fulfilment transitions. Do not send payment or sale-record events, and do not send events for orders that did not originate from Cata — see Pitfall 3.

Response handling — retry the transient, stop on the permanent.

Cata returns Meaning You should
200 Accepted Done
network error, timeout Cata unreachable Retry with bounded exponential backoff
408, 429 Timeout / rate-limited Retry with backoff; honour Retry-After when present
5xx Fault on Cata's side Retry with backoff
401, 403 Signature or credential rejected Terminal — retrying cannot fix it. Alert your team.
400, 422 Malformed payload, or a status transition Cata rejects (e.g. moving backwards) Terminal — do not retry. Log it and move on.
404 Unknown orderId Terminal — the order did not come from Cata, or was never dispatched

Retrying a terminal 4xx indefinitely just grows your queue and our logs without ever succeeding. Cap total attempts and elapsed time on the retryable cases too — we suggest roughly 6 attempts over about 15 minutes, then give up and alert. Tell us the policy you settle on.

A.7 Summary

# Method Path Purpose
1 GET {POS_BASE_URL}/v1/health Connection test
2 GET {POS_BASE_URL}/v1/outlets List outlets
3 GET {POS_BASE_URL}/v1/outlets/{outletId}/products Catalogue, paginated
4 POST {POS_BASE_URL}/v1/outlets/{outletId}/orders Create order — receives Cata's DTO verbatim on this route
5 POST {CATA_WEBHOOK_URL} Status webhook — you call us

Rows 1–4 sit under your POS_BASE_URL. Row 5 is Cata's URL, which we give you.

The /v1/... paths are a suggestion, not a requirement — mount them wherever suits your API. What matters is the methods, the parameters and the payload shapes.