DovExpress — Public API & Webhook Integration Guide (EN)
Audience: external partners/clients integrating with DovExpress for order injection, tracking, and proof-of-delivery (POD). Last reviewed against source:
api/src(controllersapi.ts,orders.ts,clients.ts,status.ts; servicewebhooks.ts).
Everything below is live in production unless explicitly marked [PROPOSED] (a planned gap — don't rely on it yet).
Contents
- Overview
- Quickstart
- Authentication
- Endpoints
- Webhooks
- Errors
- Status catalog
- Timezone
- Proof of Delivery (POD)
- Status lifecycle & roadmap
- Quick reference
1. Overview
DovExpress exposes a small REST API for clients. A client is a row in the Clients table identified by a unique api_key. As a client you can:
- Create orders for delivery.
- Query a single order (by internal id, or by your reference / DovExpress code) with its full tracking history.
- Receive a webhook
POSTon every status change, once you register a webhook URL.
The order lifecycle is driven by a shared status catalog (44 statuses in production). Status changes are produced by DovExpress operations (warehouse, distributor, driver app, admin panel) and pushed to you in real time.
You ──POST /api/orders/create──▶ DovExpress
│
operations move the order through statuses
│
Your webhook ◀──POST {data}──────────┘ (status change, incl. POD on Delivered)2. Quickstart
A working integration is four steps:
- Get your
api-keyfrom DovExpress (your sole credential — keep it secret). - Register your webhook so you receive status updates:bash
curl -X PUT https://api.dovexpresscr.com/api/webhook \ -H "api-key: $DOV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/dovexpress/webhook" }' - Create an order:bash
curl -X POST https://api.dovexpresscr.com/api/orders/create \ -H "api-key: $DOV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reference_id": "CR0256301601", "contactDetails": { "contactName": "Juan Perez", "contactPhone": "88888888" }, "addressDetails": { "address": "200m norte de la iglesia", "postalCode": "10203" } }' - Track it — either wait for webhook events, or poll:bash
curl https://api.dovexpresscr.com/api/orders/reference/CR0256301601 \ -H "api-key: $DOV_API_KEY"
Sync your status catalog once at startup with
GET /api/statuses(§4.4) and map by the DOV status code — never hardcode numeric codes.
3. Authentication
Production base URL:
https://api.dovexpresscr.comCredential: every request must send the header:
api-key: <YOUR_CLIENT_API_KEY>
The API resolves the client from api-key. A missing or unknown key returns 401 Unauthorized. There is no OAuth/JWT for clients — the api-key is the sole credential. Keep it server-side and secret; never expose it in browser or mobile code.
All endpoints are scoped to your client: you can only read and write your own orders.
4. Endpoints
4.1 Create order — POST /api/orders/create
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
reference_id | string | – | Your tracking reference. Unique per client (re-use → 409). |
contactDetails.contactName | string | ✅ | Recipient name. |
contactDetails.contactPhone | string | – | Primary phone. |
contactDetails.contactPhone2 | string | – | Secondary phone. |
addressDetails.address | string | ✅ | 1..2000 chars. |
addressDetails.postalCode | string | ✅ | |
addressDetails.state / region / city / country | string | – | |
addressDetails.lat / lng | number | – | Coordinates for routing. |
addressDetails.notes | string | – | Delivery notes for the driver. |
packageDetails.product | string | – | "Name - qty", or comma-separated "A - 1, B - 2". |
packageDetails.quantity | number | – | |
packageDetails.products[] | array | – | Structured alternative: { product, name, quantity }. |
cod | number | – | Cash-on-delivery amount. |
notes | string | – | Order-level notes. |
Example body
{
"reference_id": "CR0256301601", // optional; unique per client
"contactDetails": {
"contactName": "Juan Perez", // required
"contactPhone": "88888888",
"contactPhone2": "70000000"
},
"addressDetails": {
"address": "200m norte de la iglesia", // required (1..2000 chars)
"state": "San José",
"region": "Central",
"city": "Escazú",
"country": "Costa Rica",
"postalCode": "10203", // required
"lat": 9.9281,
"lng": -84.0907,
"notes": "Llamar antes"
},
"packageDetails": {
"product": "Shoes - 1", // "Name - qty", or "A - 1, B - 2"
"quantity": 1,
"products": [
{ "product": "SKU123", "name": "Shoes", "quantity": 1 }
]
},
"cod": 25000,
"notes": "Fragile"
}Behavior
- New orders start at status
Created. reference_idis deduplicated per client: re-sending the same reference returns409 Conflict.- DovExpress generates the internal order
code(e.g.DOV_218345) — this is the tracking number shown in the tracking portal. Store it alongside yourreference_id.
4.2 Get order by internal id — GET /api/orders/:id
4.3 Get order by reference or code — GET /api/orders/reference/:reference
:reference matches either your reference_id or the DovExpress code. Both 4.2 and 4.3 are scoped to your client and return:
{
"data": {
"id": "uuid",
"code": "DOV_218345", // DovExpress order/tracking code
"reference_id": "CR0256301601",
"contactDetails": { "contactName": "...", "contactPhone": "...", "contactPhone2": "..." },
"addressDetails": { "address": "...", "state": "...", "region": "...", "city": "...",
"country": "...", "postalCode": "...", "lat": 0, "lng": 0, "notes": "..." },
"packageDetails": { "product": "...", "quantity": "..." },
"cod": 25000,
"currentStatus": {
"code": 5014, "statusId": "uuid", "statusName": "Delivered", "statusNameEs": "Entregado",
"createdAt": "2026-02-11T17:38:58.000Z",
"pod": [ { "type": "photo", "url": "https://dovexpress.s3.amazonaws.com/uploads/photos/..." } ]
},
"pod": [ { "type": "photo", "url": "https://dovexpress.s3.amazonaws.com/uploads/photos/..." } ],
"trackingHistory": [
{ "code": 5001, "statusId": "uuid", "statusName": "Created", "statusNameEs": "Creado", "createdAt": "...", "pod": [] },
{ "code": 5025, "statusId": "uuid", "statusName": "In Transit", "statusNameEs": "En Tránsito", "createdAt": "...", "pod": [] },
{ "code": 5014, "statusId": "uuid", "statusName": "Delivered", "statusNameEs": "Entregado", "createdAt": "...",
"pod": [ { "type": "photo", "url": "https://dovexpress.s3.amazonaws.com/uploads/photos/..." } ] }
]
}
}Each trackingHistory item and currentStatus includes the DOV status code (code), the English (statusName) and Spanish (statusNameEs) names, the timestamp (createdAt), and a pod array of proof-of-delivery file URLs for that event (populated e.g. on Delivered). The order object also carries a top-level pod (the POD of the current event) and code (the DovExpress order/tracking code).
4.4 List status catalog — GET /api/statuses
Returns the full shared catalog. Call it once at startup (and periodically) to keep your mapping in sync, including any status DovExpress adds later:
curl https://api.dovexpresscr.com/api/statuses -H "api-key: $DOV_API_KEY"{
"data": [
{ "code": 5014, "name": "Delivered", "name_es": "Entregado", "is_final": true, "requires_photo": true, "requires_signature": false },
{ "code": 5025, "name": "In Transit", "name_es": "En Tránsito", "is_final": false, "requires_photo": false, "requires_signature": false }
]
}code is the DOV status code (see §7). Map your internal states against it.
4.5 Self-service webhook management — GET / PUT / DELETE /api/webhook
Manage your own webhook endpoint with your api-key — no DovExpress intervention required:
| Method | Effect |
|---|---|
GET /api/webhook | Returns { "data": { "url": "...", "updatedAt": "..." } }, or { "data": null } if none. |
PUT /api/webhook | Body { "url": "https://your-endpoint/hook" } → registers or updates (upsert; overwrites the previous URL). |
DELETE /api/webhook | Removes your webhook. |
The relation is one webhook URL per client (ClientsWebhooks, client_id unique).
5. Webhooks
5.1 Configuration
The webhook is self-service via your api-key (§4.5): PUT to register/update, GET to read, DELETE to remove. A DovExpress admin can also register it on your behalf. One webhook URL per client; PUT upserts.
5.2 Delivery & payload
On a status change, DovExpress sends an HTTP POST to your URL with:
- Header
Authorization: Basic <fixed token>— the token DovExpress issued you. - JSON body shaped as
{ "data": <payload> }.
There are two payload shapes, depending on the path that triggered the change:
A) Single status update (rich — most events):
{
"data": {
"id": "uuid",
"code": "DOV_218345",
"reference_id": "CR0256301601",
"contactDetails": { "...": "..." },
"addressDetails": { "...": "..." },
"packageDetails": { "product": "...", "quantity": "..." },
"cod": 25000,
"currentStatus": { "code": 5014, "statusId": "uuid", "statusName": "Delivered", "statusNameEs": "Entregado", "createdAt": "...",
"pod": [ { "type": "photo", "url": "https://dovexpress.s3.amazonaws.com/uploads/photos/..." } ] },
"trackingHistory": [ { "code": 5001, "statusId": "...", "statusName": "...", "statusNameEs": "...", "createdAt": "...", "pod": [] } ]
}
}B) Bulk operations (minimal — e.g. mass assignment / cargo received):
{ "data": { "orderId": "uuid", "statusId": "uuid", "userId": "uuid" } }⚠️ The bulk payload (B) carries only internal UUIDs — no reference, no status name/code, no POD. When you receive it, resolve details by calling
GET /api/orders/:id(§4.2) and mappingstatusIdagainst your cachedGET /api/statuses(§4.4).
5.3 Proof of delivery in the webhook — YES ✅
The payload's trackingHistory carries a pod array per event, and the order object exposes a top-level pod for the current event. On Delivered, this contains the proof-of-delivery photo URL(s) (and signature when present), so you receive the POD automatically — no polling. The same enrichment applies to the GET-order endpoints (§4.2–4.3).
5.4 Handling webhooks — recommendations
- Verify before trusting: check the
Authorization: Basicheader matches the token DovExpress issued you; reject anything else with401. - Acknowledge fast: return HTTP
2xxas soon as you've persisted the event; do heavy work asynchronously so DovExpress isn't blocked on your processing. - Be idempotent: the same event may arrive more than once. Deduplicate on a stable key — e.g.
id+statusId+createdAt— and make re-processing a no-op. - Handle both shapes: branch on the payload (rich
currentStatusvs. minimalorderId/statusId/userId) and enrich shape B via the API as in §5.2.
6. Errors
The API uses conventional HTTP status codes. The codes you should handle explicitly:
| Code | When | What to do |
|---|---|---|
401 Unauthorized | Missing or unknown api-key (or webhook Authorization mismatch). | Check the credential/header. |
400 Bad Request | Invalid body — missing required field, or address outside 1..2000 chars. | Fix the payload before retrying. |
409 Conflict | reference_id already used by your client. | Treat as "already created"; do not retry blindly. |
404 Not Found | Order id/reference does not exist or is not yours. | Verify the identifier; orders are client-scoped. |
5xx | Transient server error. | Retry with backoff; webhook deliveries you fail to 2xx should be reconciled by polling. |
Treat
4xxas your error (fix and retry deliberately) and5xx/network errors as transient (retry with exponential backoff). For idempotent safety on creates, key on yourreference_id.
7. Status catalog
All clients share one Status catalog. Production currently has 44 statuses.
Don't hardcode codes. Each status has a DOV status code — a DovExpress-owned, auto-assigned, stable integer (
MAX+1, starting at5001; existing codes never change). Fetch the live catalog fromGET /api/statuses(§4.4) and map againstcode. The names below are a human reference; the authoritativecode/name/name_esalways come from the API.
7.1 Key milestone statuses
| name (EN) | name_es | Final |
|---|---|---|
| Created | Creado | |
| Second Attempt | Segundo Intento | |
| First attempt — Consignee Moved | 1er Intento Cambió Domicilio | |
| First attempt — Consignee Not Available | 1er Intento Destinatario No Disponible | |
| First attempt — Refused by Consignee | 1er Intento Rehusado por Destinatario | |
| First attempt — Inaccessible Delivery Zone | 1er Intento Fuera de Cobertura | |
| First attempt — Incorrect Address | 1er Intento Dirección Errónea | |
| First attempt — Unknown Consignee | 1er Intento Destinatario Desconocido | |
| 2nd attempt — Consignee Not Available | 2do Intento Destinatario No Disponible | |
| 2nd attempt — Incorrect Address | 2do Intento Dirección Incorrecta | |
| 2nd attempt — Refused by Consignee | 2do Intento Rehusado por Destinatario | |
| 2nd attempt — Unknown Consignee | 2do Intento Destinatario Desconocido | |
| Delivered | Entregado | ✅ |
7.2 Operational / internal statuses
Assigned to Distributor, Assigned to Driver, In Transit, Cargo Received, Received by Distributor, Accepted, First Attempt, Absent, Cannot Locate, Incorrect Phone, No Money, Does Not Want It, Bought Other Product, Will Buy Later, Other Promotion, To Rescue, Already Received the Package, Duplicate Package, Rescheduled, Wrong Address, Wrong Price, Wrong Product, Out of Coverage, Did Not Request Anything, Returned to Sender (final), Returned to Warehouse (DOV), Call Center Time, Out of the country, Hospitalized, Package in Warehouse, Rejected.
Final statuses: only Delivered and Returned to Sender. Once an order is final, its status can no longer change. Photo required when setting: Delivered, Duplicate Package, Rescheduled.
8. Timezone
The API process runs with TZ='America/Costa_Rica' (UTC−6, no daylight saving). Tracking event timestamps are managed and displayed in Costa Rica time. Timestamps serialized over JSON are ISO-8601; convert to America/Costa_Rica for local display.
9. Proof of Delivery (POD)
- On Delivered, the driver app captures a mandatory photo (and a signature where the status requires it). Files are stored in S3 as
OrderFileswithfile_typeofphotoorsignature. - Public URL format:
https://dovexpress.s3.amazonaws.com/uploads/photos/<uuid>-<timestamp> - POD URLs are delivered to you automatically in the webhook
Deliveredevent (podarray, §5.3) and are also available on demand via the GET-order endpoints (§4.2–4.3).
10. Status lifecycle & roadmap
When a DovExpress admin adds a new status, it is automatically assigned the next free DOV status code (MAX+1, starting at 5001) — unique and stable, so existing codes never shift. The new status immediately appears in GET /api/statuses. You stay in sync by polling that endpoint; because codes are stable, a simple diff against your stored catalog surfaces any additions.
Delivered in this integration:
- ✅ Status catalog endpoint —
GET /api/statuses(§4.4). - ✅ Enriched, consistent webhook payload across all paths (single + bulk), including
code,reference_id,statusName,statusNameEs,createdAt. - ✅ POD in the webhook on
Delivered— file URL(s) attached automatically (§5.3). - ✅ Self-service webhook management —
GET/PUT/DELETE /api/webhookwith upsert (§4.5).
Optional / future [PROPOSED]:
status.createdpush event — proactively notify integrated clients when a new status is added (today you detect it by pollingGET /api/statuses).
11. Quick reference
| Capability | Status |
|---|---|
Create order (POST /api/orders/create) | ✅ Implemented |
| Get order by id / reference | ✅ Implemented |
| Costa Rica timestamps (UTC−6) | ✅ Implemented |
| Webhook on status change (per client) | ✅ Implemented |
| Rich, consistent payload (single + bulk) | ✅ Implemented |
| POD images in webhook / API | ✅ Implemented |
Status catalog endpoint (GET /api/statuses) | ✅ Implemented |
Self-service webhook management (GET/PUT/DELETE /api/webhook) | ✅ Implemented |
| DOV status codes (auto, unique, stable) | ✅ Implemented |
New-status push notification (status.created) | ❌ Optional / future |
Endpoint cheat-sheet — all require the api-key header, base https://api.dovexpresscr.com:
| Method | Path | Purpose |
|---|---|---|
POST | /api/orders/create | Create an order |
GET | /api/orders/:id | Get order by internal id |
GET | /api/orders/reference/:reference | Get order by your reference or DOV code |
GET | /api/statuses | List the status catalog |
GET | /api/webhook | Read your webhook URL |
PUT | /api/webhook | Register / update your webhook URL |
DELETE | /api/webhook | Remove your webhook |