Drum Delivery API

Same-day delivery for your online store

Book riders for your orders straight from your checkout. Quote, book and track deliveries in Lagos from your own systems, with no calls and no extra dashboard.

Built for stores that deliver every day

A rider when you book

Book a quote and get the rider's name, phone and vehicle back in the same call.

Free sandbox, instantly

Sign up and get a test key straight away. A test wallet with ₦1,000,000 of test credit, no real riders, no real money.

Pay per delivery

Top up with Paystack. Each delivery comes off your balance.

Refunds built in

No rider found? The full amount comes back. Cancel before pickup and you get the delivery fee back.

Updates pushed to you

Signed events for pickup, transit, delays and delivery.

A tracking link to share

Every delivery has a live page you can send to your customer.

How it works

Four steps from order to doorstep

01

Quote

Send pickup and drop-off. Get a price, valid for 15 minutes.

02

Book

Book the quote. A rider is assigned straight away.

03

Track

Picked up, in transit, delivered. We tell you as it happens.

04

Pay

The delivery is charged to your wallet when you book.

Docs

Build it

Quick start

Create an Online Store account, then open API keysin your dashboard and create a test key. You can start testing straight away; there's nothing to wait for.

Set $BASE to https://api.drum.ng/api/v1/partner-dispatch and $KEY to your drm_test_… key. Five calls take a test delivery from quote to doorstep. Prefer clicking? The Sandbox console in your dashboard sends the same requests and shows the code for cURL, Python and Node.js.

1. Check your key.

curl -X GET "$BASE/me" \
  -H "X-API-Key: $KEY"

2. Get a quote. It holds for 15 minutes.

curl -X POST "$BASE/quotes" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pickup": {
      "address": "12 Admiralty Way, Lekki Phase 1",
      "lat": 6.4474,
      "lng": 3.4723,
      "contact_name": "Lekki Store",
      "contact_phone": "+2348031234567"
    },
    "dropoff": {
      "address": "5 Allen Avenue, Ikeja",
      "lat": 6.6018,
      "lng": 3.3515,
      "contact_name": "Ada Okafor",
      "contact_phone": "+2348098765432"
    },
    "package_description": "Groceries, 2 bags",
    "metadata": {
      "order_id": "ORD-10045"
    }
  }'

3. Book it. The rider comes back in the response.

curl -X POST "$BASE/shipments" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a9e-booking-ORD-10045" \
  -d '{
    "quote_id": "DRM-SHP-261013-101500-A3F7C1B9E2",
    "external_ref": "ORD-10045"
  }'

4. Add your webhook. Save the signing secret; you only see it once.

curl -X PUT "$BASE/webhooks" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/webhooks/drum"
  }'

5. Move it along. Test deliveries also advance on their own every 30 seconds.

curl -X POST "$BASE/sandbox/shipments/DRM-SHP-261013-101500-A3F7C1B9E2/advance" \
  -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

You get an event at each step: shipment.assigned, shipment.picked_up, shipment.in_transit, shipment.delivered. Full reference: $BASE/docs.

Test and live

One base URL. Your key picks the mode.

TestLive
Keydrm_test_…drm_live_…
AvailableAs soon as you sign upAfter Drum approves your go-live request
RidersCourier sandbox, no real ridersReal riders
CostFree: test credit onlyCharged to your wallet when you book
PricesCourier sandbox rate + Drum platform feeLive courier rate + Drum platform fee
Status updatesEvery ~30 s, or on demand with /sandbox/…/advanceAs the rider moves
WalletSeparate test wallet, ₦1,000,000 to start, instant free top-upsPrepaid, topped up with Paystack

Test and live never mix. They have separate deliveries, separate wallets and separate statements. A live key can’t see test deliveries, test money can never pay for a live delivery, and order references only need to be unique within each mode.

You see a key once, when you create it. Lost or leaked? Revoke it in your dashboard and create a new one. You can run two keys side by side to rotate without downtime.

Keys and limits

Send your key as X-API-Key on every request, from your servers only. Never ship it in a browser or mobile app.

Keys come with every permission. Ask for a read-only key if a system only needs to look things up.

PermissionLets you
dispatch:quoteGet quotes
dispatch:createBook deliveries (and advance test deliveries)
dispatch:readLook up and list deliveries
dispatch:cancelCancel deliveries
wallet:readSee your balance and statement
wallet:fundTop up
webhooks:manageSet, test and rotate your webhook
  • Lock keys to your servers. Send us your outbound IPs and we’ll reject calls from anywhere else.
  • 120 requests a minute per key. Go over and you get 429 with a Retry-After header.
  • Give bookings 45 seconds. A live booking waits until a rider is assigned.
  • GET /me shows which mode and permissions a key has.
  • Once you’re live, only a live key can change your webhook. Test keys can still read it.

Booking a delivery

Quote with POST /quotes, then book with POST /shipments. The quote id stays the delivery id from then on.

POST /quotes

FieldTypeRequiredNotes
pickup, dropoffobjectYesSee below
package_descriptiontext, up to 200NoShown to the rider
weight_kg0–50NoDefault 1
declared_valueNGNNoDefault 5,000. Covers the package in transit
service_levelstandard | premium | expedientNoDefault standard (cheapest courier). Other levels aren't offered on every route
metadataup to 20 keysNoReturned on the delivery and every event

Each stop needs address, lat, lng, contact_name and contact_phone. address_line_2, city, state (both default to Lagos) and contact_email are optional. Send the customer’s map pin: riders navigate by it.

POST /shipments

FieldRequiredNotes
quote_idYesWithin 15 minutes of quoting
external_refRecommendedYour order number. Find deliveries by it with GET /shipments?external_ref=

Add an Idempotency-Key header to every booking. If a call times out, send it again with the same key and body: you get the first answer back and are charged once.

A booked delivery looks like this, everywhere: in responses, lookups and events.

JSON
{
  "id": "DRM-SHP-261013-101500-A3F7C1B9E2",
  "object": "shipment",
  "environment": "live",
  "status": "assigned",
  "external_ref": "ORD-10045",
  "pickup": {
    "address": "12 Admiralty Way, Lekki Phase 1",
    "lat": 6.4474,
    "lng": 3.4723,
    "contact_name": "Lekki Store",
    "contact_phone": "+2348031234567"
  },
  "dropoff": {
    "address": "5 Allen Avenue, Ikeja",
    "lat": 6.6018,
    "lng": 3.3515,
    "contact_name": "Ada Okafor",
    "contact_phone": "+2348098765432"
  },
  "package": {
    "description": "Groceries, 2 bags",
    "weight_kg": 1,
    "value": 5000
  },
  "pricing": {
    "delivery_fee": 2500,
    "platform_fee": 500,
    "total": 3000,
    "currency": "NGN"
  },
  "eta": {
    "pickup": "30 mins",
    "delivery": "2 hrs"
  },
  "rider": {
    "name": "Musa Bello",
    "phone": "+2348011112222",
    "vehicle_type": "Motorcycle",
    "fleet": "Kwik"
  },
  "last_location": null,
  "tracking_url": "https://…/track/DRM-SHP-261013-101500-A3F7C1B9E2",
  "metadata": {
    "order_id": "ORD-10045"
  },
  "quote_expires_at": null,
  "assigned_at": "2026-10-13T10:15:42+00:00",
  "picked_up_at": null,
  "created_at": "2026-10-13T10:15:00+00:00",
  "updated_at": "2026-10-13T10:15:42+00:00"
}

A quote has the same shape, with "status": "quoted", no rider yet, and a quote_expires_at. Send tracking_url to your customer.

Look up and cancel

  • GET /shipments/{id} returns one delivery.
  • GET /shipments?status=&external_ref=&page=1 lists them, newest first, up to 100 a page.
  • POST /shipments/{id}/cancel works until pickup. Once a rider is assigned, the platform fee is kept and the delivery fee is refunded; the response's refund shows the exact amounts.
  • While a courier is still being booked (the first minutes after POST /shipments), a cancel returns 409 dispatch_in_progress. Try again shortly.

Delivery status

You can cancel any time before pickup.

quotedPOST /quotes, valid 15 mindispatchingPOST /shipments, finding a riderassignedrider heading to pickuppicked_uprider has the packagein_transitheading to the drop-offdeliveredfinalcancelledyou cancelled before pickup,or no rider found (refunded)delayedno movement after pickup;Drum alerts the riderbookrider foundyou cancelno rideryou cancelno movementnext courier updatenew rider

If a rider hasn't picked up 30 minutes after being assigned, we send another one. The status stays assigned and you get shipment.rider_reassigned, at no extra cost. If a delivery stops moving after pickup, it shows delayed and we chase the rider.

Webhooks

We post every status change to your URL, signed so you know it came from us. Set it with PUT /webhooks. It must be https and public.

EventWhen
shipment.assignedA rider is assigned
shipment.picked_upThe rider has the package
shipment.in_transitOn the way to your customer
shipment.delayedStopped moving after pickup
shipment.rider_reassignedWe swapped in a new rider
shipment.deliveredDelivered
shipment.cancelledCancelled by you, or no rider found (refunded)
wallet.fundedYour top-up landed
pingYou called POST /webhooks/test
Event
{
  "id": "evt_5b0e7c2f9a8d4e1f8c3b2a1d0e9f8a7b",
  "type": "shipment.picked_up",
  "created_at": "2026-10-13T10:31:07+00:00",
  "environment": "live",
  "data": {
    "shipment": {
      "...": "same shape as GET /shipments/{id}"
    }
  }
}

Check the Drum-Signature header before you trust an event. It is an HMAC-SHA256 of "<t>." + raw body using your webhook secret:

Python
import hmac, hashlib, time

def verify(secret: str, raw_body: bytes, header: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
Node.js
const crypto = require("crypto");

function verify(secret, rawBody /* Buffer */, header, tolerance = 300) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=", 2)));
  const ts = Number(parts.t);
  if (!parts.v1 || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${ts}.`), rawBody])).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
  • Answer with a 2xx within 10 seconds. Do the slow work afterwards.
  • No answer? We retry 8 times over about an hour.
  • Events can repeat or arrive out of order. Skip ids you’ve seen and go by updated_at.
  • Test and live events share your URL. Check environment before acting.
  • GET /webhooks/events shows recent deliveries and any failures.

Paying for deliveries

Live deliveries are prepaid. Top up your Drum wallet with Paystack and each booking takes its quoted total from the balance. If the balance is short, the booking is declined and the quote stays open.

Top up with POST /wallet/fund and pay at the Paystack link it returns:

JSON
{
  "amount": 100000,
  "callback_url": "https://ops.example.com/drum/funded"
}

Top-ups run from ₦1,000 to ₦50,000,000. Bank transfer and USSD are credited in full; cards carry Paystack’s 1.5% fee.

What happensYour wallet
You bookCharged the quoted total
You cancel before a rider is assignedRefunded in full
You cancel after a rider is assigned, before pickupDelivery fee refunded; the platform fee is kept
No rider is foundRefunded in full, straight away
We swap ridersNo extra charge

GET /wallet shows your balance. GET /wallet/transactions is your statement.

Your test wallet

Test keys use a separate test wallet that starts with ₦1,000,000 of test credit. Test bookings are charged and refunded exactly like live ones, so you can test low balances and refunds. With a test key, POST /wallet/fundadds test funds instantly. There's no Paystack checkout, and a test wallet holds up to ₦10,000,000. Test money never reaches your live wallet.

Errors

Every error looks the same. Act on error.code:

JSON
{
  "error": {
    "code": "insufficient_funds",
    "message": "Prepaid wallet balance is too low for this shipment",
    "required": "3000",
    "available": "1200",
    "currency": "NGN"
  }
}
HTTPCodeWhat to do
401invalid_api_keyCheck the key, or ask us for a new one
402insufficient_fundsTop up, then book the same quote
403insufficient_scopeUse a key with that permission
403ip_not_allowedCall from an approved server
403live_not_enabledKeep testing, and request go-live from your dashboard
403live_key_requiredYou’re live: change webhook settings with a live key
403partner_suspendedContact us
403live_only / test_onlyUse the other key
404not_foundCheck the id and the mode
409already_bookedAlready booked. Use the delivery returned
409duplicate_external_refThat order is already booked
409not_cancellableAlready picked up
409dispatch_in_progressA courier is being booked. Retry the cancel in a minute
409courier_confirmation_pendingWe’re confirming the booking with the courier. Contact us if it’s urgent
409sandbox_limitThe test wallet is full (₦10,000,000 max)
409request_in_progress / idempotency_conflictRetry shortly with the same Idempotency-Key
409webhook_not_configuredSet a URL with PUT /webhooks
409missing_contact_email / wallet_frozenContact us
409cannot_advanceThat test delivery is finished
410quote_expiredGet a new quote
422invalid_requestFix the fields listed in errors
422idempotency_key_reusedUse a new key for a new booking
422service_level_unavailableThat level isn't offered on this route. Quote with standard
422invalid_idempotency_key / invalid_webhook_url / invalid_statusFix the value
429rate_limitedWait for Retry-After
502quote_unavailableTry again in a moment
502dispatch_failedNo rider found. You’ve been refunded; quote again
502cancel_failedNothing changed. Try the cancel again
5xxupstream_error / service_unavailable / internal_errorRetry with the same Idempotency-Key

Going live

Test as long as you like. When your setup can do the following, click Request live access in your dashboard:

  • Book, track and cancel a delivery end to end.
  • Check webhook signatures and ignore repeats.
  • Send an Idempotency-Key and your order number with every booking.

Once Drum approves the request, you can create live keys, top up your live wallet and book real deliveries. To lock your keys to your servers, send us your outbound IPs.