POST /v1/bulk-deliveries/{ref}/recipients
POST /v1/bulk-deliveries/{ref}/recipients
| Scope | Idempotency-Key | Operation ID |
|---|---|---|
delivery.create | Required | addBulkRecipients |
Add recipients before the sender confirms, while positions remain.
Each new recipient becomes a child at the next free position with its own link and SMS. A
removed child keeps its position, so a Bulk Delivery has at most five children ever:
beyond that, or beyond the effective maximum of active recipients, BULK_RECIPIENT_LIMIT.
After the sender confirmed, BULK_STATE_CHANGED. A quoted total no longer covers every
child and is withdrawn; the quote revision moves on. Allowed with the module off:
an existing Bulk Delivery continues. Idempotency-Key required. Audited
(bulk.recipient.add).
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. |
ref | path | Yes | Pattern ^BK-[0-9A-HJKMNP-TV-Z]{8}$. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
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
201
Added; one link SMS is owed per new 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.