Integration reference
Partner API
Quote, dispatch and track your deliveries with Bento.
Overview
Quote, book and track parcel deliveries from your backend. Authenticate API requests with a credential issued by Bento.
Choose the base URL for your environment. Endpoint paths such as /quotes are appended to this base. Examples use sample data. Replace locations and amounts with values from your sandbox configuration and quote.
| Environment | Base URL |
|---|
| Sandbox | https://api.bentodelivaz.com/sandbox/v1 |
| Production | https://api.bentodelivaz.com/prod/v1 |
| Local testing | http://bentoadmin.test/api/partner/v1 |
Amounts use integer kobo: 100000 represents NGN 1,000. Prices come from your approved tariff. Coordinates are decimal latitude/longitude; timestamps use RFC 3339 UTC. Pickup and destination must be within your configured service coverage.
Authentication
Send Authorization: Bearer <issued-key> and Content-Type: application/json. Keep the key on your backend, never in a browser or customer app. API keys and webhook signing secrets are separate. Sandbox and production credentials are not interchangeable.
Each endpoint lists its required scope. Keys can expire, be revoked or be rotated. Suspending bookings can leave existing delivery lookup and tracking available. No credential is required to read this documentation.
Quick start
- Obtain sandbox access from Bento. Isolation validation, basic parcel-service configuration and a technical contact are required; production contract/UAT/finance approvals are not sandbox gates.
- Request a quote. Bento calculates the route distance and applies your tariff. Quotes expire after 10 minutes; routing failures do not fall back to straight-line pricing.
- Book with the quote ID, a unique external reference, contacts and the unchanged shipment. Keep the same Idempotency-Key (8–128 characters) and body when retrying a timed-out request.
- Signal
/ready only after the vendor has packaged the parcel. Booking alone does not dispatch a rider. Repeated readiness calls do not start another dispatch.
- Track signed callbacks or retrieve the delivery snapshot. Pickup exposes
in_transit with picked_up_at. Delivery requires recipient OTP; a photo is optional.
Set BENTO_BASE_URL to one of the complete base URLs above (no trailing slash), and BENTO_API_KEY to your server-side credential. For example, sandbox quoting uses https://api.bentodelivaz.com/sandbox/v1/quotes. Run the example requests from your development environment.
API v1 · 5 operations
Endpoint reference
Append these paths to your selected environment base URL.
Calculate a server-routed expiring B2B quote
Required scope: quotes:create.
Request body fields
| Field | Type | Required | Details |
shipment | object | Yes | — |
shipment.pickup | object | Yes | — |
shipment.pickup.address | string | Yes | maxLength: 500. |
shipment.pickup.latitude | number | Yes | minimum: -90. maximum: 90. |
shipment.pickup.longitude | number | Yes | minimum: -180. maximum: 180. |
shipment.pickup.name | string | No | maxLength: 120. |
shipment.pickup.phone | string | No | maxLength: 32. |
shipment.recipient | object | Yes | — |
shipment.recipient.address | string | Yes | maxLength: 500. |
shipment.recipient.latitude | number | Yes | minimum: -90. maximum: 90. |
shipment.recipient.longitude | number | Yes | minimum: -180. maximum: 180. |
shipment.recipient.name | string | No | maxLength: 120. |
shipment.recipient.phone | string | No | maxLength: 32. |
shipment.parcel | object | Yes | — |
shipment.parcel.category_id | integer | No | minimum: 1. |
shipment.parcel.weight_kg | number | No | — |
shipment.parcel.length_cm | number | No | — |
shipment.parcel.width_cm | number | No | — |
shipment.parcel.height_cm | number | No | — |
shipment.parcel.instructions | string | null | No | maxLength: 1000. |
Example request cURL
curl --request POST "$BENTO_BASE_URL/quotes" \
--header "Authorization: Bearer $BENTO_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"shipment": {
"pickup": {
"address": "Example pickup address",
"latitude": 9.0765,
"longitude": 7.3986,
"name": "Example sender",
"phone": "+2340000000000"
},
"recipient": {
"address": "Example delivery address",
"latitude": 9.065,
"longitude": 7.42,
"name": "Example recipient",
"phone": "+2340000000001"
},
"parcel": {
"instructions": "Example shipment; replace with eligible test data"
}
}
}'
Example response 201 · JSON
{
"id": "00000000-0000-4000-8000-000000000001",
"distance_metres": 4200,
"amount_kobo": 100000,
"currency": "NGN",
"tariff_version": "example-v1",
"breakdown": [
{
"name": "delivery",
"amount_kobo": 100000
}
],
"expires_at": "2030-01-01T10:10:00Z"
}
Response fields
| Field | Type | Details |
id | string | format: uuid. |
distance_metres | integer | minimum: 0. |
amount_kobo | integer | minimum: 0. |
currency | string | Value: NGN. |
tariff_version | string | — |
breakdown | array | — |
expires_at | string | format: date-time. |
Book once from a quote; wait for readiness
Required scope: deliveries:create.
Parameters
| Name | Location | Required | Details |
|---|
Idempotency-Key | header | Yes | Same key and same body returns original creation response; changed body conflicts. |
Request body fields
| Field | Type | Required | Details |
quote_id | string | Yes | format: uuid. |
external_reference | string | Yes | maxLength: 128. |
shipment | object | Yes | — |
shipment.pickup | object | Yes | — |
shipment.pickup.address | string | Yes | maxLength: 500. |
shipment.pickup.latitude | number | Yes | minimum: -90. maximum: 90. |
shipment.pickup.longitude | number | Yes | minimum: -180. maximum: 180. |
shipment.pickup.name | string | Yes | maxLength: 120. |
shipment.pickup.phone | string | Yes | maxLength: 32. |
shipment.recipient | object | Yes | — |
shipment.recipient.address | string | Yes | maxLength: 500. |
shipment.recipient.latitude | number | Yes | minimum: -90. maximum: 90. |
shipment.recipient.longitude | number | Yes | minimum: -180. maximum: 180. |
shipment.recipient.name | string | Yes | maxLength: 120. |
shipment.recipient.phone | string | Yes | maxLength: 32. |
shipment.parcel | object | Yes | — |
shipment.parcel.category_id | integer | No | minimum: 1. |
shipment.parcel.weight_kg | number | No | — |
shipment.parcel.length_cm | number | No | — |
shipment.parcel.width_cm | number | No | — |
shipment.parcel.height_cm | number | No | — |
shipment.parcel.instructions | string | null | No | maxLength: 1000. |
Example request cURL
curl --request POST "$BENTO_BASE_URL/deliveries" \
--header "Authorization: Bearer $BENTO_API_KEY" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: partner-booking-001' \
--data '{
"quote_id": "00000000-0000-4000-8000-000000000001",
"external_reference": "PARTNER-ORDER-001",
"shipment": {
"pickup": {
"address": "Example pickup address",
"latitude": 9.0765,
"longitude": 7.3986,
"name": "Example sender",
"phone": "+2340000000000"
},
"recipient": {
"address": "Example delivery address",
"latitude": 9.065,
"longitude": 7.42,
"name": "Example recipient",
"phone": "+2340000000001"
},
"parcel": {
"instructions": "Example shipment; replace with eligible test data"
}
}
}'
Example response 201 · JSON
{
"id": "00000000-0000-4000-8000-000000000002",
"external_reference": "PARTNER-ORDER-001",
"status": "accepted",
"readiness_status": "awaiting_ready",
"amount_kobo": 100000,
"currency": "NGN",
"tariff_version": "example-v1",
"version": 1,
"created_at": "2030-01-01T10:00:00Z",
"ready_at": null,
"assigned_at": null,
"picked_up_at": null,
"delivered_at": null,
"canceled_at": null,
"failure_reason": null,
"proof_of_delivery": null,
"lookup_url": "https://api.bentodelivaz.com/sandbox/v1/deliveries/00000000-0000-4000-8000-000000000002"
}
Response fields
| Field | Type | Details |
id | string | format: uuid. |
external_reference | string | — |
status | string | Values: accepted, assigned, in_transit, delivered, canceled, dispatch_failed. |
readiness_status | string | Values: awaiting_ready, ready. |
amount_kobo | integer | — |
currency | string | Value: NGN. |
tariff_version | string | — |
version | integer | — |
created_at | string | null | format: date-time. |
ready_at | string | null | format: date-time. |
assigned_at | string | null | format: date-time. |
picked_up_at | string | null | format: date-time. |
delivered_at | string | null | format: date-time. |
canceled_at | string | null | format: date-time. |
failure_reason | string | null | — |
proof_of_delivery | object | null | — |
proof_of_delivery.method | string | Value: otp. |
proof_of_delivery.verified | boolean | — |
proof_of_delivery.verified_at | string | format: date-time. |
proof_of_delivery.photo_available | boolean | — |
lookup_url | string | — |
POST/deliveries/{id}/ready
#
Signal vendor readiness and initiate dispatch idempotently
Required scope: deliveries:ready.
Parameters
| Name | Location | Required | Details |
|---|
id | path | Yes | Public delivery UUID owned by this partner. |
Example request cURL
curl --request POST "$BENTO_BASE_URL/deliveries/$DELIVERY_ID/ready" \
--header "Authorization: Bearer $BENTO_API_KEY" \
--header 'Content-Type: application/json'
Example response 200 · JSON
{
"id": "00000000-0000-4000-8000-000000000002",
"external_reference": "PARTNER-ORDER-001",
"status": "accepted",
"readiness_status": "ready",
"amount_kobo": 100000,
"currency": "NGN",
"tariff_version": "example-v1",
"version": 2,
"created_at": "2030-01-01T10:00:00Z",
"ready_at": "2030-01-01T10:02:00Z",
"assigned_at": null,
"picked_up_at": null,
"delivered_at": null,
"canceled_at": null,
"failure_reason": null,
"proof_of_delivery": null,
"lookup_url": "https://api.bentodelivaz.com/sandbox/v1/deliveries/00000000-0000-4000-8000-000000000002"
}
Response fields
| Field | Type | Details |
id | string | format: uuid. |
external_reference | string | — |
status | string | Values: accepted, assigned, in_transit, delivered, canceled, dispatch_failed. |
readiness_status | string | Values: awaiting_ready, ready. |
amount_kobo | integer | — |
currency | string | Value: NGN. |
tariff_version | string | — |
version | integer | — |
created_at | string | null | format: date-time. |
ready_at | string | null | format: date-time. |
assigned_at | string | null | format: date-time. |
picked_up_at | string | null | format: date-time. |
delivered_at | string | null | format: date-time. |
canceled_at | string | null | format: date-time. |
failure_reason | string | null | — |
proof_of_delivery | object | null | — |
proof_of_delivery.method | string | Value: otp. |
proof_of_delivery.verified | boolean | — |
proof_of_delivery.verified_at | string | format: date-time. |
proof_of_delivery.photo_available | boolean | — |
lookup_url | string | — |
Retrieve the current owned delivery snapshot
Required scope: deliveries:read.
Parameters
| Name | Location | Required | Details |
|---|
id | path | Yes | Public delivery UUID owned by this partner. |
Example request cURL
curl --request GET "$BENTO_BASE_URL/deliveries/$DELIVERY_ID" \
--header "Authorization: Bearer $BENTO_API_KEY"
Example response 200 · JSON
{
"id": "00000000-0000-4000-8000-000000000002",
"external_reference": "PARTNER-ORDER-001",
"status": "in_transit",
"readiness_status": "ready",
"amount_kobo": 100000,
"currency": "NGN",
"tariff_version": "example-v1",
"version": 4,
"created_at": "2030-01-01T10:00:00Z",
"ready_at": "2030-01-01T10:02:00Z",
"assigned_at": "2030-01-01T10:04:00Z",
"picked_up_at": "2030-01-01T10:15:00Z",
"delivered_at": null,
"canceled_at": null,
"failure_reason": null,
"proof_of_delivery": null,
"lookup_url": "https://api.bentodelivaz.com/sandbox/v1/deliveries/00000000-0000-4000-8000-000000000002"
}
Response fields
| Field | Type | Details |
id | string | format: uuid. |
external_reference | string | — |
status | string | Values: accepted, assigned, in_transit, delivered, canceled, dispatch_failed. |
readiness_status | string | Values: awaiting_ready, ready. |
amount_kobo | integer | — |
currency | string | Value: NGN. |
tariff_version | string | — |
version | integer | — |
created_at | string | null | format: date-time. |
ready_at | string | null | format: date-time. |
assigned_at | string | null | format: date-time. |
picked_up_at | string | null | format: date-time. |
delivered_at | string | null | format: date-time. |
canceled_at | string | null | format: date-time. |
failure_reason | string | null | — |
proof_of_delivery | object | null | — |
proof_of_delivery.method | string | Value: otp. |
proof_of_delivery.verified | boolean | — |
proof_of_delivery.verified_at | string | format: date-time. |
proof_of_delivery.photo_available | boolean | — |
lookup_url | string | — |
GET/deliveries/{id}/location
#
Retrieve the current assigned rider last-reported position
Required scope: location:read.
Parameters
| Name | Location | Required | Details |
|---|
id | path | Yes | Public delivery UUID owned by this partner. |
Example request cURL
curl --request GET "$BENTO_BASE_URL/deliveries/$DELIVERY_ID/location" \
--header "Authorization: Bearer $BENTO_API_KEY"
Example response 200 · JSON
{
"delivery_id": "00000000-0000-4000-8000-000000000002",
"availability": "available",
"location": {
"latitude": 9.07,
"longitude": 7.41,
"observed_at": "2030-01-01T10:16:00Z"
},
"as_of": "2030-01-01T10:16:10Z",
"age_seconds": 10,
"is_stale": false,
"reason": null
}
Response fields
| Field | Type | Details |
delivery_id | string | format: uuid. |
availability | string | Values: available, not_available, tracking_ended. |
location | object | null | — |
location.latitude | number | — |
location.longitude | number | — |
location.observed_at | string | format: date-time. |
as_of | string | format: date-time. |
age_seconds | integer | null | — |
is_stale | boolean | — |
reason | string | null | — |
Status and tracking
Statuses are accepted, assigned, in_transit, delivered, canceled and dispatch_failed. Readiness is separate: awaiting_ready or ready. Bookings are accepted before a rider is assigned. Use the lookup response's version and timestamps for reconciliation.
A rider rejection can return a delivery to accepted for reassignment; it does not necessarily cancel the shipment. When assignment cannot complete before the configured deadline, dispatch_failed is reported. Cancellation, return and redelivery arrangements follow your agreed service policy.
Location is available only for the current assigned rider during active execution. The response includes observed_at, age_seconds, is_stale and availability. Show stale or unavailable reports honestly. Tracking ends on delivery, cancellation or dispatch failure; a former rider's location is not exposed after reassignment. Keep your partner key on your server and authorize your own customer's access to any map.
Polling and freshness limits are set during onboarding. Suggested starting values are 15-second polling and 60-second staleness, subject to validation. Respect rate limits and Retry-After.
Webhooks
Bento posts signed JSON to your provisioned HTTPS receiver. Callback delivery is at least once: duplicates and out-of-order events are possible. Deduplicate by event_id, compare per-delivery version, and use lookup after gaps. Persist an accepted event before returning any 2xx response.
| Header |
Meaning |
X-Bento-Event-Id |
Stable event identifier |
X-Bento-Timestamp |
Unix seconds at signing time; refreshed on retry |
X-Bento-Key-Id |
Select the separately supplied verification secret |
X-Bento-Signature |
sha256= plus the hexadecimal HMAC-SHA256 signature |
Sign/verify timestamp + '.' + exact raw request body. Use constant-time comparison and a bounded timestamp tolerance, for example five minutes. Verify the raw bytes before JSON parsing; do not reserialize parsed JSON to reconstruct a body.
Events include delivery.accepted, delivery.ready, delivery.assigned, delivery.in_transit, delivery.delivered, delivery.canceled and delivery.dispatch_failed. Onboarding sends a signed connection_test with event_id, occurred_at and test=true; acknowledge it without creating a shipment or expecting delivery/version fields.
Sandbox quote/booking tests may start without a callback. Status events remain queued without spending retries until the URL, signing secret and validation are ready. After validation, older queued milestones may arrive; their original IDs, occurrence times and versions remain unchanged. Failed callbacks use bounded retries; Bento can replay exhausted events. Agree secret rotation and install a staged replacement before Bento switches signing keys.
Download the Node.js verification example below. Raw OTPs are not part of partner tracking responses or webhook payloads.
Errors and retries
Errors have the shape:
{
"error": {
"code": "quote_expired",
"message": "Quote expired",
"request_id": "example-request-id"
}
}
| HTTP status |
Action |
| 400 |
Use the approved HTTPS API endpoint. |
| 401 |
Check credential expiry/revocation and environment. |
| 403 |
Check endpoint scope or booking suspension. |
| 404 |
Resource missing or not owned by your partner. |
| 409 |
Resolve an idempotency/reference conflict or invalid transition; do not create a second booking to bypass it. |
| 422 |
Correct shipment data or obtain a fresh valid quote. |
| 429 |
Wait for Retry-After and reduce request frequency. |
| 503 |
Temporary dependency/configuration failure; preserve booking key/body when retrying. |
Quote expiry, changed shipment inputs and a superseded unused tariff can require a new quote. Accepted booking prices remain fixed. Replaying an identical creation request returns its original response, even after quote expiry; use lookup for the current state.
The JSON error request_id and response X-Request-Id support troubleshooting. Share identifiers with your technical contact, never secret values or recipient OTPs.
Integration toolkit
Downloads
Import the collection, inspect the schemas or add signature verification to your receiver.