BiteExpress logo
For Business

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
StatusCodeMeaning
401unauthorizedMissing or unknown key
403partner-suspendedYour account is suspended; contact BiteExpress
403feature-disabledThe partner API is switched off platform-wide
429Rate 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.

Request
{
  "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
}
  • pickup may be omitted if BiteExpress has set a default pickup on your account.
  • distance_km is 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.
  • instructions reaches the rider. declared_value is recorded on the order for the rider and is not insurance.
  • Coordinates are decimal degrees. Both points must fall inside a BiteExpress parcel zone.
Response
{
  "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.

StatusMeaning
201The delivery was created. The wallet has been debited by fee.total and riders can see it.
200A delivery with this reference already existed; it is returned unchanged.
402 insufficient_balanceThe wallet cannot cover the fee. Nothing was created.
422 out_of_coveragePickup or drop-off is outside every parcel zone.
422unknown_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

statusMeaning
confirmedPaid and waiting for a rider
rider_assignedA rider accepted and is heading to pickup
in_transitCollected and on the way to the receiver
deliveredHanded over at the drop-off
canceledCanceled before collection; fee refunded to your wallet
returnedCould 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.

Headers
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
Body
{
  "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.

PHP
$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);
Node
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.

  1. Store the API key and webhook secret server-side only.
  2. Quote at checkout, create after payment, always with your own order id as reference.
  3. Handle 402 insufficient_balance by alerting your operations team to top up.
  4. Verify every webhook signature and deduplicate on X-BiteExpress-Delivery.
  5. Show status_label and, once present, the rider's name and phone to your customer.