Deliverect — Adapter Guide¶
Which integration is this? This is the consolidated knowledge pack for Deliverect in the new model (see ADR 0004); it absorbs the former
expert-deliverectrepo, now frozen. The field maps and behaviour below are grounded in the productionkds-management-service/service/DeliverectService/integration (the source of truth). Inpos-integration-serviceDeliverect currently has a Go adapter underinternal/adapters/deliverect/; the JS adapter scripts underscripts/adapters/deliverect/are not yet built — this pack is the reference for generating them.
Provider Info¶
| Field | Value |
|---|---|
| Slug | deliverect |
| Auth Type | oauth (OAuth 2.0 client credentials) |
| Base URL | Per-tenant: staging https://api.staging.deliverect.com, prod https://api.deliverect.com |
| API Docs | https://developers.deliverect.com/ |
| Model | Push — Deliverect pushes menus/orders to us via webhooks; we push orders into Deliverect via outbound API. We are a Channel partner. |
| Sample fixtures | samples/deliverect/ — see sync_products_input.json |
Authentication¶
Outbound (Us → Deliverect): OAuth 2.0 client credentials.
POST {baseUrl}/oauth/token
{ "client_id": "...", "client_secret": "...",
"audience": "{baseUrl}/oauth/token", "grant_type": "client_credentials" }
→ use Authorization: Bearer {access_token} on all outbound calls
Token is cached ~60 min and re-fetched on miss (client credentials has no refresh
token — just request a new one). In the new model, client_id / client_secret
come from the provider connection (apiKey / apiSecret or config), and the
script exchanges them for the bearer token.
Inbound (Deliverect → Us): HMAC-SHA256 webhook verification.
- Header
x-server-authorization-hmac-sha256, value =HMAC-SHA256(webhookSecret, rawBody). - Inbound webhooks also carry
X-Tenant-Idandx-sub-domain. - HMAC is verified in Go, not in the JS sandbox.
Credential fields (all required)¶
base_url, client_id, client_secret, account_id, webhook_secret,
channel_name. (channelLinkId — the Deliverect↔Cata channel link — lives in
outlet_providers.settings.)
Integration Architecture¶
Inbound webhooks (Deliverect → Us):
| Webhook | Path | Topic |
|---|---|---|
| Store registration | POST /v1/delphinus/store |
sync_outlet |
| Menu sync | POST /v1/delphinus/menu |
sync_products (async) |
| Product snooze | POST /v1/delphinus/product/snooze |
snooze_items |
| Busy mode | POST /v1/delphinus/store/status |
(busy mode) |
| Order status | POST /v1/delphinus/order |
order_status_update |
Outbound API (Us → Deliverect):
| Call | Method & Path |
|---|---|
| Get token | POST {baseUrl}/oauth/token |
| Create order | POST {baseUrl}/{channelName}/order/{channelLinkId} |
| Cancel order | POST {baseUrl}/{channelName}/order/{channelLinkId} (status 100) |
| Courier update | POST {baseUrl}/{channelName}/courierUpdate/{channelLinkId} |
| Get locations | GET {baseUrl}/locations?where={"account":"{accountId}"}&max_results=500 |
Data Format¶
| Type | Rule | Example |
|---|---|---|
| Prices (inbound) | integer cents → divide by 100 | 1500 → 15.00 |
| Prices (outbound) | × 100, double-rounded | 15.50 → 1550 |
| Tax rates | integer → divide by 1000 | 10000 → 10.0% |
| Timestamps | ISO-8601 or YYYY-MM-DD HH:MM:SS |
— |
| Content-Type | application/json |
— |
Outbound rounding: round(round(amount*100)/100 * 100) (avoids float drift).
Tax sentinel: a tax of 0/negative is stored as -1 = "use default tax".
Quirks & Constraints¶
| Quirk | Detail |
|---|---|
| Prices are cents | Divide by 100 inbound, multiply ×100 outbound. (Contrast: Revel is decimal.) |
| Async menu processing | The menu webhook stores the raw payload and queues it for async processing to dodge the 30s webhook timeout. |
| Bundle detection | isCombo=true or isVariant=true → treated as a bundle; bundle items resolve from both the Modifiers and Products maps. |
| Upsell skipped | Modifier groups with isUpsell=true are skipped entirely. |
| Description truncation | Item descriptions truncated to 245 runes (multibyte-safe). |
| Modifier input type | SINGLE when min=1,max=1; else MULTIPLE. AllowMultipleQty when max>1 && multiMax>=max. |
| Translations | Names/descriptions resolved against the tenant DEFAULT_LANGUAGE via nameTranslations/descriptionTranslations. |
| Item dedup | Items keyed {storeId}-{itemCode}; availability flags merged (OR) across menus. |
| Cancel reuses create endpoint | Cancel = create endpoint with status=100, reason "reject_by_outlet". |
| Delivery finalize = serve | For DELIVERY, status 90 maps to ServeOrder (driver still delivering), not complete. |
| Location coordinates | GeoJSON: coordinates[0]=longitude, [1]=latitude. |
Mappings reference¶
These maps come from the production kds integration; the JS adapter should emit the Cata Standard API shapes.
Topic: test_connection¶
Verify credentials by obtaining a token and/or calling GET /locations.
Topic: list_remote_outlets¶
GET {baseUrl}/locations?where={"account":"{accountId}"}&max_results=500 →
return { id: location._id, name: address.restaurantName, address: address.street }.
Topic: sync_outlet¶
Inbound store-registration webhook (POST /v1/delphinus/store). Our registration
response returns the other webhook URLs (statusUpdateURL, menuUpdateURL,
snoozeUnsnoozeURL, busyModeURL).
| Cata field | Source |
|---|---|
externalId / locationId |
location._id |
name |
address.restaurantName |
address.line1 / city / state / country / postalCode |
address.street / city / stateOrProvince / country / postalCode |
lon / lat |
address.coordinates.coordinates[0] / [1] (GeoJSON) |
timezone |
location.timezone |
openingHours |
location.openingHours[] → {"monday":["09:00-22:00"]} (dayOfWeek 1=Mon…7=Sun) |
contact |
location.contact.phoneNumber |
channelLinkId |
request.channelLinkId |
Samples: sync_outlet_input.json →
sync_outlet_output.json.
Topic: sync_products¶
Inbound menu webhook (POST /v1/delphinus/menu), processed async. Flatten the
Deliverect menu tree to Cata products.
Product:
| Cata field | Source | Transform |
|---|---|---|
itemCode |
product.plu |
primary identifier |
name / description |
product.name / description |
via getTranslation(); description truncated 245 runes |
basePrice |
product.price |
/ 100 |
visible |
— | always true |
pickupAvailable / deliveryAvailable / eatInAvailable |
menuType |
0=both, 1=delivery, 2=pickup, 3=eat-in |
taxOverride / …Delivery / …EatIn |
takeawayTax / deliveryTax / eatInTax |
/ 1000 (or -1) |
isBundle |
isCombo / isVariant |
true if either (subject to internal-bundle overrides) |
posCategory |
parent category.name |
— |
Modifier group: name via translation; inputType SINGLE(min=1,max=1)/MULTIPLE;
minSelect/maxSelect from min/max (or -1); itemCode from modifierGroup.plu.
Modifier option: itemCode from modifier.plu; additionalPrice = price/100;
asDefault from defaultQuantity; tax /1000.
Bundle section (isCombo): itemCode/name/min/max from the modifier group;
bundle item: itemCode + price (surcharge/100) from the Modifiers/Products map.
Samples: sync_products_input.json →
sync_products_output.json.
Topic: order_dispatch¶
POST {baseUrl}/{channelName}/order/{channelLinkId} with a bearer token.
| Deliverect field | Cata source | Transform |
|---|---|---|
channelOrderId |
order.uuid |
direct |
channelOrderDisplayId |
order.dailyQueueNo |
direct |
orderType |
order.deliveryMethod |
PICKUP→1, DELIVERY→2, EATIN→3 |
orderIsAlreadyPaid |
order.paidAt |
true when paid |
items[].plu/name/quantity/remark |
itemCode/itemName/qty/notes |
direct |
items[].price |
itemOnlyPrice |
× 100 |
items[].subItems[] |
modifiers[] |
plu/name direct, price ×100, qty default 1 |
| bundle children | bundleItem.surcharge |
nested subItems, price ×100, qty 1 |
payment.amount |
order.totalPay |
× 100; payment.type 0 (CC online) |
deliveryCost |
deliveryFee - deliveryFeeDiscount |
×100 |
discountTotal |
discountItem + pointsToDiscount |
negative, ×100 |
bagFee / tip |
packaging fees / tipsAmount |
×100 |
serviceCharge |
(totalPay - subTotal - deliveryFee - bagFee - tips + discountTotal) |
×100 |
Customer/delivery address are set for DELIVERY orders (recipient name, phone,
street, lat/long). Samples:
order_dispatch_input.json →
order_dispatch_output.json.
Topic: order_status_update¶
Inbound POST /v1/delphinus/order. Look up the Cata order by channelOrderId.
Deliverect status codes → Cata¶
| Code | Meaning | Cata action/status |
|---|---|---|
| 10 | New | (received) |
| 20 | Accepted | ACCEPTED (also submit delivery for DELIVERY) |
| 40 | Printed | — |
| 50 | Preparing | IN PROGRESS |
| 60 | Prepared | — |
| 70 | Pickup ready | READY |
| 90 | Finalized | COMPLETED (non-delivery); READY/serve for DELIVERY |
| 95 | Auto-finalized | same as 90 |
| 100 | Should-cancel | CANCELLED |
| 110 | Cancelled | CANCELLED (reason reject_by_outlet) |
| 120 | Failed | CANCELLED |
Samples: order_status_webhook.json →
order_status_output.json.
Optional topics¶
snooze_items— inboundPOST /v1/delphinus/product/snooze:operations[0].data.items[].plu+snoozeEnd/snoozeStart, keyed to store bychannelLinkId.- Busy mode — inbound
POST /v1/delphinus/store/status(statuse.g.busy/available). Not a standard Cata topic. - Cancel order / courier update — outbound, reuse the create/courier endpoints.
Changelog¶
2026-06-09 — Consolidated from expert-deliverect¶
Imported the Deliverect knowledge pack (auth, architecture, data format, status
codes, per-topic field maps, samples) into pos-integration-service. Corrected
this guide's auth from a flat api_key/Bearer sketch to the real OAuth 2.0
client-credentials flow, fixed the order-create endpoint
(/{channelName}/order/{channelLinkId}, not /v1/orders), and the status codes
(10/20/40…120, not 1–5). Cents handling confirmed correct for Deliverect.
Verification log¶
| Topic | Last verified | Environment | Notes |
|---|---|---|---|
| sync_outlet | kds prod | service/DeliverectService |
store registration + GET /locations |
| sync_products | kds prod | service/DeliverectService |
async menu webhook, 3 product models |
| order_dispatch | kds prod | service/DeliverectService |
create order, cents ×100 |
| order_status_update | kds prod | service/DeliverectService |
status codes 10–120 |
Open uncertainties (for the JS adapter)¶
- Exact Cata Standard API field names vs the kds internal model used above.
- Whether
account_id/channel_namelive inprovider_connections.configoroutlet_providers.settingsin the new model. - Courier-update / busy-mode: in scope for the JS adapter or Go-only?