README docs KDS_API.md

KDS API — kitchen display orders

Kitchen Display System (KDS) endpoints for listing active orders and updating acceptance status. Responses use the same per-invoice object shape and datatypes as POST /v1/invoices/sync / GET /v1/invoices/history, defined in src/validations/invoice.validation.js (syncInvoicePayloadSchema), plus one KDS-only field: order_type_icon.

Authentication: jwtAuth — valid JWT in Authorization: Bearer <token>. Tenant scope (customer_id, location_id) is taken from the token, not from query parameters.

Validation: src/validations/kds.validation.js (request + response contract schemas for frontend alignment).

See also:


Routes

Method Path Handler
GET /v1/kds-orders List kitchen orders for the current business day
PUT /v1/kds-orders/:uuid Update order (rebump, done_at, line items)

Legacy / gateway paths (for migration reference):

Legacy / external Invoice service
GET /kds-orders GET /v1/kds-orders
PUT /kds-order-status PUT /v1/kds-orders/:uuid
PUT /api/v2/kds-orders/:uuid PUT /v1/kds-orders/:uuid (via gateway)

Authentication

Authorization: Bearer <jwt>
Claim Aliases Required
customer_id customerId yes
location_id locationId yes

Missing claims → 400 Authentication must include customer_id and location_id.

All data is scoped to the token’s customer_id + location_id.


GET /v1/kds-orders

Returns a plain JSON array of invoice objects (no pagination wrapper). Empty array when no orders match.

Implementation: kdsController.getKdsOrderskdsService.getKdsOrdersInvoiceRepository.getKdsOrdersmapInvoiceToSyncPayloadShape.

Query parameters

Param Type Default Description
brand_id number Filter by invoices.brand_id
c string Product category filter (products.type). Omit or All = no filter
is_order_rebump 0 | 1 0 1 = return done orders (done_at IS NOT NULL); 0 = active queue (done_at IS NULL)
app_version string Client version; affects items[].qty numeric type in response
build_number number | string Used with app_version for qty typing
icon_pixel_size number 24 1–512; width/height for order_type_icon URL
icon_format string png png, jpg, jpeg, webp, gif
icon_url_format string Alias of icon_format
limit number 10000 Max orders (capped at 10000)

Invalid query values → 400 with Joi validation message.

Selection rules

Orders included when all of the following hold:

  • customer_id + location_id match JWT
  • created_at within the current business day (from opening_hour until next day’s opening_hour via APP_URL business-hours API)
  • status not in canceled, cancel, cancelled
  • done_at IS NULL (unless is_order_rebump=1)
  • Line items: only is_deleted = 0 rows are loaded
  • Sorted by order_no ascending

Example request

GET /v1/kds-orders?brand_id=1&is_order_rebump=0&icon_pixel_size=32&icon_format=png
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response

HTTP status: 200 OK

Body: kdsOrdersListResponseSchema — array of kdsOrderResponseSchema.

[
  {
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "status": "open",
    "customer_id": 1,
    "location_id": 2,
    "order_no": 42,
    "customer_invoice_id": 1001,
    "created_at": 1718534400,
    "updated_at": 1718534500,
    "paid_at": null,
    "done_at": null,
    "is_accepted": 0,
    "order_type": "Dine In",
    "order_type_icon_key": "mdi:silverware-fork-knife",
    "order_type_icon": "https://api.iconify.design/mdi/silverware-fork-knife.png?width=32&height=32",
    "payments": [
      { "payment_method": "cash", "amount": 50, "tip_amount": 0 }
    ],
    "items": [
      {
        "name": "Burger",
        "product_id": 10,
        "qty": 2,
        "actual_price": 25,
        "vat_pct": 5,
        "is_prepared": 0,
        "modifiers": [],
        "addons": null
      }
    ]
    // … remaining sync payload fields — see INVOICE_REQUEST.md
  }
]

Response datatype rules

Aligned with syncInvoicePayloadSchema / INVOICE_HISTORY.md:

Concern Rule
Timestamps (created_at, updated_at, paid_at, done_at, item timestamps) Unix seconds (number)
Boolean flags on invoice/items 0 / 1 numbers
is_accepted on invoice 0 / 1 / 2 (0 pending, 1 accepted, 2 rejected)
payments Array of { payment_method, amount, tip_amount } or null
items Parent lines only; addons nested; modifiers flat array
notes String array parsed from DB order_notes (legacy iOS / invoiceSyncV3 key)
order_type_icon KDS-only — raster URL string or null

PUT /v1/kds-orders/:uuid

Updates a tenant-scoped kitchen order: done_at / rebump and per-line is_prepared resets. Publishes Ably when the invoice row changes (ABLY_PUBLISH_KEY).

Implementation: kdsController.updateKdsOrderStatuskdsService.updateKdsOrderStatusInvoiceRepository.updateKdsOrderFromKds + InvoiceItemsRepositories.updatePreparedByIdentifiersably.service.publishOrderStatus.

Path parameters

Param Type Required Description
uuid string yes Invoice UUID

Request body

Field Type Required Description
is_rebump 0 | 1 no 1 clears invoice done_at (order back in active queue); ignores body done_at
done_at number no Unix seconds — sets invoice done_at when is_rebump is not 1
is_accepted 0 | 1 | 2 no Kitchen acceptance; 1/2 also publish Grubtech accept/reject via RabbitMQ
identifiers object[] no Each entry: { identifier, is_prepared } — sets invoice_items.is_prepared on matching rows (0 or 1)

All body fields are optional; send only what you need to change.

Behaviour

Input Effect
is_rebump: 1 invoices.done_at = null
done_at (and not rebump) invoices.done_at set from Unix seconds + Grubtech prepared
is_accepted: 1 / 2 Persists acceptance + Grubtech accept / reject
identifiers[] For each entry, matching rows get invoice_items.is_prepared set to the given 0 or 1 for this invoice + tenant

Duplicate identifier values in the array: the last entry wins. Empty identifier strings are ignored.

Example request

PUT /v1/kds-orders/13A2C02A-C989-4C1B-B9E0-E14A6EB36867
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "is_rebump": 1,
  "done_at": 172839103,
  "identifiers": [
    { "identifier": "1234567890", "is_prepared": 0 },
    { "identifier": "1234567890", "is_prepared": 1 }
  ]
}

Gateway equivalent:

curl --location --request PUT 'https://app.syspos.ae/api/v2/kds-orders/13A2C02A-C989-4C1B-B9E0-E14A6EB36867A1' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer <token>' \
  --data '{
    "is_rebump": 1,
    "done_at": 172839103,
    "identifiers": [
      { "identifier": "1234567890", "is_prepared": 0 }
    ]
  }'

When is_rebump is 1, done_at in the body is not applied; invoice done_at is cleared instead.

Response

HTTP status: 200 OK

Body: kdsOrderStatusUpdateResponseSchema

{
  "status": true,
  "message": "Kds Order 1001 rebumped"
}

Example messages (all responses use "status": true):

Condition message
is_rebump: 1 Kds Order {order_no} rebumped
Items reset via identifiers Kds Order {order_no} items updated
Other invoice update Kds Order {order_no} updated
UUID not found for tenant No invoice found against this uuid
No customer_invoice_id yet Order rebumped. / Order updated. (no order number)

Side effects

  1. invoices.done_at cleared when is_rebump === 1, or set from done_at otherwise
  2. Optional is_accepted (0/1/2) persisted when provided
  3. invoice_items.is_prepared updated for each matched identifiers entry
  4. invoices.updated_at set to server time when the invoice row changes
  5. Ably publish on channel order:{uuid}, event status, when the invoice row changes
  6. RabbitMQ grubtech.status (best-effort): accept/reject when is_accepted is 1/2; prepared when done_at is set

Ably realtime — order status

Subscribe to channel order:{invoiceUuid}, event status.

Payload: kdsAblyStatusPayloadSchema

{
  "status": "preparing",
  "is_accepted": 1,
  "done_at": null,
  "updated_at": "2026-06-16T12:34:56.789Z"
}

Derived status field

Condition status
is_accepted === 2 cancelled
done_at set ready
is_accepted === 1 preparing
otherwise pending

Environment: set ABLY_PUBLISH_KEY in .env. If unset, DB update still succeeds; publish is skipped with a warning log.


Validation schemas (frontend contract)

Exported from src/validations/kds.validation.js:

Export Purpose
getKdsOrders Request validator for GET /v1/kds-orders query
updateKdsOrderStatus Request validator for PUT /v1/kds-orders/:uuid
kdsOrdersListResponseSchema Full list response body
kdsOrderResponseSchema Single order (syncInvoicePayloadSchema + order_type_icon)
kdsOrderStatusUpdateResponseSchema Status update response
kdsAblyStatusPayloadSchema Ably status event payload
kdsAcceptanceStatusDb 0 | 1 | 2 (persisted is_accepted / Ably)
kdsRealtimeStatus pending | preparing | ready | cancelled

Base invoice read shape: syncInvoicePayloadSchema in src/validations/invoice.validation.js.


Related code

Piece Path
Routes src/routes/v1/kds.route.js
Controller src/controllers/kds.controller.js
Service src/services/kds.service.js
Repository src/data/repositories/InvoiceRepository.jsgetKdsOrders, updateKdsOrderFromKds
Item repository src/data/repositories/InvoiceItemsRepositories.jsupdatePreparedByIdentifiers
Response mapper src/utils/invoice-sync-response-mapper.jsmapInvoiceToSyncPayloadShape
KDS helpers src/utils/kdsResponseHelpers.js → acceptance status mapping, order_type_icon, qty typing
Ably src/services/ably.service.js
Validation src/validations/kds.validation.js

Errors

Case HTTP Body
Invalid / missing JWT 401 Standard auth error
Missing customer_id / location_id in token 400 Authentication must include customer_id and location_id
Invalid query or body (Joi) 400 Validation message from error handler
Success — list 200 [] or array of orders
Success — status update 200 { status: true, message }