Skip to content

POS Vendor Integration Guide

Readiness — read before you build against this

Status: the endpoints described here are live; the end-to-end journey is unproven. Reviewed 2026-09-04.

The four endpoints (outlets, product sync, webhook registration, order status) ✅ Live in production
This document's full self-integration journey, start to finish ⚠️ Not yet validated end to end. The individual endpoints are exercised in production; this specific sequence has not been proven as a whole.
Automatic retry of order dispatch 🔄 In progress. Until it ships, delivery is at-most-once — see §3.5.
Order cancellation notice to your POS 🔄 In progress (§8.2)

What this means for you. The building blocks are live, but the path as a whole is unvalidated, so expect to hit edges this document does not yet cover. Talk to us before you start — we will work through it with you rather than leave you to find them alone.

If you would prefer Cata to build the connector instead, see POS API Requirements, where you expose an API and we write the adapter. That route is more established.

This guide provides step-by-step instructions for POS systems that want to integrate with Cata's platform. If you're a POS vendor building an integration, follow this guide to understand what you need to implement.


1. Authentication

All POS integrations with Cata use API Key authentication.

1.1 API Key Format

  • Header: X-Api-Key
  • Value: A tenant-scoped API key provided by Cata (ask your Cata partner manager)
  • Scope: Each key is valid for exactly one tenant (restaurant group)

1.2 API Key Validation

Before implementing any integration logic, verify your API key works:

curl -i "https://{tenant}.sgp1.samba-technologies.xyz/service/pos-integration/api/v1/auth/whoami" \
  -H "X-Api-Key: your-api-key-here"

Success Response (200 OK):

{
  "authenticated": true,
  "tenantId": "tenant-uuid-123",
  "apiKeyId": 42,
  "expiresAt": "2026-12-31T23:59:59Z"
}

Failure Responses: - 401 Unauthorized: Key is missing, invalid, or expired - Always treat 401 as a fatal error — don't retry

1.3 Tenant Subdomain Resolution

Each tenant has a unique subdomain. The API routes to the correct database based on the subdomain:

https://{TENANT_SUBDOMAIN}.sgp1.samba-technologies.xyz/service/pos-integration/api/v1/...

Example: If your tenant is "golden-dragon", your base URL is:

https://golden-dragon.sgp1.samba-technologies.xyz/service/pos-integration/api/v1

1.4 API Key Rotation

  • Cata admin console can issue new keys and revoke old ones
  • Rotation requires no coordination — issue a new key, update your integration, then revoke the old one
  • No downtime needed

2. Webhook Registration for Order Status

When Cata receives an order payment, it dispatches the order to your POS via a webhook. You must register exactly one webhook URL per outlet to receive these order notifications.

2.1 Register a Webhook (One-time Setup)

Endpoint: POST /api/v1/webhooks/register

Required Headers: - X-Api-Key: your-api-key - Content-Type: application/json

Request Body:

{
  "outletId": "store-uuid-123",
  "provider": "your-pos-system-slug",
  "callbackUrl": "https://your-pos-api.example.com/webhooks/cata/orders",
  "events": ["order.paid"]
}

Field Explanations:

Field Type Description
outletId UUID The outlet (store location) UUID. Get this from Cata.
provider string A slug uniquely identifying your POS system in Cata's system (e.g., "revel", "your-pos"). One webhook per (outletId, provider).
callbackUrl URL Your public HTTPS endpoint that will receive order payloads. Must be HTTPS. Must be reachable from the internet.
events array Event subscriptions. Currently only ["order.paid"] is supported.

Success Response (201 Created):

{
  "code": 201,
  "isSuccess": true,
  "message": "webhook registered",
  "webhook": {
    "id": 1,
    "outletId": "store-uuid-123",
    "provider": "your-pos",
    "callbackUrl": "https://your-pos-api.example.com/webhooks/cata/orders",
    "events": ["order.paid"],
    "secret": "whsec_abc123xyz789...",
    "isActive": true,
    "createdAt": "2026-03-13T10:00:00Z"
  }
}

⚠️ CRITICAL: Save the Secret Immediately

The secret is shown only once in the registration response. Store it securely — you'll use it to verify webhook signatures. If you lose it, rotate it using the endpoint below.

2.2 Update a Webhook

If you need to change the callback URL or subscribed events later:

Endpoint: PUT /api/v1/webhooks/{webhookId}

Request Body:

{
  "callbackUrl": "https://new-url.example.com/webhooks/cata/orders",
  "events": ["order.paid"],
  "isActive": true
}

All fields are optional — omit any you don't want to change.

2.3 Rotate the Signing Secret

If you suspect the secret is compromised or lost it:

Endpoint: POST /api/v1/webhooks/{webhookId}/rotate-secret

Response:

{
  "secret": "whsec_new_secret_xyz..."
}

A new secret is generated and returned once. Update your local configuration immediately.

2.4 List Your Registered Webhooks

Endpoint: GET /api/v1/webhooks

Response:

{
  "webhooks": [
    {
      "id": 1,
      "outletId": "store-uuid-123",
      "provider": "your-pos",
      "callbackUrl": "https://your-pos-api.example.com/webhooks/cata/orders",
      "events": ["order.paid"],
      "isActive": true,
      "createdAt": "2026-03-13T10:00:00Z",
      "updatedAt": "2026-03-15T14:30:00Z"
    }
  ],
  "total": 1
}

Note: secret is never included in list responses — it's only shown at registration and rotation.

2.5 Delete a Webhook

Endpoint: DELETE /api/v1/webhooks/{webhookId}

This soft-deletes the webhook and stops all deliveries to its callback URL. To resume, re-register with the same (outletId, provider) pair — you'll get a new ID and secret.


3. Receiving Webhook Events

When an order is paid in Cata, Cata POSTs to your registered callbackUrl. This section explains what you receive, how to verify it, and how to parse it.

3.1 Webhook Delivery Headers

Every webhook request includes these headers for identification and signature verification:

Header Value Purpose
X-Cata-Event order.paid Event type
X-Cata-Delivery-ID UUID Identifies this delivery attempt, not the order. A new value on every attempt — do not use it as an idempotency key (see §3.4)
X-Cata-Timestamp Unix timestamp Time Cata dispatched the webhook
X-Cata-Signature sha256=<hex> HMAC-SHA256 signature (see section 3.2)

3.2 Verify the Webhook Signature

Every webhook is signed using your secret. Always verify the signature before processing the order.

Signature Formula:

X-Cata-Signature = "sha256=" + HEXDIGEST( HMAC-SHA256(secret, raw_body) )

Python Example:

import hmac
import hashlib
import json

def verify_webhook(secret, raw_body, signature_header):
    """Verify the X-Cata-Signature header against the webhook body."""
    expected_sig = "sha256=" + hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected_sig, signature_header)

# In your Flask/Django handler:
@app.post('/webhooks/cata/orders')
def handle_order_webhook():
    raw_body = request.get_data()  # Raw bytes, not parsed JSON
    signature = request.headers['X-Cata-Signature']

    if not verify_webhook(SECRET, raw_body, signature):
        return {'error': 'invalid signature'}, 401

    order = json.loads(raw_body)
    # Process order...

Node.js Example:

const crypto = require('crypto');

function verifyWebhook(secret, rawBody, signatureHeader) {
  const expectedSig = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(signatureHeader)
  );
}

app.post('/webhooks/cata/orders', (req, res) => {
  const signature = req.get('X-Cata-Signature');
  const rawBody = req.body; // Middleware must NOT parse JSON first

  if (!verifyWebhook(process.env.WEBHOOK_SECRET, rawBody, signature)) {
    return res.status(401).json({ error: 'invalid signature' });
  }

  const order = JSON.parse(rawBody);
  // Process order...
});

3.3 Webhook Body (Order Payload)

The webhook body is a JSON object with the complete order details:

{
  "uuid": "ord-550e8400-e29b-41d4-a716-446655440000",
  "orderRefNo": "ORD-2024-001",
  "dailyQueueNo": "A12",
  "storeUuid": "store-uuid-123",
  "storeName": "Downtown Branch",
  "deliveryMethod": "DELIVERY",
  "status": "PAID",
  "currency": "AED",
  "isPreorder": false,
  "items": [
    {
      "plu": "SKU-BURGER-01",
      "name": "Classic Burger",
      "quantity": 2,
      "itemOnlyPrice": 35.00,
      "modifierOnlyPrice": 5.00,
      "itemPrice": 40.00,
      "itemSubTotal": 80.00,
      "modifiers": [
        {
          "modifierHeaderId": "mh-cheese",
          "modifierOptionId": "mo-extra-cheese",
          "modifierHeaderName": "Cheese",
          "modifierOptionName": "Extra Cheese",
          "price": 5.00,
          "quantity": 1
        }
      ]
    }
  ],
  "subtotal": 80.00,
  "discountTotal": 0.00,
  "serviceCharge": 0.00,
  "deliveryFee": 5.00,
  "totalPay": 85.00,
  "payment": {
    "amount": 85.00,
    "tip": 5.00,
    "method": "CARD"
  },
  "customer": {
    "uuid": "cust-123",
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+971501234567"
  },
  "delivery": {
    "address": "123 Main St, Dubai",
    "notes": "Leave at door"
  },
  "createdAt": "2024-12-01T14:30:00Z"
}

Key Fields for Order Processing:

Field Type Use
uuid UUID Unique order identifier — use this to track the order in Cata
orderRefNo string Human-readable order reference (for receipts, kitchen display)
dailyQueueNo string Queue number for your KDS
items array Line items with modifiers
totalPay decimal Final amount paid (including fees, discounts)
discountTotal decimal Every reduction on the order — a promotion/coupon and a loyalty-points redemption
discounts array Optional itemised breakdown of discountTotal — {type, name, amount, externalId, pointsUsed}, where type is promotion or points. Read it when your POS records a points redemption separately. Entries sum to discountTotal; if absent, use discountTotal — never apply both as separate reductions
deliveryMethod enum DELIVERY, PICKUP, DINE_IN, TAKEOUT
customer object Customer details for delivery/contact
createdAt ISO-8601 When the order was placed

3.4 Handling Duplicate Deliveries

Deduplicate on uuid from the order body — not on X-Cata-Delivery-ID.

The two headers look interchangeable but are not:

Value Stable across a repeat of the same order? Use it for
uuid (order body) Yes — it is the order's identity, and the same order always carries the same value Idempotency
X-Cata-Delivery-ID (header) No — a fresh value is minted for every delivery attempt Correlating a single attempt in logs when you contact support

Because the delivery ID changes on every attempt, deduplicating on it would let a repeat of the same order through as new. Key your check on uuid.

Two properties matter, and the obvious implementation has neither:

  1. Atomic. A SELECT then INSERT is not enough — two deliveries arriving together can both pass the SELECT and both create a ticket. Let a unique constraint on uuid decide the winner.
  2. Persist before you process. Acknowledge once the order is durably stored, then process asynchronously (see §3.5).
@app.post('/webhooks/cata/orders')
def handle_order_webhook():
    order = json.loads(request.get_data())

    # Atomic insert-or-fetch. `order_uuid` carries a UNIQUE constraint, so
    # concurrent deliveries of the same order cannot both create a ticket —
    # exactly one INSERT wins and the rest come back as `created=False`.
    record, created = ReceivedOrder.objects.get_or_create(
        order_uuid=order['uuid'],
        defaults={'payload': order, 'received_at': now()},
    )

    if not created:
        # Already accepted. Return the original outcome; do not enqueue again.
        return {'status': 'already accepted', 'posOrderId': record.pos_order_id}, 200

    # Durably accepted — acknowledge now, do the kitchen work off the request path.
    enqueue_for_kitchen(record.id)

    return {'status': 'accepted'}, 200

Why you still need this even though Cata does not retry today

Delivery is at-most-once right now (§3.5), so a duplicate is unlikely — but "unlikely" is not "impossible": a request can be replayed by an intermediary, and automatic retry is on our roadmap, at which point repeats become normal. A dedupe key you added up front costs nothing; one you add after a double kitchen ticket costs a customer.

3.5 Webhook Response

You must respond with HTTP 200 OK (or any 2xx status) to confirm receipt:

{
  "status": "accepted",
  "message": "Order received and queued for kitchen"
}

Cata does not parse your response body — only the status code matters.

Delivery is currently at-most-once — you must reconcile

A non-2xx response marks the order as failed and the order is not re-sent. Automatic retry with backoff is in progress; until it ships, do not design for at-least-once delivery.

What this means in practice:

  • Return 2xx as soon as you have durably accepted the order (persisted it), not after you have finished processing it. Acknowledge fast, process asynchronously.
  • Treat endpoint availability as order-critical while this stands.
  • Reconcile: if you detect a gap, contact your Cata integration contact to have the order re-dispatched.

Keep your handler idempotent (see §3.4) either way — when retry ships, delivery becomes at-least-once and repeats become normal rather than exceptional.


4. Update Order Status

As the order moves through your kitchen/fulfillment pipeline, push status updates back to Cata. This allows the customer to track their order in real-time.

4.1 Status Update Endpoint

Endpoint: POST /api/v1/orders/{orderId}/status

URL Parameters: - {orderId}: The order UUID from the webhook (e.g., ord-550e8400-e29b-41d4-a716-446655440000)

Required Headers: - X-Api-Key: your-api-key - Content-Type: application/json

Request Body:

{
  "status": "ACCEPTED",
  "posOrderId": "POS-12345",
  "timestamp": "2024-12-01T14:35:00Z"
}

Field Explanations:

Field Type Required Description
status string Yes The new order status (see 4.2 for valid values)
posOrderId string No Your internal POS order ID (for reconciliation)
timestamp ISO-8601 Yes When the status change occurred (UTC)

4.2 Valid Status Values

The order must follow this state machine. Not all transitions are allowed:

PAID             → ACCEPTED, CANCELLED, COMPLETED
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)

Status Descriptions:

Status Meaning When to Send
ACCEPTED Order received and confirmed Kitchen staff acknowledged the order
IN PROGRESS Order is being prepared Started cooking
READY Order is ready for pickup/delivery Ready at pickup counter or with driver
DRIVER PICKED UP Driver picked up the order (delivery only) Driver confirmed they have the order
COMPLETED Order completed Delivered to customer or picked up
CANCELLED Order cancelled Customer or POS cancelled the order

4.3 Important Notes on Status Updates

  1. Exact Status String Format
  2. Note the literal spaces in "IN PROGRESS" and "DRIVER PICKED UP"
  3. No underscores, no case-folding
  4. Example: "IN PROGRESS" (not "in_progress" or "inProgress")

  5. Cancellation Requires a Reason

  6. When setting status to CANCELLED, include a reason field:

    {
      "status": "CANCELLED",
      "reason": "Out of stock",
      "timestamp": "2024-12-01T14:45:00Z"
    }
    

  7. Idempotency

  8. Replaying the same status twice is safe
  9. First transition: PAID → ACCEPTED at timestamp T1
  10. Replay ACCEPTED at timestamp T1 or T2: no-op, original T1 timestamp preserved
  11. Response will show previousStatus == currentStatus

4.4 Success Response (200 OK)

{
  "orderId": "ord-550e8400-e29b-41d4-a716-446655440000",
  "currentStatus": "ACCEPTED",
  "previousStatus": "PAID",
  "transitionedAt": "2024-12-01T14:35:00Z"
}

4.5 Error Responses

400 Bad Request:

{
  "error": {
    "code": 400,
    "message": "unsupported status: INVALID_STATUS",
    "details": "partners may only push [ACCEPTED, IN PROGRESS, READY, DRIVER PICKED UP, COMPLETED, CANCELLED]"
  }
}

401 Unauthorized:

{
  "error": {
    "code": 401,
    "message": "invalid api key",
    "details": "the provided X-Api-Key is invalid or expired"
  }
}

404 Not Found:

{
  "error": {
    "code": 404,
    "message": "order not found",
    "details": "no dispatched order found for the given orderId in this tenant"
  }
}

409 Conflict (Invalid State Transition):

{
  "error": {
    "code": 409,
    "message": "invalid status transition",
    "details": "cannot change order status from PAID to READY"
  }
}


5. Complete Integration Checklist

Use this checklist to verify your integration is complete:

  • [ ] Authentication
  • [ ] Obtained API key from Cata partner manager
  • [ ] Tested /auth/whoami endpoint successfully
  • [ ] Stored key securely (environment variable, not in code)

  • [ ] Webhook Registration

  • [ ] Called POST /api/v1/webhooks/register once per outlet
  • [ ] Received webhook secret and saved it securely
  • [ ] Webhook callback URL is publicly accessible over HTTPS
  • [ ] Callback URL handles multiple webhooks concurrently
  • [ ] Stored webhookId for future reference (updates, rotation, etc.)

  • [ ] Webhook Receipt & Verification

  • [ ] Callback handler checks X-Cata-Signature before processing
  • [ ] Signature verification uses correct secret and HMAC-SHA256
  • [ ] Callback returns HTTP 200 on successful receipt
  • [ ] Callback returns HTTP 200 on a repeat of an order it already holds (idempotency, keyed on the order uuid — see §3.4)

  • [ ] Order Processing

  • [ ] Parse order JSON from webhook body
  • [ ] Extract uuid as the unique order identifier
  • [ ] Extract line items with modifiers and prices
  • [ ] Create order in your POS system
  • [ ] Handle all delivery methods (DELIVERY, PICKUP, DINE_IN, TAKEOUT)

  • [ ] Status Updates

  • [ ] Push status via POST /api/v1/orders/{orderId}/status as order progresses
  • [ ] Use exact status strings (ACCEPTED, IN PROGRESS, READY, etc.)
  • [ ] Include timestamp in ISO-8601 format
  • [ ] Respect the state machine — not all transitions are valid
  • [ ] Include reason when status is CANCELLED

  • [ ] Error Handling

  • [ ] Retry your outbound calls to Cata (status updates) on 408/429/5xx with bounded backoff; treat other 4xx as terminal
  • [ ] Handle 401 Unauthorized gracefully (re-authenticate, don't retry)
  • [ ] Log webhook delivery IDs and timestamps for debugging
  • [ ] Monitor for missing/stale orders in POS

  • [ ] Testing

  • [ ] Tested end-to-end with a real order in sandbox/staging
  • [ ] Verified order appears in POS with correct items and prices
  • [ ] Verified status updates appear in Cata UI in real-time
  • [ ] Tested webhook signature verification with a known secret
  • [ ] Tested idempotent replay (same order uuid twice) — one ticket, not two
  • [ ] Tested the same order uuid arriving concurrently — the uniqueness check is atomic, not SELECT-then-INSERT

6. Troubleshooting

Webhook not received

  1. Check that callbackUrl is publicly accessible (no firewall/NAT blocking)
  2. Test manually: curl -X POST https://your-pos-api/webhooks/cata/orders
  3. Check firewall rules allow inbound HTTPS from Cata's IP ranges
  4. Verify callback handler logs are being written (enable debug logging)

Signature verification fails

  1. Verify you're using the correct secret (not the webhookId)
  2. Verify you're signing the raw body bytes, not the parsed JSON
  3. Verify the header name is exactly X-Cata-Signature
  4. Verify you're using HMAC-SHA256 (not SHA256, HMAC-MD5, etc.)
  5. Test with the example payloads in the troubleshooting section (below)

Status updates are rejected (400/409)

  1. Verify the status string matches exactly (spaces, case, etc.)
  2. Verify the current order status allows the transition
  3. Verify you're using the uuid from the webhook, not orderRefNo

API key keeps returning 401

  1. Verify the key hasn't expired (check expiresAt from /auth/whoami)
  2. Verify the subdomain matches the tenant (e.g., golden-dragon.sgp1...)
  3. Ask Cata to rotate the key if it's old
  4. Verify header name is exactly X-Api-Key (case-sensitive)

7. Code Examples

Python (Flask)

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
from datetime import datetime

app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_abc123xyz789...'
API_KEY = 'your-api-key'
TENANT_SUBDOMAIN = 'golden-dragon'
BASE_URL = f'https://{TENANT_SUBDOMAIN}.sgp1.samba-technologies.xyz/service/pos-integration/api/v1'

def verify_webhook(secret, raw_body, signature_header):
    expected_sig = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected_sig, signature_header)

@app.post('/webhooks/cata/orders')
def handle_order():
    raw_body = request.get_data()
    signature = request.headers.get('X-Cata-Signature')
    # Attempt-scoped; log it so support can trace one delivery. Not an idempotency key.
    delivery_id = request.headers.get('X-Cata-Delivery-ID')

    if not verify_webhook(WEBHOOK_SECRET, raw_body, signature):
        return {'error': 'invalid signature'}, 401

    order = json.loads(raw_body)
    print(f"Order received: {order['uuid']} (delivery {delivery_id}), items: {len(order['items'])}")

    # Idempotency: key on the order uuid, atomically. See §3.4.
    # TODO: replace with your storage layer's atomic insert-or-fetch.
    record, created = received_orders.get_or_create(order_uuid=order['uuid'], payload=order)
    if not created:
        return {'status': 'already accepted'}, 200

    # TODO: Create order in your POS system, then:
    update_order_status(order['uuid'], 'ACCEPTED')

    return {'status': 'accepted'}, 200

def update_order_status(order_uuid, status):
    """Push order status back to Cata."""
    import requests

    response = requests.post(
        f'{BASE_URL}/orders/{order_uuid}/status',
        headers={'X-Api-Key': API_KEY},
        json={
            'status': status,
            'timestamp': datetime.utcnow().isoformat() + 'Z'
        }
    )

    if response.status_code != 200:
        print(f"Status update failed: {response.status_code} {response.text}")
    else:
        print(f"Status updated to {status}")

if __name__ == '__main__':
    app.run(port=5000)

Node.js (Express)

const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();
app.use(express.raw({ type: 'application/json' })); // Keep body as Buffer

const WEBHOOK_SECRET = 'whsec_abc123xyz789...';
const API_KEY = 'your-api-key';
const BASE_URL = 'https://golden-dragon.sgp1.samba-technologies.xyz/service/pos-integration/api/v1';

function verifyWebhook(secret, rawBody, signatureHeader) {
  const expectedSig = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(signatureHeader)
  );
}

app.post('/webhooks/cata/orders', async (req, res) => {
  const signature = req.get('X-Cata-Signature');
  // Attempt-scoped; log it so support can trace one delivery. Not an idempotency key.
  const deliveryId = req.get('X-Cata-Delivery-ID');

  if (!verifyWebhook(WEBHOOK_SECRET, req.body, signature)) {
    return res.status(401).json({ error: 'invalid signature' });
  }

  const order = JSON.parse(req.body);
  console.log(`Order received: ${order.uuid} (delivery ${deliveryId}), items: ${order.items.length}`);

  // Idempotency: key on the order uuid, atomically. See §3.4.
  // TODO: replace with your storage layer's atomic insert-or-fetch.
  const { created } = await receivedOrders.getOrCreate(order.uuid, order);
  if (!created) {
    return res.json({ status: 'already accepted' });
  }

  // TODO: Create order in your POS system, then:
  updateOrderStatus(order.uuid, 'ACCEPTED');

  res.json({ status: 'accepted' });
});

async function updateOrderStatus(orderId, status) {
  try {
    const response = await axios.post(
      `${BASE_URL}/orders/${orderId}/status`,
      {
        status: status,
        timestamp: new Date().toISOString()
      },
      {
        headers: { 'X-Api-Key': API_KEY }
      }
    );
    console.log(`Status updated to ${status}`);
  } catch (error) {
    console.error(`Status update failed: ${error.response?.status} ${error.response?.data}`);
  }
}

app.listen(3000, () => console.log('Listening on port 3000'));

8. Future Enhancements (TODO)

The following features are planned but not yet implemented:

8.1 TODO: OAuth 2.0 Authentication

When: Future release
What: Alongside the current API key flow, support OAuth 2.0 with scoped permissions

Scopes (proposed): - orders:read — read order details - orders:write — submit/update orders - products:read — read menu/catalog - webhooks:* — manage webhooks

Impact: No migration required for current API-key integrations. Will be purely additive.

8.2 TODO: Additional Webhook Events

When: Future
What: Expand beyond just order.paid to include:

Event Trigger Status
order.paid Order is paid ✅ Live
order.cancelled Order was cancelled 🔄 TODO
menu.updated Menu was published 🔄 TODO
outlet.status_changed Outlet opened/closed 🔄 TODO

8.3 TODO: Bulk Order API

When: Future
What: Accept multiple orders in a single request (useful for back-office integrations)

Endpoint: POST /api/v1/orders/bulk-dispatch
Payload: Array of order payloads

8.4 ✅ Customer Auto-Registration — now built

Status: implemented (this entry was stale; corrected 2026-09-04).

What: For POS systems that require a customer record to exist before an order can be created, Cata pre-registers the customer as part of dispatch rather than failing the order.

Matching is per-provider — some POS match on phone, others on email. If your POS requires pre-existing customers, tell us which field is the match key and whether it must be unique.

There is no longer a manual workaround to apply.

8.5 TODO: Idempotency Headers

When: Future
What: Support Idempotency-Key header on order dispatch for stricter deduplication

Format: Standard UUID or any unique string
Behavior: Replaying with the same key within a 24-hour window returns the same response without re-processing

8.6 TODO: Order Cancellation from POS

When: Future
What: Allow your POS to request order cancellation in Cata (currently Cata can cancel, but POS cannot initiate)

Endpoint: POST /api/v1/orders/{orderId}/cancel-request
Payload: Reason + confirmation


9. Support & Contact

For integration questions or issues:

  1. Check this guide for common problems (section 6)
  2. Check the API reference at https://dev.docs.cata.sg (or the docs URL for your environment)
  3. Contact Cata: integration-support@cata.sg
  4. GitHub Issues: If you find a bug in the API, open an issue with steps to reproduce

Appendix: Example Payloads

Appendix A: Order Webhook Example

Headers:

X-Cata-Event: order.paid
X-Cata-Delivery-ID: evt-550e8400-e29b-41d4-a716-446655440000
X-Cata-Timestamp: 1725112200
X-Cata-Signature: sha256=abc123xyz789...

Body (raw JSON):

{
  "uuid": "ord-550e8400-e29b-41d4-a716-446655440000",
  "orderRefNo": "ORD-2024-001",
  "dailyQueueNo": "A12",
  "storeUuid": "store-uuid-123",
  "storeName": "Golden Dragon - Downtown",
  "deliveryMethod": "DELIVERY",
  "status": "PAID",
  "currency": "AED",
  "isPreorder": false,
  "items": [
    {
      "plu": "SKU-KUNG-PAO",
      "name": "Kung Pao Chicken",
      "quantity": 1,
      "itemOnlyPrice": 42.00,
      "modifierOnlyPrice": 3.00,
      "itemPrice": 45.00,
      "itemSubTotal": 45.00,
      "modifiers": [
        {
          "modifierHeaderId": "mh-spice",
          "modifierOptionId": "mo-spice-medium",
          "modifierHeaderName": "Spice Level",
          "modifierOptionName": "Medium",
          "price": 0.00,
          "quantity": 1
        },
        {
          "modifierHeaderId": "mh-sauce",
          "modifierOptionId": "mo-sauce-extra",
          "modifierHeaderName": "Extra Sauce",
          "modifierOptionName": "Extra",
          "price": 3.00,
          "quantity": 1
        }
      ]
    },
    {
      "plu": "SKU-FRIED-RICE",
      "name": "Fried Rice",
      "quantity": 2,
      "itemOnlyPrice": 18.00,
      "modifierOnlyPrice": 0.00,
      "itemPrice": 18.00,
      "itemSubTotal": 36.00,
      "modifiers": []
    }
  ],
  "subtotal": 81.00,
  "discountTotal": 5.00,
  "serviceCharge": 0.00,
  "deliveryFee": 15.00,
  "totalPay": 91.00,
  "payment": {
    "amount": 91.00,
    "tip": 0.00,
    "method": "CARD"
  },
  "customer": {
    "uuid": "cust-550e8400",
    "name": "Ahmed Al Mansouri",
    "email": "ahmed@example.com",
    "phone": "+971501234567"
  },
  "delivery": {
    "address": "123 Sheikh Zayed Road, Dubai, UAE",
    "notes": "Call on arrival, building entrance on left"
  },
  "createdAt": "2024-09-01T14:30:00Z"
}

Appendix B: Webhook Registration Example

Request:

curl -X POST "https://golden-dragon.sgp1.samba-technologies.xyz/service/pos-integration/api/v1/webhooks/register" \
  -H "X-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "outletId": "store-uuid-123",
    "provider": "your-pos-system",
    "callbackUrl": "https://api.your-pos.example.com/webhooks/cata/orders",
    "events": ["order.paid"]
  }'

Response (201):

{
  "code": 201,
  "isSuccess": true,
  "message": "webhook registered",
  "webhook": {
    "id": 42,
    "outletId": "store-uuid-123",
    "provider": "your-pos-system",
    "callbackUrl": "https://api.your-pos.example.com/webhooks/cata/orders",
    "events": ["order.paid"],
    "secret": "whsec_0123456789abcdef",
    "isActive": true,
    "createdAt": "2024-09-01T10:00:00Z"
  }
}


Last updated: September 1, 2024
Version: 1.0.0