Skip to content

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 (controllers api.ts, orders.ts, clients.ts, status.ts; service webhooks.ts).

Everything below is live in production unless explicitly marked [PROPOSED] (a planned gap — don't rely on it yet).

Contents

  1. Overview
  2. Quickstart
  3. Authentication
  4. Endpoints
  5. Webhooks
  6. Errors
  7. Status catalog
  8. Timezone
  9. Proof of Delivery (POD)
  10. Status lifecycle & roadmap
  11. 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 POST on 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:

  1. Get your api-key from DovExpress (your sole credential — keep it secret).
  2. 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" }'
  3. 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" }
      }'
  4. 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.com

  • Credential: 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

FieldTypeRequiredNotes
reference_idstringYour tracking reference. Unique per client (re-use → 409).
contactDetails.contactNamestringRecipient name.
contactDetails.contactPhonestringPrimary phone.
contactDetails.contactPhone2stringSecondary phone.
addressDetails.addressstring1..2000 chars.
addressDetails.postalCodestring
addressDetails.state / region / city / countrystring
addressDetails.lat / lngnumberCoordinates for routing.
addressDetails.notesstringDelivery notes for the driver.
packageDetails.productstring"Name - qty", or comma-separated "A - 1, B - 2".
packageDetails.quantitynumber
packageDetails.products[]arrayStructured alternative: { product, name, quantity }.
codnumberCash-on-delivery amount.
notesstringOrder-level notes.

Example body

jsonc
{
  "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_id is deduplicated per client: re-sending the same reference returns 409 Conflict.
  • DovExpress generates the internal order code (e.g. DOV_218345) — this is the tracking number shown in the tracking portal. Store it alongside your reference_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:

jsonc
{
  "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:

bash
curl https://api.dovexpresscr.com/api/statuses -H "api-key: $DOV_API_KEY"
jsonc
{
  "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:

MethodEffect
GET /api/webhookReturns { "data": { "url": "...", "updatedAt": "..." } }, or { "data": null } if none.
PUT /api/webhookBody { "url": "https://your-endpoint/hook" } → registers or updates (upsert; overwrites the previous URL).
DELETE /api/webhookRemoves 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):

jsonc
{
  "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):

jsonc
{ "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 mapping statusId against your cached GET /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: Basic header matches the token DovExpress issued you; reject anything else with 401.
  • Acknowledge fast: return HTTP 2xx as 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 currentStatus vs. minimal orderId/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:

CodeWhenWhat to do
401 UnauthorizedMissing or unknown api-key (or webhook Authorization mismatch).Check the credential/header.
400 Bad RequestInvalid body — missing required field, or address outside 1..2000 chars.Fix the payload before retrying.
409 Conflictreference_id already used by your client.Treat as "already created"; do not retry blindly.
404 Not FoundOrder id/reference does not exist or is not yours.Verify the identifier; orders are client-scoped.
5xxTransient server error.Retry with backoff; webhook deliveries you fail to 2xx should be reconciled by polling.

Treat 4xx as your error (fix and retry deliberately) and 5xx/network errors as transient (retry with exponential backoff). For idempotent safety on creates, key on your reference_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 at 5001; existing codes never change). Fetch the live catalog from GET /api/statuses (§4.4) and map against code. The names below are a human reference; the authoritative code/name/name_es always come from the API.

7.1 Key milestone statuses

name (EN)name_esFinal
CreatedCreado
Second AttemptSegundo Intento
First attempt — Consignee Moved1er Intento Cambió Domicilio
First attempt — Consignee Not Available1er Intento Destinatario No Disponible
First attempt — Refused by Consignee1er Intento Rehusado por Destinatario
First attempt — Inaccessible Delivery Zone1er Intento Fuera de Cobertura
First attempt — Incorrect Address1er Intento Dirección Errónea
First attempt — Unknown Consignee1er Intento Destinatario Desconocido
2nd attempt — Consignee Not Available2do Intento Destinatario No Disponible
2nd attempt — Incorrect Address2do Intento Dirección Incorrecta
2nd attempt — Refused by Consignee2do Intento Rehusado por Destinatario
2nd attempt — Unknown Consignee2do Intento Destinatario Desconocido
DeliveredEntregado

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 OrderFiles with file_type of photo or signature.
  • Public URL format: https://dovexpress.s3.amazonaws.com/uploads/photos/<uuid>-<timestamp>
  • POD URLs are delivered to you automatically in the webhook Delivered event (pod array, §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:

  1. Status catalog endpointGET /api/statuses (§4.4).
  2. Enriched, consistent webhook payload across all paths (single + bulk), including code, reference_id, statusName, statusNameEs, createdAt.
  3. POD in the webhook on Delivered — file URL(s) attached automatically (§5.3).
  4. Self-service webhook managementGET/PUT/DELETE /api/webhook with upsert (§4.5).

Optional / future [PROPOSED]:

  1. status.created push event — proactively notify integrated clients when a new status is added (today you detect it by polling GET /api/statuses).

11. Quick reference

CapabilityStatus
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:

MethodPathPurpose
POST/api/orders/createCreate an order
GET/api/orders/:idGet order by internal id
GET/api/orders/reference/:referenceGet order by your reference or DOV code
GET/api/statusesList the status catalog
GET/api/webhookRead your webhook URL
PUT/api/webhookRegister / update your webhook URL
DELETE/api/webhookRemove your webhook