POST /v1/bulk-deliveries
POST /v1/bulk-deliveries
| Scope | Idempotency-Key | Operation ID |
|---|---|---|
delivery.create | Required | createBulkDelivery |
Create a Bulk Delivery, one parent with two to five children.
One pickup, one sender, one vehicle and two to five recipients, each becoming an ordinary
delivery request with its own DX- reference and its own secure link, sent by SMS once the
creation commits. Each recipient supplies their own drop-off: there is no
destination field. A count outside the effective limits is BULK_RECIPIENT_LIMIT and
nothing is created; a phone already among the recipients is VALIDATION_FAILED
with detail.reason: duplicate_phone unless the tenant allows duplicates. The module off,
for the platform or the tenant, is MODULE_DISABLED. The partner must be
available to the tenant (NO_ELIGIBLE_PARTNER, PARTNER_BLOCKED); the pickup pin follows
the outcomes; the vehicle must be offered for the pickup (VEHICLE_UNAVAILABLE).
Nothing is priced or dispatched. Idempotency-Key required: a repeated key replays the
parent with 200. Audited (bulk.create).
Request
Signed with your API credential: see Authentication.
| Parameter | In | Required | Description |
|---|---|---|---|
Idempotency-Key | header | Yes | One key per user intent, reused for every retry of that intent. See Idempotency. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
partner | string | Yes | A partner reference available to the tenant (GET /v1/partners) |
sender | object | Yes | |
sender.name | string | Yes | At most 100 characters. |
sender.phone | string | Yes | At most 32 characters. |
pickup | object | Yes | |
pickup.pin | object | Yes | |
pickup.pin.latitude | number | Yes | WGS84; precision is judged as sent |
pickup.pin.longitude | number | Yes | |
pickup.pin.accuracy_m | number | No | |
pickup.ghanapost_reference | string | No | |
pickup.address_description | string | No | |
pickup.landmark | string | No | |
vehicle_type | string | Yes | A catalogue code the partner offers for the pickup |
service_type | string | No | STANDARD when absent One of: EXPRESS, PRIORITY, STANDARD, NEXT_DAY. |
note | string | No | At most 280 characters. |
recipients | array of object | Yes | |
recipients[].first_name | string | Yes | At most 100 characters. |
recipients[].last_name | string | Yes | At most 100 characters. |
recipients[].phone | string | Yes | Normalised to E.164 by the server At most 32 characters. |
recipients[].email | string | No | At most 254 characters. |
Responses
200
An earlier creation with this key, replayed; nothing changed.
| Field | Type | Required | Description |
|---|---|---|---|
bulk_reference | string | Yes | Pattern ^BK-[0-9A-HJKMNP-TV-Z]{8}$. |
status | string | Yes | One of: BULK_DRAFT, BULK_AWAITING_RECIPIENTS, BULK_PARTIALLY_CONFIRMED, BULK_READY_FOR_QUOTE, BULK_AWAITING_SENDER_CONFIRMATION, BULK_DISPATCHING, BULK_IN_PROGRESS, BULK_PARTIALLY_COMPLETED, BULK_COMPLETED, BULK_CANCELLED, BULK_EXCEPTION. |
recipient_count | integer | Yes | 2–5. |
active_recipient_count | integer | Yes | 0–5. |
confirmed_count | integer | Yes | 0–5. |
quote_revision | integer | Yes | |
total | string or null | Yes | The sum of the children's quotes, null until every active child is quoted. |
currency | string | Yes | One of: GHS. |
children | array of object | Yes | |
children[].position | integer | Yes | 1–5. |
children[].delivery_reference | string | Yes | Public reference of a delivery job or Bulk child. Pattern ^DX-[0-9A-HJKMNP-TV-Z]{8}$. |
children[].status | string | Yes | The child request's status (request-level states; REMOVED once removed) |
children[].location_confirmed | boolean | Yes | The recipient confirmed the drop-off |
children[].recipient | object | Yes | |
children[].recipient.first_name | string | Yes | |
children[].recipient.last_name | string | Yes | |
children[].recipient.phone | string | Yes | Masked: the last three digits only |
children[].link_expires_at | string or null, date-time | Yes | When the child's live link stops working; null once revoked |
children[].resends_remaining | integer or null | Yes | Resends left while the recipient is awaited, else null |
children[].resend_available_at | string or null, date-time | Yes | When the resend cooldown ends while the recipient is awaited, else null |
children[].quote | object or null | Yes | The child's own quote: the server's total only, never its arithmetic. expired is judged by the server clock. Null before the child is quoted. |
children[].quote.reference | string | Yes | Pattern ^QT-[0-9A-HJKMNP-TV-Z]{8}$. |
children[].quote.total | string | Yes | Exact decimal with two places; never a JSON number. Pattern ^-?[0-9]+\.[0-9]{2}$. |
children[].quote.currency | string | Yes | One of: GHS. |
children[].quote.expires_at | string, date-time | Yes | |
children[].quote.expired | boolean | Yes | |
children[].customer_status | string or null | Yes | The child's delivery status as tracking shows it, once dispatched |
children[].tracking_links | object | No | The child's tracking links, shown once on the tenant's confirming response: the sender's link confirms the handover |
children[].tracking_links.sender_url | string | Yes | |
children[].tracking_links.recipient_url | string | Yes | |
children[].tracking_links.expires_at | string, date-time | Yes | |
cancellations | array of object | No | After dispatch, each child's answer to the cancellation; present only on that answer. |
cancellations[].delivery_reference | string | Yes | Public reference of a delivery job or Bulk child. Pattern ^DX-[0-9A-HJKMNP-TV-Z]{8}$. |
cancellations[].cancelled | boolean | Yes | |
cancellations[].reason | string or null | Yes | Why the stage policy left it, e.g. after_pickup |
created_at | string, date-time | Yes |
201
Created; one link SMS is owed per child.
| Field | Type | Required | Description |
|---|---|---|---|
bulk_reference | string | Yes | Pattern ^BK-[0-9A-HJKMNP-TV-Z]{8}$. |
status | string | Yes | One of: BULK_DRAFT, BULK_AWAITING_RECIPIENTS, BULK_PARTIALLY_CONFIRMED, BULK_READY_FOR_QUOTE, BULK_AWAITING_SENDER_CONFIRMATION, BULK_DISPATCHING, BULK_IN_PROGRESS, BULK_PARTIALLY_COMPLETED, BULK_COMPLETED, BULK_CANCELLED, BULK_EXCEPTION. |
recipient_count | integer | Yes | 2–5. |
active_recipient_count | integer | Yes | 0–5. |
confirmed_count | integer | Yes | 0–5. |
quote_revision | integer | Yes | |
total | string or null | Yes | The sum of the children's quotes, null until every active child is quoted. |
currency | string | Yes | One of: GHS. |
children | array of object | Yes | |
children[].position | integer | Yes | 1–5. |
children[].delivery_reference | string | Yes | Public reference of a delivery job or Bulk child. Pattern ^DX-[0-9A-HJKMNP-TV-Z]{8}$. |
children[].status | string | Yes | The child request's status (request-level states; REMOVED once removed) |
children[].location_confirmed | boolean | Yes | The recipient confirmed the drop-off |
children[].recipient | object | Yes | |
children[].recipient.first_name | string | Yes | |
children[].recipient.last_name | string | Yes | |
children[].recipient.phone | string | Yes | Masked: the last three digits only |
children[].link_expires_at | string or null, date-time | Yes | When the child's live link stops working; null once revoked |
children[].resends_remaining | integer or null | Yes | Resends left while the recipient is awaited, else null |
children[].resend_available_at | string or null, date-time | Yes | When the resend cooldown ends while the recipient is awaited, else null |
children[].quote | object or null | Yes | The child's own quote: the server's total only, never its arithmetic. expired is judged by the server clock. Null before the child is quoted. |
children[].quote.reference | string | Yes | Pattern ^QT-[0-9A-HJKMNP-TV-Z]{8}$. |
children[].quote.total | string | Yes | Exact decimal with two places; never a JSON number. Pattern ^-?[0-9]+\.[0-9]{2}$. |
children[].quote.currency | string | Yes | One of: GHS. |
children[].quote.expires_at | string, date-time | Yes | |
children[].quote.expired | boolean | Yes | |
children[].customer_status | string or null | Yes | The child's delivery status as tracking shows it, once dispatched |
children[].tracking_links | object | No | The child's tracking links, shown once on the tenant's confirming response: the sender's link confirms the handover |
children[].tracking_links.sender_url | string | Yes | |
children[].tracking_links.recipient_url | string | Yes | |
children[].tracking_links.expires_at | string, date-time | Yes | |
cancellations | array of object | No | After dispatch, each child's answer to the cancellation; present only on that answer. |
cancellations[].delivery_reference | string | Yes | Public reference of a delivery job or Bulk child. Pattern ^DX-[0-9A-HJKMNP-TV-Z]{8}$. |
cancellations[].cancelled | boolean | Yes | |
cancellations[].reason | string or null | Yes | Why the stage policy left it, e.g. after_pickup |
created_at | string, date-time | Yes |
400
Structural validation failed or an Idempotency-Key is missing.
The error envelope: code, message, detail, request_id.
401
Unknown, revoked or wrong-environment credential, bad signature, or expired session.
The error envelope: code, message, detail, request_id.
403
Tenant access denied (existence is not disclosed), insufficient scope, MFA/step-up required, partner blocked or module disabled.
The error envelope: code, message, detail, request_id.
409
State changed, race lost, or idempotency conflict. Refetch before deciding again.
The error envelope: code, message, detail, request_id.
422
Semantically invalid (location, vehicle, server-controlled field, bulk limits).
The error envelope: code, message, detail, request_id.
429
Rate limit reached; honour Retry-After.
The error envelope: code, message, detail, request_id.
500
Unexpected failure. Only code and request_id are returned.
The error envelope: code, message, detail, request_id.
503
Pricing/routing unavailable, request timeout, retryable contention, or an operator must act first (ACCOUNTING_PERIOD_UNAVAILABLE, outcome REQUIRES_ADMIN), or the payment provider cannot start a payment (PAYMENT_PROVIDER_UNAVAILABLE, outcome REQUIRES_PAYMENT; nothing charged).
The error envelope: code, message, detail, request_id.