API Reference

Integrate with the Delivery Gateway. Authenticate with HMAC, submit orders, and receive real-time webhook events.

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

URL
http://localhost:6060

Response shape

Every response — success or failure — follows this envelope.

Success
{
  "success": true,
  "data": { ... }
}
Error
{
  "success": false,
  "message": "Description"
}

Health check

BASH
curl http://localhost:6060/health
JSON
{
  "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.

Formula
signature = HMAC-SHA256(timestamp + JSON.stringify(body), apiSecret)
            → base64
Generate signature
API_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

The timestamp must be within ±5 minutes of server time. Sync the client clock with NTP if you see 401 Request timeout.
Exchange credentials for a token
POST/api/v1/auth/token
Returns a JWT valid for 1 hour. Use it on every order or parcel request as Authorization: Bearer <token>.

Required headers

X-API-KEY
stringrequired
Your API key from the admin.
X-TIMESTAMP
stringrequired
Unix seconds. Rejected if more than ±5 min from server time.
X-SIGNATURE
stringrequired
Base64 HMAC-SHA256 of timestamp + body.

Request

BASH
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

JSON
{
  "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.

Create order
POST/api/v1/orders
Submitting the same clientOrderId twice returns the existing order (idempotent).

Body fields

clientOrderId
stringrequired
Your unique ID. Returned with every webhook so you can correlate.
provider
stringrequired
Provider slug, e.g. jalat.
remark
stringoptional
Note for the order, max 255 chars.
parcels
Parcel[]required
At least one parcel.

Parcel fields

clientParcelId
stringrequired
Your unique ID for this parcel. Used in webhook callbacks.
address
stringrequired
Delivery address.
recipientPhone
stringrequired
Recipient phone number.
recipientName
stringoptional
Recipient name.
price
numberoptional
COD amount.
latitude
numberoptional
GPS latitude.
longitude
numberoptional
GPS longitude.
remark
stringoptional
Note to driver, max 255 chars.

Request

BASH
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

JSON
{
  "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"
  }
}
Get order
GET/api/v1/orders/:clientOrderId
Fetch an order by your clientOrderId. Returns all parcels with their current status, tracking link, and driver.

Request

BASH
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl http://localhost:6060/api/v1/orders/ORD-2026-001 \
  -H "Authorization: Bearer $TOKEN"

Response 200

JSON
{
  "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.

Get parcel
GET/api/v1/parcels/:clientParcelId

Request

BASH
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl http://localhost:6060/api/v1/parcels/PCL-001 \
  -H "Authorization: Bearer $TOKEN"

Response 200

JSON
{
  "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.

Provider callback
POST/api/v1/webhooks/:providerSlug
Same canonical/HMAC pattern as outbound calls to the provider. The body is signed and verified as raw bytes — don't reformat the JSON between signing and sending.

Required headers

X-TIMESTAMP
stringrequired
Unix seconds; rejected if more than ±5 min from now.
X-SIGNATURE
stringrequired
Base64 HMAC-SHA256 over timestamp + rawBody.

Allowed status values

PENDINGPICKUP_ASSIGNEDPICKED_UPON_DELIVERYDELIVEREDDELIVERY_FAILED

Jalat payload fields

parcelId
stringrequired
Jalat's internal parcel ID.
partnerParcelId
stringrequired
Your clientParcelId.
address
stringrequired
Delivery address.
recipientPhone
stringrequired
Recipient phone.
status
ParcelStatusrequired
Current parcel status.
trackingLink
stringoptional
Public tracking URL.
driver
{ id, name, phone }optional
Assigned driver.
reason
stringoptional
Failure reason if status is failed.
latitude / longitude
numberoptional
GPS coordinates.

Request

BASH
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

200
Acceptedoptional
Processing happens async. Body is null.
400
Bad requestoptional
Body isn't JSON or fails schema (e.g. unknown status).
401
Unauthorizedoptional
Missing/invalid X-TIMESTAMP or X-SIGNATURE, or timestamp expired.
404
Not 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

The admin key lives in 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.
Create client
POST/api/v1/admin/clients
Generates apiKey and apiSecret. apiSecret is returned only once — store it immediately.

Body fields

name
stringrequired
Human-readable client name.
email
stringrequired
Unique identifier (not used for sending).
webhookUrl
stringoptional
Where to forward provider status events.
webhookSecret
stringoptional
Used to sign outbound webhook payloads.

Request

BASH
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

JSON
{
  "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"
  }
}
List clients
GET/api/v1/admin/clients
apiSecret is never returned here — only at create or rotate time.

Request

BASH
curl http://localhost:6060/api/v1/admin/clients \
  -H "X-ADMIN-KEY: $ADMIN_KEY"

Response 200

JSON
{
  "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"
    }
  ]
}
Update client
PATCH/api/v1/admin/clients/:id
All body fields are optional; only provided fields are updated. email, apiKey, and apiSecret cannot be changed here — use rotate for the secret.

Body fields (all optional)

name
stringoptional
Display name.
webhookUrl
stringoptional
Delivery callback URL.
webhookSecret
stringoptional
Outbound signing secret.
status
"active" | "suspended"optional
Suspending a client immediately revokes their tokens.

Request

BASH
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"
  }'
Rotate secret
POST/api/v1/admin/clients/:id/rotate
Generates a new apiSecret. The old secret is immediately invalidated and the new plaintext is returned once.

Request

BASH
curl -X POST http://localhost:6060/api/v1/admin/clients/664a1b2c3d4e5f6a7b8c9d0e/rotate \
  -H "X-ADMIN-KEY: $ADMIN_KEY"

Response 200

JSON
{
  "success": true,
  "data": {
    "id": "664a1b2c3d4e5f6a7b8c9d0e",
    "apiKey": "3d1dae7d-1234-4abc-9def-abcdef012345",
    "apiSecret": "new-uuid-secret-here"
  }
}

Errors

All error responses follow the same envelope.

JSON
{
  "success": false,
  "message": "Description of the error"
}
HTTP status codes
400
Bad requestoptional
Validation failed or invalid JSON.
401
Unauthorizedoptional
Missing, invalid, or expired token / bad signature.
404
Not foundoptional
Resource (order, parcel, client) not found.
409
Conflictoptional
Duplicate identifier (e.g. clientOrderId or email already used).
429
Too many requestsoptional
Rate limit exceeded — auth attempts are 10/min per API key.
500
Internaloptional
Unexpected server error.