Introduction
The Delivery Gateway sits between your system and logistics providers (e.g. Jalat). You submit orders to one API and receive status updates via webhooks — without dealing with each provider's protocol directly.
Base URL
http://localhost:6060Response shape
Every response — success or failure — follows this envelope.
{
"success": true,
"data": { ... }
}{
"success": false,
"message": "Description"
}Health check
curl http://localhost:6060/health{
"status": "ok",
"timestamp": "2026-04-28T08:00:00.000Z"
}Authentication
Order and parcel endpoints require a JWT bearer token. To get one, sign a request with your apiSecret and exchange it at the token endpoint.
HMAC signing
The canonical string is the Unix timestamp concatenated with the raw JSON body, signed with your apiSecret.
signature = HMAC-SHA256(timestamp + JSON.stringify(body), apiSecret)
→ base64API_KEY="your_api_key"
API_SECRET="your_api_secret"
TIMESTAMP=$(date +%s)
BODY='{}'
CANONICAL="${TIMESTAMP}${BODY}"
SIGNATURE=$(echo -n "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
echo "X-TIMESTAMP: $TIMESTAMP"
echo "X-SIGNATURE: $SIGNATURE"Clock skew
401 Request timeout./api/v1/auth/tokenRequired headers
X-API-KEYstringrequired | Your API key from the admin. |
X-TIMESTAMPstringrequired | Unix seconds. Rejected if more than ±5 min from server time. |
X-SIGNATUREstringrequired | Base64 HMAC-SHA256 of timestamp + body. |
Request
API_KEY="your_api_key"
API_SECRET="your_api_secret"
TIMESTAMP=$(date +%s)
BODY='{}'
SIGNATURE=$(echo -n "${TIMESTAMP}${BODY}" | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)
curl -X POST http://localhost:6060/api/v1/auth/token \
-H "Content-Type: application/json" \
-H "X-API-KEY: $API_KEY" \
-H "X-TIMESTAMP: $TIMESTAMP" \
-H "X-SIGNATURE: $SIGNATURE" \
-d '{}'Response 200
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": "1h"
}
}Orders
Submit one order with one or more parcels. The gateway forwards it to the provider you specify and returns the unified order shape.
/api/v1/ordersBody fields
clientOrderIdstringrequired | Your unique ID. Returned with every webhook so you can correlate. |
providerstringrequired | Provider slug, e.g. jalat. |
remarkstringoptional | Note for the order, max 255 chars. |
parcelsParcel[]required | At least one parcel. |
Parcel fields
clientParcelIdstringrequired | Your unique ID for this parcel. Used in webhook callbacks. |
addressstringrequired | Delivery address. |
recipientPhonestringrequired | Recipient phone number. |
recipientNamestringoptional | Recipient name. |
pricenumberoptional | COD amount. |
latitudenumberoptional | GPS latitude. |
longitudenumberoptional | GPS longitude. |
remarkstringoptional | Note to driver, max 255 chars. |
Request
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -X POST http://localhost:6060/api/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"clientOrderId": "ORD-2026-001",
"provider": "jalat",
"remark": "Handle with care",
"parcels": [
{
"clientParcelId": "PCL-001",
"address": "No. 10, Street 271, Phnom Penh",
"recipientPhone": "012345678",
"recipientName": "Dara Sok",
"price": 15.50,
"latitude": 11.5564,
"longitude": 104.9282,
"remark": "Call before delivery"
},
{
"clientParcelId": "PCL-002",
"address": "No. 25, Street 63, Phnom Penh",
"recipientPhone": "097654321",
"recipientName": "Sina Chan"
}
]
}'Response 201
{
"success": true,
"data": {
"_id": "664a1b2c3d4e5f6a7b8c9d0e",
"clientId": "663f...",
"clientOrderId": "ORD-2026-001",
"provider": "jalat",
"status": "PENDING",
"remark": "Handle with care",
"parcels": [
{
"clientParcelId": "PCL-001",
"providerParcelId": "JAL-99001",
"address": "No. 10, Street 271, Phnom Penh",
"recipientPhone": "012345678",
"recipientName": "Dara Sok",
"price": 15.5,
"status": "PENDING"
}
],
"createdAt": "2026-04-28T08:00:00.000Z"
}
}/api/v1/orders/:clientOrderIdRequest
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl http://localhost:6060/api/v1/orders/ORD-2026-001 \
-H "Authorization: Bearer $TOKEN"Response 200
{
"success": true,
"data": {
"_id": "664a1b2c3d4e5f6a7b8c9d0e",
"clientOrderId": "ORD-2026-001",
"provider": "jalat",
"status": "ON_DELIVERY",
"parcels": [
{
"clientParcelId": "PCL-001",
"providerParcelId": "JAL-99001",
"status": "ON_DELIVERY",
"trackingLink": "https://track.jalat.com/JAL-99001",
"driver": {
"id": "D-42",
"name": "Vuth Kim",
"phone": "011111111"
}
}
]
}
}Parcels
Look up a single parcel without loading the whole order. Useful for tracking pages.
/api/v1/parcels/:clientParcelIdRequest
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl http://localhost:6060/api/v1/parcels/PCL-001 \
-H "Authorization: Bearer $TOKEN"Response 200
{
"success": true,
"data": {
"clientParcelId": "PCL-001",
"providerParcelId": "JAL-99001",
"address": "No. 10, Street 271, Phnom Penh",
"recipientPhone": "012345678",
"recipientName": "Dara Sok",
"status": "DELIVERED",
"trackingLink": "https://track.jalat.com/JAL-99001",
"driver": {
"id": "D-42",
"name": "Vuth Kim",
"phone": "011111111"
}
}
}Webhooks
Providers post status updates to this endpoint. Each request must be signed with the provider's HMAC scheme — unsigned or invalid requests return 401.
/api/v1/webhooks/:providerSlugRequired headers
X-TIMESTAMPstringrequired | Unix seconds; rejected if more than ±5 min from now. |
X-SIGNATUREstringrequired | Base64 HMAC-SHA256 over timestamp + rawBody. |
Allowed status values
PENDINGPICKUP_ASSIGNEDPICKED_UPON_DELIVERYDELIVEREDDELIVERY_FAILEDJalat payload fields
parcelIdstringrequired | Jalat's internal parcel ID. |
partnerParcelIdstringrequired | Your clientParcelId. |
addressstringrequired | Delivery address. |
recipientPhonestringrequired | Recipient phone. |
statusParcelStatusrequired | Current parcel status. |
trackingLinkstringoptional | Public tracking URL. |
driver{ id, name, phone }optional | Assigned driver. |
reasonstringoptional | Failure reason if status is failed. |
latitude / longitudenumberoptional | GPS coordinates. |
Request
PROVIDER_SECRET="jalat_api_secret_from_provider_config"
BODY='{"parcelId":"JAL-99001","partnerParcelId":"PCL-001","address":"No. 10","recipientPhone":"012345678","status":"DELIVERED","createdAt":"2026-04-28T07:00:00Z","updatedAt":"2026-04-28T08:30:00Z"}'
TS=$(date +%s)
SIG=$(echo -n "${TS}${BODY}" | openssl dgst -sha256 -hmac "$PROVIDER_SECRET" -binary | base64)
curl -X POST http://localhost:6060/api/v1/webhooks/jalat \
-H "Content-Type: application/json" \
-H "X-TIMESTAMP: $TS" \
-H "X-SIGNATURE: $SIG" \
-d "$BODY"Response codes
200Acceptedoptional | Processing happens async. Body is null. |
400Bad requestoptional | Body isn't JSON or fails schema (e.g. unknown status). |
401Unauthorizedoptional | Missing/invalid X-TIMESTAMP or X-SIGNATURE, or timestamp expired. |
404Not foundoptional | Provider slug not registered or inactive. |
Admin
Internal endpoints for managing client accounts. Every request requires the static admin key in the X-ADMIN-KEY header.
Keep this key server-side
ADMIN_API_KEY (min 32 chars). It must never be embedded in browser or mobile builds. The dashboard proxies every call from a server action./api/v1/admin/clientsBody fields
namestringrequired | Human-readable client name. |
emailstringrequired | Unique identifier (not used for sending). |
webhookUrlstringoptional | Where to forward provider status events. |
webhookSecretstringoptional | Used to sign outbound webhook payloads. |
Request
ADMIN_KEY="your_admin_key"
curl -X POST http://localhost:6060/api/v1/admin/clients \
-H "Content-Type: application/json" \
-H "X-ADMIN-KEY: $ADMIN_KEY" \
-d '{
"name": "System A",
"email": "system-a@example.com",
"webhookUrl": "https://system-a.example.com/delivery/callback",
"webhookSecret": "optional-outbound-signing-secret"
}'Response 201
{
"success": true,
"data": {
"id": "664a1b2c3d4e5f6a7b8c9d0e",
"name": "System A",
"email": "system-a@example.com",
"status": "active",
"apiKey": "3d1dae7d-1234-4abc-9def-abcdef012345",
"apiSecret": "7f8e21bc-aaaa-bbbb-cccc-ddddeeeeffff",
"webhookUrl": "https://system-a.example.com/delivery/callback",
"createdAt": "2026-04-28T08:00:00.000Z",
"updatedAt": "2026-04-28T08:00:00.000Z"
}
}/api/v1/admin/clientsRequest
curl http://localhost:6060/api/v1/admin/clients \
-H "X-ADMIN-KEY: $ADMIN_KEY"Response 200
{
"success": true,
"data": [
{
"id": "664a1b2c3d4e5f6a7b8c9d0e",
"name": "System A",
"email": "system-a@example.com",
"status": "active",
"apiKey": "3d1dae7d-1234-4abc-9def-abcdef012345",
"webhookUrl": "https://system-a.example.com/delivery/callback",
"createdAt": "2026-04-28T08:00:00.000Z",
"updatedAt": "2026-04-28T08:00:00.000Z"
}
]
}/api/v1/admin/clients/:idBody fields (all optional)
namestringoptional | Display name. |
webhookUrlstringoptional | Delivery callback URL. |
webhookSecretstringoptional | Outbound signing secret. |
status"active" | "suspended"optional | Suspending a client immediately revokes their tokens. |
Request
curl -X PATCH http://localhost:6060/api/v1/admin/clients/664a1b2c3d4e5f6a7b8c9d0e \
-H "Content-Type: application/json" \
-H "X-ADMIN-KEY: $ADMIN_KEY" \
-d '{
"name": "System A v2",
"webhookUrl": "https://new-url.example.com/callback",
"status": "suspended"
}'/api/v1/admin/clients/:id/rotateRequest
curl -X POST http://localhost:6060/api/v1/admin/clients/664a1b2c3d4e5f6a7b8c9d0e/rotate \
-H "X-ADMIN-KEY: $ADMIN_KEY"Response 200
{
"success": true,
"data": {
"id": "664a1b2c3d4e5f6a7b8c9d0e",
"apiKey": "3d1dae7d-1234-4abc-9def-abcdef012345",
"apiSecret": "new-uuid-secret-here"
}
}Errors
All error responses follow the same envelope.
{
"success": false,
"message": "Description of the error"
}400Bad requestoptional | Validation failed or invalid JSON. |
401Unauthorizedoptional | Missing, invalid, or expired token / bad signature. |
404Not foundoptional | Resource (order, parcel, client) not found. |
409Conflictoptional | Duplicate identifier (e.g. clientOrderId or email already used). |
429Too many requestsoptional | Rate limit exceeded — auth attempts are 10/min per API key. |
500Internaloptional | Unexpected server error. |