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:
- INVOICE_REQUEST.md — full invoice / line-item field reference
- INVOICE_HISTORY.md — same read shape, paginated history endpoint
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.getKdsOrders → kdsService.getKdsOrders → InvoiceRepository.getKdsOrders → mapInvoiceToSyncPayloadShape.
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_idmatch JWTcreated_atwithin the current business day (fromopening_houruntil next day’sopening_hourviaAPP_URLbusiness-hours API)statusnot incanceled,cancel,cancelleddone_at IS NULL(unlessis_order_rebump=1)- Line items: only
is_deleted = 0rows are loaded - Sorted by
order_noascending
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.updateKdsOrderStatus → kdsService.updateKdsOrderStatus → InvoiceRepository.updateKdsOrderFromKds + InvoiceItemsRepositories.updatePreparedByIdentifiers → ably.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
invoices.done_atcleared whenis_rebump === 1, or set fromdone_atotherwise- Optional
is_accepted(0/1/2) persisted when provided invoice_items.is_preparedupdated for each matchedidentifiersentryinvoices.updated_atset to server time when the invoice row changes- Ably publish on channel
order:{uuid}, eventstatus, when the invoice row changes - RabbitMQ
grubtech.status(best-effort):accept/rejectwhenis_acceptedis 1/2;preparedwhendone_atis 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.js → getKdsOrders, updateKdsOrderFromKds |
| Item repository | src/data/repositories/InvoiceItemsRepositories.js → updatePreparedByIdentifiers |
| Response mapper | src/utils/invoice-sync-response-mapper.js → mapInvoiceToSyncPayloadShape |
| 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 } |