Developers

Delivery API Integration Guide

Eziza is a delivery marketplace, not a dispatch service — nearby riders and logistics companies bid on your delivery, you (or your buyer) pick an offer, then Eziza handles live GPS tracking, OTP-verified handoff, and payout.

Base URL nvwpsccleewgirlwokys.supabase.co/functions/v1
Auth Bearer API key
Contact admin@eziza.com.ng

01. Getting access

Two paths in, depending on what you need.

  • Try it yourself, right now — sign up for a free sandbox account. No approval wait, and nothing you do there touches a real rider — see Sandbox mode.
  • Going live — email admin@eziza.com.ng once you're ready for real riders and real deliveries, and we'll promote your tenant. If you'd rather skip sandbox entirely, we can also create a live tenant for you directly.

Either way you end up with a tenant record, a partner portal login to manage your own API keys and webhook config, and an API key shown once at creation, then never retrievable again — store it like any other secret. You can issue additional keys at any time (e.g. one for staging, one for production), or revoke one yourself.

02. Sandbox mode

Every tenant starts in sandbox. Sandbox keys are prefixed eziza_test_, live keys eziza_live_. Sandbox deliveries never reach the real job board — a simulator plays only the rider's side of the lifecycle, so your integration code exercises the exact same endpoints and webhooks it would in production:

TransitionWho drives it in sandbox
A rider/company offers on your deliverySimulated — appears ~10s after creation
open → assignedYou — call accept-bid yourself
Rider "arrives" at pickupSimulated — ~15s after assignment
awaiting_pickup_confirm → picked_upYou — call confirm-pickup yourself
Rider marks deliveredSimulated — ~20s after pickup
delivered → confirmedYou — call confirm-receipt yourself

Every real webhook fires exactly the same way for sandbox deliveries, so you can fully exercise your webhook receiver without a human rider anywhere.

Going live doesn't invalidate your sandbox key — it keeps working, just against real riders now. We'd recommend issuing a fresh eziza_live_ key right after promotion.

03. Authentication

Every request — except incoming webhooks, which are signed instead (see Webhooks) — needs your API key as a bearer token:

Authorization: Bearer <your_api_key>

A missing, wrong, revoked key, or a key belonging to a deactivated tenant, gets a 401: { "error": "Unauthorized" }

04. Delivery lifecycle

open → assigned → awaiting_pickup_confirm → picked_up → delivered → confirmed

open or assigned → cancelled

StatusMeaningWho moves it here
openLive on the job board, accepting bidscreate-delivery
assignedA bid was accepted, a rider/company is assignedaccept-bid
awaiting_pickup_confirmRider arrived, waiting on your merchant to hand overthe rider's own app
picked_upHandoff confirmed, rider en routeconfirm-pickup
deliveredMarked delivered, awaiting OTP confirmationthe rider's own app
confirmedReceipt confirmed — rider/company gets paidconfirm-receipt
cancelledCancelled while still open or assignedcancel-delivery

open deliveries get a bid_closes_at timestamp (30 min out by default). This is advisory only and not currently enforced — a delivery can still receive and accept bids after this time passes.

Alternative path: instead of waiting for a bid, you can book a third-party courier directly — see External Carriers under Endpoints.

05. Endpoints

All bodies and responses are JSON. OPTIONS is supported for CORS on every endpoint.

POST/create-delivery

Puts a delivery live on the job board.

FieldTypeNotes
external_order_idstringrequired — your own order ID, unique per tenant
external_refstringfree-form, echoed back in webhooks
pickup_addressstringrequired
pickup_lat / pickup_lngnumber
pickup_statestringused for rider/company geographic matching
pickup_contact_name / _phonestring
delivery_addressstringrequired
delivery_lat / delivery_lngnumber
delivery_contact_name / _phonestring
package_descriptionstring
package_valuenumber
package_category_idnumberonly needed for External Carriers — from tenant-shipbubble-package-options
package_weight_kgnumbersame as above
package_dimensionobject{ "length": 10, "width": 10, "height": 10 }
fixed_pricenumberif set, the first rider/company to respond is auto-assigned at exactly this amount. If nobody responds within 20 minutes you get a delivery.price_too_low webhook. Omit for normal open bidding.
201
{ "delivery_id": "uuid", "status": "open", "bid_closes_at": "2026-07-13T10:30:00.000Z", "fixed_price": null }

GET/get-delivery?id=<delivery_id>

Full delivery status, assigned rider info, and live GPS if a rider is currently active on it. rider is null until a bid is accepted; rider_location is null unless the delivery is active and the rider has a live GPS row. 404 if the delivery doesn't exist or isn't yours.

POST/cancel-delivery

Only works while status is open or assigned — 409 otherwise.

{ "delivery_id": "uuid", "reason": "optional string" }

POST/accept-bid

Accepts one bid on an open delivery; every other pending bid is automatically rejected. 409 if the delivery isn't open anymore; 404 if the bid doesn't exist or was already resolved.

{ "delivery_id": "uuid", "bid_id": "uuid" }

No endpoint lists bids on a delivery today — the only way to discover them is the bid.placed webhook. Ask us for a list-bids endpoint if you need a polling fallback.

POST/confirm-pickup

Call when your merchant has physically handed the package to the rider. Only valid while status is awaiting_pickup_confirm.

POST/confirm-receipt

Call when your buyer confirms receipt. Only valid while status is delivered. This is what triggers the rider/company getting paid.

External Carriers

Book a real third-party courier directly on an open delivery, instead of waiting for a bid. Requires a prepaid wallet_balance on your tenant, and that the delivery was created with package_category_id, package_weight_kg, and package_dimension set.

  • POST/tenant-shipbubble-package-options — the carrier's live category/box-size presets.
  • POST/tenant-shipbubble-rates — { delivery_id } → a list of courier quotes.
  • POST/tenant-shipbubble-book — { quote_id } → books it, debits the wallet, delivery becomes assigned.
  • POST/tenant-shipbubble-cancel — { booking_id } → cancels and credits the fee back.

06. Webhooks

If your tenant record has a webhook_url set, Eziza POSTs an event to it whenever something changes. Every request carries:

Content-Type: application/json
X-Eziza-Event: <event name>
X-Eziza-Signature: <hex HMAC-SHA256 of the raw request body>

Verify the signature before trusting the payload — compute HMAC-SHA256 of the exact raw body bytes using your WEBHOOK_SIGNING_SECRET, hex-encode it, and compare with a constant-time comparison:

// Node.js
const crypto = require('crypto');
function isValid(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
No retries. If your endpoint is down or returns a non-2xx, the event is logged internally but not retried. Poll get-delivery periodically as a fallback for anything time-sensitive.

delivery.* — status changed

X-Eziza-Eventstatus
delivery.assignedassigned
delivery.awaiting_pickupawaiting_pickup_confirm
delivery.picked_uppicked_up
delivery.delivereddelivered
delivery.confirmedconfirmed
delivery.cancelledcancelled
{
  "event": "delivery.picked_up",
  "delivery_id": "uuid",
  "external_order_id": "…",
  "external_ref": "…",
  "status": "picked_up",
  "rider_id": "uuid",
  "agreed_price": 2500,
  "timestamp": "2026-07-13T10:12:00.000Z"
}

bid.placed — a rider or company placed a new offer

{
  "event": "bid.placed",
  "delivery_id": "uuid",
  "external_order_id": "…",
  "bid": { "id": "uuid", "bidder_type": "rider", "rider_id": "uuid", "company_id": null, "amount": 2500, "status": "pending" },
  "timestamp": "…"
}

delivery.price_too_low — a fixed-price delivery got no takers

Fired once, 20 minutes after creation, for a fixed_price delivery still open with no one having accepted it. No auto-fallback happens — raising the price (a new create-delivery call) or switching to External Carriers is your decision.

{ "event": "delivery.price_too_low", "delivery_id": "uuid", "external_order_id": "…", "fixed_price": 1500, "timestamp": "…" }

location.updated — live rider GPS

Fired on every GPS update from a rider currently active on one of your deliveries — the same data get-delivery's rider_location field surfaces.

{ "event": "location.updated", "delivery_id": "uuid", "external_order_id": "…", "latitude": 6.5244, "longitude": 3.3792, "updated_at": "…" }

07. Errors

StatusMeaning
400Missing required field(s)
401Missing/invalid/revoked API key, or your tenant is deactivated
404Not found, or exists but belongs to a different tenant
409Valid request, but the delivery isn't in a state that allows this action
500Unexpected error — including a reused external_order_id

All error bodies are { "error": "human-readable message" }.

08. Support

Questions, or requesting a new tenant/key: admin@eziza.com.ng, or visit Support.