API Documentation

Reference for approved API partners integrating with the DBGL delivery API.

1. Getting Started
  1. Sign up at apip/signup with your business and contact details.
  2. Our team reviews and approves your application in CPN.
  3. On approval, you're emailed an api_key and api_secret (the secret is shown only once, so store it securely).
  4. (Optional) Register a webhook URL (section 7) to get push notifications on order events instead of polling.
2. Authentication

Every call to apip/api/ requires both headers:

X-Api-Key: your_api_key
X-Api-Secret: your_api_secret

There is no session/cookie auth and no CSRF token for the API — it's stateless, key-authenticated per request. A suspended or not-yet-approved client will get a 401.

3. Create Order

POST /apip/api/orders_create.php

Required fields:

  • partner_order_ref — your own order number. Also used as an idempotency key: posting the same value again returns the existing order (HTTP 200) instead of creating a duplicate.
  • vendor_name, vendor_phone, vendor_address, vendor_city, vendor_state — the pickup point (your vendor).
  • customer_name, customer_phone, delivery_address, delivery_city, delivery_state — the dropoff (your customer).
  • package_description

Optional fields:

  • vendor_email, vendor_street, delivery_street
  • weight_kg, declared_value, special_instructions
  • service_type — defaults to standard (also accepts express)
  • pickup_latitude / pickup_longitude, delivery_latitude / delivery_longitude — recommended; if omitted, we geocode vendor_address/delivery_address server-side instead. Supplying coordinates directly is faster (skips a geocoding round-trip) and slightly more accurate, but address-only is fully supported — useful if your platform has no way to capture coordinates (e.g. a chat-based storefront). If neither coordinates nor a resolvable address are available, the order falls into manual review (see below).

Example request:

curl -X POST https://yourdomain/apip/api/orders_create.php \
  -H "X-Api-Key: your_api_key" \
  -H "X-Api-Secret: your_api_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "partner_order_ref": "MKT-00123",
    "vendor_name": "Acme Vendor Store",
    "vendor_phone": "08011112222",
    "vendor_address": "12 Adeola Odeku St, Victoria Island",
    "vendor_city": "Victoria Island",
    "vendor_state": "Lagos",
    "pickup_latitude": 6.4281,
    "pickup_longitude": 3.4219,
    "customer_name": "Chidi Customer",
    "customer_phone": "08033334444",
    "delivery_address": "5 Bourdillon Rd, Ikoyi",
    "delivery_city": "Ikoyi",
    "delivery_state": "Lagos",
    "delivery_latitude": 6.4531,
    "delivery_longitude": 3.4342,
    "package_description": "Electronics package",
    "weight_kg": 2.5,
    "service_type": "standard"
  }'

Example response (201 Created):

{
  "order_id": "ORD293535848",
  "waybill_no": "WB459009877",
  "status": "confirmed",
  "total_amount": 3687,
  "discount_percent": 0,
  "discount_amount": 0,
  "currency": "NGN",
  "review_required": false,
  "delivery_pin": "384920"
}

discount_percent/discount_amount show exactly what, if anything, was deducted for a negotiated account discount — 0 if none applies. total_amount is always the final, already-discounted amount.

If review_required is true and status is on_hold, we couldn't determine a distance for this delivery — either coordinates were missing and the supplied address couldn't be geocoded, or the daily geocoding limit was reached. An unrecognized city/area name alone does not put an order on hold — we still price it, we just can't attach a zone for internal reporting. The order still exists with a waybill — our ops team resolves distance issues manually, and the status will update once confirmed. Check back via the status endpoint below.

delivery_pin is the 6-digit code your recipient (or you) will need to give the rider to confirm delivery — the same code shown on our internal delivery-confirmation screens. It's null until the order is confirmed: immediately for a normal order, or once our ops team resolves an on_hold one. Poll the status endpoint below if it's not present yet.

4. Get a Price Quote

POST /apip/api/orders_quote.php

Returns pricing for a would-be order without creating anything — no order, no waybill, no notification, safe to call as often as you need while a customer is deciding. Uses the exact same pricing engine as Create Order, so a quote and the order you go on to create from the same input will always price identically.

Required fields:

  • vendor_city, vendor_state, delivery_city, delivery_state
  • Per side, one of: pickup_latitude + pickup_longitude, OR vendor_address (street-level text) — same for delivery with delivery_latitude/delivery_longitude or delivery_address. Coordinates are faster; address-only works the same as Create Order (we geocode it for you).

Optional fields:

  • service_type — defaults to standard
  • weight_kg — defaults to 0

Example response (200 OK):

{
  "service_type": "standard",
  "distance_km": 6.4,
  "base_fee": 1200,
  "distance_charge": 1850,
  "weight_charge": 125,
  "discount_percent": 10,
  "discount_amount": 317.5,
  "insurance_fee": 0,
  "total_amount": 3175,
  "currency": "NGN"
}

base_fee + distance_charge + weight_charge - discount_amount + insurance_fee = total_amount, always.

This is a live calculation against current rates, not a locked-in price — if you quote, wait, then create the order, the two should normally match but aren't guaranteed to if our rates change in between. An unrecognized city/area name does not block a quote — pricing works purely from distance and weight. If distance genuinely can't be determined (no coordinates and the supplied address can't be geocoded), you get a clear 422 quote_unavailable rather than a degraded guess — there's nothing to hold for manual review the way an on_hold order has, so we tell you immediately instead.

5. Check Status

GET /apip/api/orders_status.php?order_id=ORD293535848

or by your own reference:

GET /apip/api/orders_status.php?partner_order_ref=MKT-00123

Example response (200 OK):

{
  "order_id": "ORD293535848",
  "partner_order_ref": "MKT-00123",
  "status": "confirmed",
  "waybill_no": "WB459009877",
  "shipment_status": "pending_assignment",
  "delivery_pin": "384920",
  "total_amount": 3687,
  "discount_percent": 0,
  "discount_amount": 0,
  "currency": "NGN",
  "payment_status": "invoiced",
  "payment_link_url": "https://yourdomain/apip/index.php?page=paylink&id=...&token=...",
  "created_at": "2026-07-28 09:01:46",
  "confirmed_at": null,
  "picked_up_at": null,
  "delivered_at": null,
  "cancelled_at": null
}

Lookups are scoped to your own API client — an order created by another partner, or an unknown order_id/partner_order_ref, always returns 404. This works as pure polling, or pair it with webhooks (section 7 below) for push notifications instead of repeated polling.

6. Billing & Payments

Each order's payment_status moves through:

  • unpaid — delivered but not yet billed
  • invoiced — bundled into a payment request; not yet paid
  • paid — payment confirmed

When we invoice one or more of your orders, we generate a single payment link covering them and email it to your account's contact address. The link is also returned as payment_link_url on the status endpoint above once one exists (null while still unpaid). Paying via the link updates every bundled order's payment_status to paid; the same URL keeps working afterward and simply shows a paid receipt.

There's no API endpoint to request a payment link yourself — invoicing is initiated on our side. Poll orders_status.php or watch for the emailed link.

If your account has a negotiated discount, discount_percent and discount_amount — returned by orders_create.php, orders_status.php, and orders_quote.php — show exactly what was deducted, if anything. total_amount is always the final, already-discounted figure; nothing further to calculate.

7. Webhooks

Optional push notifications for order-lifecycle events, so you don't have to poll orders_status.php. Polling still works and isn't deprecated — webhooks are additive, and delivery isn't guaranteed exactly-once, so your handler should be idempotent per (order_id, event).

Configure your endpoint:

POST /apip/api/webhook_config.php
X-Api-Key: your_api_key
X-Api-Secret: your_api_secret
Content-Type: application/json

{ "webhook_url": "https://yourdomain.com/dbgl-webhook" }

Must be https:// and publicly reachable (no private/internal addresses). The first time you set a URL, the response includes a webhook_secret — shown only once, store it securely. Calling this again to change the URL keeps the same secret unless you also pass "regenerate_secret": true, which issues a new one (invalidating the old one immediately). GET the same endpoint anytime to see your current URL and whether a secret is set (the secret itself is never returned again).

Events:

EventFires when
order.confirmedOrder created and priced successfully, or an on_hold order is resolved by our ops team
order.on_holdOrder created but pickup/delivery zone or pricing couldn't be auto-resolved
order.assignedA rider is assigned
order.picked_upRider confirms pickup
order.deliveredRider confirms delivery (verification code accepted)
order.cancelled / order.failedOrder manually cancelled or marked failed by our ops team
payment.paidAn invoice covering this order is paid (via the emailed payment link, or marked paid manually)

This list covers the normal lifecycle. Manual corrections by our ops team can occasionally emit an order.<status> event outside this list (e.g. reverting an order to an earlier status) — treat the event list as open-ended and ignore any event value you don't recognize rather than rejecting it.

Payload (identical shape to the status endpoint's core fields):

{
  "event": "order.delivered",
  "order_id": "ORD293535848",
  "partner_order_ref": "MKT-00123",
  "status": "delivered",
  "shipment_status": "delivered",
  "delivery_pin": "384920",
  "total_amount": 3687,
  "currency": "NGN",
  "timestamp": "2026-08-22T14:03:11+01:00"
}

Verifying the signature: every delivery includes an X-Webhook-Signature header — the hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook_secret. Recompute it and compare with a constant-time check before trusting the payload:

$expected = hash_hmac('sha256', $raw_body, $webhook_secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'])) {
    http_response_code(401);
    exit;
}

We send with a 3-second timeout and expect any 2xx response. Anything else (timeout, non-2xx, connection error) is retried with exponential backoff — up to 5 attempts total, spaced further apart each time, capped at roughly hourly. After 5 failed attempts we stop retrying that event; poll orders_status.php as a fallback if you suspect a missed notification.

8. Errors

Errors are returned as JSON in this shape:

{
  "error": {
    "code": "invalid_credentials",
    "message": "Invalid API key or secret."
  }
}
StatusWhen
400Request body is not valid JSON
401Missing/invalid API key or secret, or the client is not approved/suspended
404Order not found, or not owned by this API client
405Wrong HTTP method for the endpoint (e.g. GET on orders_create.php)
422Missing required field or malformed request
429Daily geocoding limit reached for your account (only applies to address-only requests — never happens if you supply coordinates directly). Not a permanent cap; contact us to raise it
500Unexpected server error