API Documentation
Reference for approved API partners integrating with the DBGL delivery API.
1. Getting Started
- Sign up at apip/signup with your business and contact details.
- Our team reviews and approves your application in CPN.
- On approval, you're emailed an
api_keyandapi_secret(the secret is shown only once, so store it securely). - (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_streetweight_kg,declared_value,special_instructionsservice_type— defaults tostandard(also acceptsexpress)pickup_latitude/pickup_longitude,delivery_latitude/delivery_longitude— recommended; if omitted, we geocodevendor_address/delivery_addressserver-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, ORvendor_address(street-level text) — same for delivery withdelivery_latitude/delivery_longitudeordelivery_address. Coordinates are faster; address-only works the same as Create Order (we geocode it for you).
Optional fields:
service_type— defaults tostandardweight_kg— defaults to0
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 billedinvoiced— bundled into a payment request; not yet paidpaid— 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:
| Event | Fires when |
|---|---|
order.confirmed | Order created and priced successfully, or an on_hold order is resolved by our ops team |
order.on_hold | Order created but pickup/delivery zone or pricing couldn't be auto-resolved |
order.assigned | A rider is assigned |
order.picked_up | Rider confirms pickup |
order.delivered | Rider confirms delivery (verification code accepted) |
order.cancelled / order.failed | Order manually cancelled or marked failed by our ops team |
payment.paid | An 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."
}
}
| Status | When |
|---|---|
| 400 | Request body is not valid JSON |
| 401 | Missing/invalid API key or secret, or the client is not approved/suspended |
| 404 | Order not found, or not owned by this API client |
| 405 | Wrong HTTP method for the endpoint (e.g. GET on orders_create.php) |
| 422 | Missing required field or malformed request |
| 429 | Daily 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 |
| 500 | Unexpected server error |