Channels
SMS API reference
POST /v1/sms/send queues one message; POST /v1/sms/bulk queues a promotional recipient set and returns a batch. Set route explicitly: transactional is strictly one recipient per request.
Endpoints
| Method and path | Purpose |
|---|---|
| POST /v1/sms/send | Send to one recipient, or to a small set |
| POST /v1/sms/bulk | Send to an array of recipients and receive a batch id |
| GET /v1/messages | List messages, keyset-paginated |
| GET /v1/messages/{id} | One message with its full status timeline |
| GET /v1/message-batches | List batches |
| GET /v1/message-batches/{id} | One batch |
| GET /v1/sms/sender-ids | List sender IDs and their registration status |
| GET /v1/sms/sender-ids/{id} | One sender ID |
Request fields
| Field | Required | Notes |
|---|---|---|
| sender | Yes | A sender ID approved for this workspace on the route named by route. Up to 11 characters. |
| recipient | Yes* | One Nigerian number. 080..., 80..., 234... and +234... are normalized. |
| recipients | Yes* | An array, or a comma-separated string. Use only with promotional bulk sends. |
| message | Yes | Up to 1000 characters. |
| route | Yes | "transactional" or "promotional". No default. sms_type is a legacy alias. |
| template_id | No | Use a stored template instead of inline text. |
*Supply either recipient or recipients. The single endpoint accepts both; the bulk endpoint expects recipients.
curl -X POST https://api.sendozi.com/v1/sms/send \
-H "Authorization: Bearer $SENDOZI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-48291-otp" \
-d '{
"sender": "Sendozi",
"recipient": "+2348012345678",
"message": "Your Northstar code is [code]. It expires in [minutes] minutes.",
"template_id": "tpl_your_approved_otp_template",
"event_type": "otp",
"event_reference": "login-user_42-attempt_003",
"route": "transactional"
}'curl -X POST https://api.sendozi.com/v1/sms/bulk \
-H "Authorization: Bearer $SENDOZI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-2026-08-21" \
-d '{
"sender": "Sendozi",
"recipients": ["+2348012345678", "+2349012345678", "+2347012345678"],
"message": "Term two resumes on Monday 8 September. Fees are due by Friday.",
"route": "promotional"
}'Choosing a route
Every SMS request must declare route as either "transactional" or "promotional". Sendozi does not default it, because the two travel different routes with different delivery behaviour against the Nigerian DND register.
- "transactional" and its alias "otp" select the corporate route: OTPs, alerts, receipts and service notices, which reach DND-active subscribers.
- "promotional" and its alias "promo" select the marketing route, which the networks filter for DND-active subscribers.
- Anything else returns 400 invalid_request with a resolution naming the two valid values.
- Both routes bill at the same rate. The route affects delivery, not price.
- A sender ID is approved for one route, against the sample message in its request letter. A send whose sms_type does not match its sender ID's route returns 403 sender_id_route_mismatch before the wallet is touched, in both directions.
- Sender IDs created before the routes were separated report sms_route: "unclassified" and cannot send on either route. They return 403 sender_id_unclassified until Sendozi Operations records the route the operator approved, or you resubmit under the correct one.
- Transactional API requests accept exactly one submitted recipient. An array, comma-separated list, or a duplicated number returns 400 transactional_bulk_not_allowed before any wallet reservation or provider dispatch.
The transactional versus promotional guide works through the grey cases; the DND guide explains the register itself.
Recipient format
Nigerian mobile numbers only. Sendozi accepts 08012345678, 8012345678, 2348012345678 and +2348012345678, normalizes each to E.164, and rejects invalid or international numbers.
Cost and pages
SMS is billed per page, resolved from the character count of the whole message, at ₦7.00 per page. The response carries customer_price_kobo so you always know what a send cost - including in sandbox, where it is reported but not charged.
| Characters | Pages |
|---|---|
| 1 to 160 | 1 |
| 161 to 306 | 2 |
| 307 to 459 | 3 |
| 460 to 612 | 4 |
| 613 to 765 | 5 |
| 766 to 918 | 6 |
| 919 to 1000 | 7 |
Message response
{
"success": true,
"data": {
"message_id": "msg_1f4c0a2b",
"channel": "sms",
"mode": "production",
"sender": "Sendozi",
"recipient": "+2348012345678",
"sms_type": "transactional",
"status": "submitted",
"status_timeline": [
{ "status": "accepted", "at": "2026-08-21T09:14:20.004Z" },
{ "status": "submitted", "at": "2026-08-21T09:14:21.882Z" }
],
"request_id": "req_8ecdcb4ce2ae4290",
"customer_price_kobo": 700,
"created_at": "2026-08-21T09:14:20.004Z",
"sent_at": "2026-08-21T09:14:21.882Z",
"delivered_at": null
},
"request_id": "req_8ecdcb4ce2ae4290"
}status moves from accepted to submitted to delivered or failed. Read delivery reports and webhooks before building anything that reacts to it.
Sender IDs
GET /v1/sms/sender-ids returns the sender IDs registered for your workspace with their status per network. A production send using an unapproved sender is refused with sender_id_not_approved rather than being rerouted under another name. Registration itself happens in the Console; see the sender ID guide.
What can stop a production send
Production sends run thirteen checks in a fixed order and return the first failure without charging. Sandbox sends run the first four only.
- Platform maintenance mode - provider_not_configured
- Recipients valid for the channel - invalid_recipient
- Content passes policy checks - message_policy_violation
- No recipient is suppressed - recipient_suppressed
- API key is active - api_key_blocked
- Workspace is active - workspace_suspended
- KYC approved and production access granted - kyc_required
- Channel active for the workspace - channel_not_active
- Sender ID approved - sender_id_not_approved
- Wallet exists and is not frozen - wallet_frozen
- Balance covers the estimated cost - insufficient_balance
- Provider available - provider_not_configured
Frequently asked questions
- How do I send bulk SMS through the Sendozi API?
- POST to /v1/sms/bulk with a sender, a recipients array of +234 numbers, the message and an sms_type. The response returns a batch you can follow with GET /v1/message-batches/{id}.
- What is the maximum SMS length?
- 1,000 characters in a single send. Longer messages are rejected rather than truncated.
- How many recipients can one bulk request take?
- The request takes an array or a comma-separated string; practical size is governed by the 300-requests-per-minute workspace rate limit and your wallet balance. Split very large campaigns into batches with distinct idempotency keys.
- Can I send SMS to a number outside Nigeria?
- No. The SMS API validates recipients as Nigerian mobile numbers. Anything else returns invalid_recipient.
- Is transactional SMS priced differently from promotional SMS?
- No. Both are ₦7.00 per page. sms_type selects the route, not the rate.
Related reading
Getting started
Quickstart: send your first SMS
Get an API key, send a sandbox SMS, read the response envelope, register a delivery webhook and move to production. A complete first integration in one page.
Getting started
Authentication and API keys
How to authenticate Sendozi API requests, the difference between sandbox and production keys, how keys are stored, and how to rotate a key without downtime.
Reliability
Idempotency
How Idempotency-Key works on Sendozi send endpoints, what happens on a repeat with the same or a different body, how long keys are retained, and how to choose a key.
Reliability
Delivery webhooks
Register a webhook endpoint, receive message.delivered and message.failed events, verify the signature over the raw body, and build a receiver that survives retries and reordering.
Reliability
Errors and the response envelope
The Sendozi response envelope, every error code with its status and meaning, which errors are worth retrying, and how request_id is used to trace a failure.