Partner Delivery API
Price a delivery at checkout, create it when the customer pays, follow it to the door, and receive a signed webhook on every status change.
Base URL: https://dashboard.bite.express/api/v1/partner
Authentication
Every request carries your API key as a bearer token and sends Accept: application/json. BiteExpress issues the key once, when your account is created or the key is rotated. Only a hash is stored on our side, so a lost key means a new key. Keep it on your server; never ship it in a browser or mobile app.
Authorization: Bearer bxp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or unknown key |
| 403 | partner-suspended | Your account is suspended; contact BiteExpress |
| 403 | feature-disabled | The partner API is switched off platform-wide |
| 429 | Rate limit exceeded (default 120 requests per minute per partner) |
Billing
Deliveries are prepaid from your partner wallet. Ask BiteExpress to credit the wallet by bank transfer; the balance is visible on GET /account. A delivery is only created when the wallet covers the quoted total, so poll the account or react to a 402 insufficient_balance to know when to top up.
The sender (you) always pays. Cash on delivery is not offered.
GET /account
{
"name": "Sabi Buy",
"status": "active",
"wallet_balance": 48200.00,
"currency": "NGN",
"webhook_url": "https://sabibuy.example/webhooks/biteexpress",
"default_pickup": { "address": "...", "latitude": 6.60, "longitude": 3.35,
"contact_name": "...", "contact_phone": "..." }
}GET /parcel-categories
The categories you can pass as parcel_category_id. Pricing can differ per category.
{ "categories": [ { "id": 3, "name": "Documents", "description": "Envelopes and small boxes" } ] }POST /deliveries/quote
Price a delivery without creating it. Call this when your customer reaches checkout and show fee.total as the delivery fee.
{
"parcel_category_id": 3,
"pickup": {
"address": "12 Allen Avenue, Ikeja",
"latitude": 6.6018,
"longitude": 3.3515,
"contact_name": "Sabi Buy Ikeja",
"contact_phone": "+2348012345678"
},
"dropoff": {
"address": "Maryland Mall, Ikeja",
"latitude": 6.5700,
"longitude": 3.3650,
"contact_name": "Bola Receiver",
"contact_phone": "+2348098765432",
"contact_email": "bola@example.com"
},
"distance_km": 8.2,
"instructions": "Fragile. Call on arrival.",
"declared_value": 15000
}pickupmay be omitted if BiteExpress has set a default pickup on your account.distance_kmis optional. It is the routed distance from your own maps provider. The straight-line distance between the two points is the floor, so a lower figure does not lower the price.instructionsreaches the rider.declared_valueis recorded on the order for the rider and is not insurance.- Coordinates are decimal degrees. Both points must fall inside a BiteExpress parcel zone.
{
"fee": { "delivery_charge": 800.00, "service_charge": 100.00, "tax": 67.50, "total": 967.50, "currency": "NGN" },
"distance_km": 8.2
}Quotes reflect the price at the moment of the call. Surge pricing can change it, so create the delivery promptly after quoting; the create response carries the fee actually charged.
POST /deliveries
Create the delivery. Same body as the quote plus a required reference, your own order id. Creating is idempotent on reference: a retry with the same reference returns the existing delivery with HTTP 200 instead of creating and charging twice.
| Status | Meaning |
|---|---|
| 201 | The delivery was created. The wallet has been debited by fee.total and riders can see it. |
| 200 | A delivery with this reference already existed; it is returned unchanged. |
402 insufficient_balance | The wallet cannot cover the fee. Nothing was created. |
422 out_of_coverage | Pickup or drop-off is outside every parcel zone. |
422 | unknown_parcel_category, pickup_required, or a validation error naming the field in code. |
GET /deliveries/{id}
The current state of a delivery by the BiteExpress id, or by your own reference with GET /deliveries?reference=YOUR-ID. Answers 404 not_found if the delivery does not belong to your account.
POST /deliveries/{id}/cancel
Optional body: { "reason": "Customer changed their mind" }. Allowed while the status is confirmed or rider_assigned. Once the parcel has been collected the call answers 409 cannot_cancel; contact BiteExpress support for a return. A successful cancel refunds the fee to your wallet and the response carries "refund": "refunded". If it says pending, BiteExpress operations will credit the wallet manually.
The delivery object
{
"id": 100123,
"reference": "SB-10293",
"status": "rider_assigned",
"status_label": "Rider assigned",
"fee": { "delivery_charge": 800.00, "service_charge": 100.00, "tax": 67.50, "total": 967.50, "currency": "NGN" },
"distance_km": 8.2,
"parcel_category": { "id": 3, "name": "Documents" },
"pickup": { "address": "...", "latitude": 6.6018, "longitude": 3.3515, "contact_name": "...", "contact_phone": "..." },
"dropoff": { "address": "...", "latitude": 6.5700, "longitude": 3.3650, "contact_name": "...", "contact_phone": "..." },
"instructions": "Fragile. Call on arrival.",
"rider": { "name": "Musa A.", "phone": "+234...", "latitude": 6.59, "longitude": 3.36,
"location_at": "2026-10-08T10:12:00+01:00" },
"timeline": [
{ "event": "order_placed", "title": "Order Placed", "at": "2026-10-08T10:00:00+01:00" },
{ "event": "rider_assigned", "title": "Rider Assigned", "at": "2026-10-08T10:04:00+01:00" }
],
"created_at": "2026-10-08T10:00:00+01:00",
"updated_at": "2026-10-08T10:04:00+01:00"
}rider is nulluntil a rider accepts. The rider's phone and position are only present while the status is rider_assigned or in_transit; closed deliveries keep the name only. The position is the rider's last report and can lag by a minute or more.
Statuses
| status | Meaning |
|---|---|
confirmed | Paid and waiting for a rider |
rider_assigned | A rider accepted and is heading to pickup |
in_transit | Collected and on the way to the receiver |
delivered | Handed over at the drop-off |
canceled | Canceled before collection; fee refunded to your wallet |
returned | Could not be delivered and was returned to the pickup |
Webhooks
Give BiteExpress an HTTPS endpoint and we POST to it on every timeline change. Each POST is signed, so verify before trusting it.
Content-Type: application/json
X-BiteExpress-Event: rider_assigned
X-BiteExpress-Delivery: 5821
X-BiteExpress-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with your webhook secret>
User-Agent: BiteExpress-Webhooks/1.0{
"event": "rider_assigned",
"status": "rider_assigned",
"occurred_at": "2026-10-08T10:04:00+01:00",
"delivery": { "...the delivery object above..." }
}Events you will see: order_placed, rider_assigned, almost_ready (the rider is heading to the pickup), picked_up, rider_at_store, arriving_soon, delivered, canceled, returned. Treat unknown events as informational and read status for the state. Cancels made by BiteExpress operations or by the rider arrive the same way as your own.
Answer with any 2xx within 10 seconds. Anything else, or a timeout, is retried with backoff (30 s, 2 min, 10 min, 30 min) up to 5 attempts. Deliveries can arrive out of order or more than once; use X-BiteExpress-Delivery to deduplicate and occurred_at to order.
Verifying the signature
Compute the HMAC over the raw request body, exactly as received, and compare with a constant-time function.
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $webhookSecret);
if (!hash_equals($expected, $_SERVER['HTTP_X_BITEEXPRESS_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$payload = json_decode($body, true);const crypto = require('crypto');
app.post('/webhooks/biteexpress', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.BITEEXPRESS_WEBHOOK_SECRET)
.update(req.body).digest('hex');
const given = req.get('X-BiteExpress-Signature') || '';
if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
const payload = JSON.parse(req.body);
res.sendStatus(200);
});Errors
Every error has the same shape. Validation errors use the field path as the code, for example dropoff.latitude.
{ "errors": [ { "code": "out_of_coverage", "message": "The pickup address is outside our delivery coverage." } ] }Testing and going live
Ask BiteExpress for a staging key. Staging runs the same code against a separate database with test riders, so deliveries you create there are never dispatched to real riders and the wallet is play money.
- Store the API key and webhook secret server-side only.
- Quote at checkout, create after payment, always with your own order id as reference.
- Handle 402 insufficient_balance by alerting your operations team to top up.
- Verify every webhook signature and deduplicate on X-BiteExpress-Delivery.
- Show status_label and, once present, the rider's name and phone to your customer.