Skip to content
Sendozi

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.

By SendoziUpdated 4 min read

Endpoints

Method and pathPurpose
POST /v1/sms/sendSend to one recipient, or to a small set
POST /v1/sms/bulkSend to an array of recipients and receive a batch id
GET /v1/messagesList messages, keyset-paginated
GET /v1/messages/{id}One message with its full status timeline
GET /v1/message-batchesList batches
GET /v1/message-batches/{id}One batch
GET /v1/sms/sender-idsList sender IDs and their registration status
GET /v1/sms/sender-ids/{id}One sender ID

Request fields

FieldRequiredNotes
senderYesA sender ID approved for this workspace on the route named by route. Up to 11 characters.
recipientYes*One Nigerian number. 080..., 80..., 234... and +234... are normalized.
recipientsYes*An array, or a comma-separated string. Use only with promotional bulk sends.
messageYesUp to 1000 characters.
routeYes"transactional" or "promotional". No default. sms_type is a legacy alias.
template_idNoUse a stored template instead of inline text.

*Supply either recipient or recipients. The single endpoint accepts both; the bulk endpoint expects recipients.

A complete send
Transactional SMS
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"
  }'
Bulk
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.

Page boundaries
CharactersPages
1 to 1601
161 to 3062
307 to 4593
460 to 6124
613 to 7655
766 to 9186
919 to 10007
Page boundaries

Message response

Fields returned for a message
JSON
{
  "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.

  1. Platform maintenance mode - provider_not_configured
  2. Recipients valid for the channel - invalid_recipient
  3. Content passes policy checks - message_policy_violation
  4. No recipient is suppressed - recipient_suppressed
  5. API key is active - api_key_blocked
  6. Workspace is active - workspace_suspended
  7. KYC approved and production access granted - kyc_required
  8. Channel active for the workspace - channel_not_active
  9. Sender ID approved - sender_id_not_approved
  10. Wallet exists and is not frozen - wallet_frozen
  11. Balance covers the estimated cost - insufficient_balance
  12. 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.

Try it in sandbox

A sk_test_ key runs the same validation as a live send, without a provider call or a charge.