Gbam Logistics delivery API
Get delivery prices, book pickups and track parcels from your own website, app or system. Deliveries are paid from your business balance.
Basics
- Base URL:
https://gbamlogistics.com/api/v1 - Authentication:
Authorization: Bearer sk_live_…. Use keys only from your server. - Test mode:
sk_test_…keys use simulated couriers and never cost anything. Try everything there first. - Money: all amounts are integers in kobo (₦1 = 100), currency
NGN. - Limits: 120 requests a minute per key (HTTP 429 with
Retry-Afterbeyond that). - Errors:
{"error": {"code": "insufficient_funds", "message": "…"}}with a matching HTTP status.
1. Get prices
POST /quotes. Use a saved pickup address ({"address_id": "…"}) or send one. Prices last 30 minutes. Leave out the delivery name and phone to get prices only (e.g. at your checkout, before the customer has typed them); those quotes come back with "bookable": false.
curl https://gbamlogistics.com/api/v1/quotes \
-H "Authorization: Bearer sk_test_…" -H "Content-Type: application/json" \
-d '{
"pickup": { "name": "Adaeze Okafor", "phone": "08030000000", "address": "1 Shop Road", "landmark": "Beside GTBank", "city": "Lekki", "state": "Lagos" },
"delivery": { "name": "Tunde Bello", "phone": "08051234567", "address": "5 Ademola Street", "city": "Wuse", "state": "FCT" },
"parcel": { "weight_kg": 1.5, "description": "Two dresses", "value": 2500000 }
}'{ "quotes": [
{ "id": "6f1c…", "service": "Express", "price": { "amount": 520000, "currency": "NGN" }, "eta_days": { "min": 1, "max": 2 }, "expires_at": "…" },
{ "id": "9a2b…", "service": "Standard", "price": { "amount": 341000, "currency": "NGN" }, "eta_days": { "min": 2, "max": 4 }, "expires_at": "…" }
] }2. Book
POST /shipments. Paid from your balance and booked straight away. Send your own reference (e.g. your order number): repeating a request with the same reference returns the same shipment and never charges twice, so retries are safe.
curl https://gbamlogistics.com/api/v1/shipments \
-H "Authorization: Bearer sk_test_…" -H "Content-Type: application/json" \
-d '{ "quote_id": "9a2b…", "reference": "ORDER-1001", "accept_terms": true }'{ "tracking_code": "QKMFNRNP18", "reference": "ORDER-1001", "status": "booked", "service": "Standard",
"price": { "amount": 341000, "currency": "NGN" }, "delivery_code": "4821",
"tracking_url": "https://gbamlogistics.com/track/QKMFNRNP18", "created_at": "…", "test": true }accept_terms: true confirms the parcel has no prohibited items and you accept the delivery terms. Give delivery_code to your customer only: riders may ask for it at delivery.
Errors to handle: 402 insufficient_funds (top up, or on a credit account: the limit is reached), 402 account_on_hold (an invoice is overdue), 410 quote_expired (get a new quote), 409 quote_used, 400 price_only_quote (the quote was made without the recipient's name and phone: quote again with them).
3. Track and manage
GET /shipments/{tracking_code}: status and every update (events).GET /shipments?limit=20&before=…: your shipments, newest first (next_beforefor the next page).POST /shipments/{tracking_code}/cancel: before pickup; refunded to your balance.GET /balance: yourbalance(below zero on a credit account: what you owe),credit_limitandavailable(what you can still spend).POST /shipments/{tracking_code}/simulatewith{"status": "delivered"}: test keys only, to try your webhooks.
Statuses: booked, picked_up, in_transit, out_for_delivery, delivered, delivery_failed, cancelled, booking_failed, refunded.
4. Webhooks
Add an https address in your dashboard. On every status change we POST {"id", "type": "parcel.delivered", "created_at", "data": {"tracking_code", "reference", "status", "previous_status", "test"}}. Reply with any 2xx within 10 seconds; otherwise we retry after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h. Use id to ignore repeats.
Check every request is from us: X-Signature: t=<unix time>,v1=<HMAC-SHA256 of "t.body" with your signing secret>.
import { createHmac, timingSafeEqual } from "node:crypto";
function isFromUs(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // too old: possible replay
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}Verify against the raw request body (before parsing JSON).