Skip to content
Sendozi

Sending

Sending SMS

POST /v1/sms/send or /v1/sms/bulk with a sender, one or more recipients, the message and a route. The send is queued and answered with 202 and a batch: invalid, repeated and opted-out numbers are removed and counted, the cost is reserved, and each recipient is charged as it is handed to the network.

By SendoziUpdated 6 min read

Endpoints

Method and pathScopePurpose
POST /v1/sms/sendmessages:sendSend to one or more recipients
POST /v1/sms/bulkmessages:sendThe same send; the response is marked "type": "bulk"

The two endpoints accept the same body and behave the same way; only the type field of the response differs. Reading status, batches and cancelling are covered in messages and batches.

Request

FieldRequiredNotes
senderYesYour Sender ID, spelt exactly as approved, up to 11 characters. A live send must use a Sender ID approved for the route in the request. Sandbox does not check it.
recipient or recipientsYesOne number in recipient, or several in recipients as a JSON array or a comma-separated string. Up to 1,000,000 per request.
messageYesThe exact text to send. Up to 1000 characters in GSM-7, or 469 if it contains any character outside GSM-7.
routeYes"promotional" or "transactional". "promo" and "otp" are accepted, and sms_type is accepted in place of route. There is no default.
Headers
HeaderRequiredNotes
Authorization: Bearer <key>YesOr X-API-Key. See authentication.
Content-Type: application/jsonYesThe body must be a JSON object, at most 32 MB.
Idempotency-KeyTransactional: yesUp to 255 characters. Strongly recommended on every send. See idempotency.
X-Request-IDNoYour own correlation id, 8 to 64 characters of A-Z, a-z, 0-9, _ or -. Echoed back as request_id.
Headers
A promotional send to three numbers
cURL
curl -X POST https://api.sendozi.com/v1/sms/bulk \
  -H "Authorization: Bearer $SENDOZI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: term-two-reminder-2026-09-24" \
  -d '{
    "sender": "Greenfield",
    "recipients": ["08012345678", "+2349012345678", "2347012345678"],
    "message": "Term two resumes on Monday 5 October. Fees are due by Friday.",
    "route": "promotional"
  }'

Response

A send that is accepted returns 202 with the batch it created. Nothing has been delivered yet: the batch is queued, and its recipients are handed to the network within moments (live promotional sends wait for the delivery window).

202 Accepted
JSON
{
  "success": true,
  "data": {
    "batch_id": "bat_9b1f2a4c6d8e0f13",
    "channel": "sms",
    "mode": "production",
    "status": "queued",
    "requested_count": 3,
    "accepted_count": 3,
    "duplicates_removed": 0,
    "invalid_count": 0,
    "suppressed_count": 0,
    "dnd_skipped_count": 0,
    "consent_rejected_count": 0,
    "estimated_cost_kobo": 2100,
    "reserved_cost_kobo": 2100,
    "progress": { "queued": 3, "processing": 0, "sent": 0, "delivered": 0, "failed": 0, "skipped": 0 },
    "type": "bulk"
  },
  "request_id": "req_6fddf1f66c77787f"
}
FieldMeaning
batch_idThe batch this send created. Follow it with GET /v1/message-batches/{batch_id}.
message_idPresent only when exactly one recipient was accepted. The id the message keeps for life.
statusqueued once every recipient is written to the queue.
requested_countRecipients in the request, before cleaning.
accepted_countRecipients that will be sent to.
duplicates_removed, invalid_count, suppressed_countRecipients removed as repeats, as invalid numbers, or because they opted out of your SMS.
dnd_skipped_countLive promotional sends only: recipients skipped, and not charged, because they returned a Do Not Disturb block to your workspace in the last 90 days.
consent_rejected_countAlways 0. Kept for compatibility.
estimated_cost_koboThe cost of the accepted recipients at the current rate.
reserved_cost_koboThe amount held from the wallet for this send. 0 in sandbox.
progressRecipients by state: queued, processing, sent, delivered, failed and skipped.

How recipients are handled

Nigerian mobile numbers only. Each number is normalised to +234 form, then invalid numbers, repeats and opted-out numbers are removed and counted. The send goes ahead for everyone left; it is refused only if nobody is left.

  • Accepted spellings: 08012345678, 8012345678, 2348012345678, +2348012345678, 002348012345678, and +234 0803... with the zero kept after the country code. Spaces, dashes and brackets are ignored. 0700 and 0800 numbers are not mobiles and are counted as invalid.
  • A valid number is a Nigerian mobile number on a 070, 071, 080, 081, 090 or 091 prefix. Anything else, including international numbers, is counted in invalid_count.
  • A number that appears twice, in any spelling, is sent once and counted in duplicates_removed.
  • A number that has opted out of SMS from your workspace is removed and counted in suppressed_count.
  • On a live promotional send, a number that returned a Do Not Disturb block to your workspace in the last 90 days is skipped and counted in dnd_skipped_count. The message that met the block was charged; the repeats are not sent.
  • If no valid number is left, the send returns 400 invalid_recipient; if every valid number has opted out, 400 recipient_suppressed.

Routes and Sender IDs

  • promotional is for campaigns, offers and announcements to your own audience. The Console sends promotional SMS only.
  • transactional is for one person's own event: an OTP, a payment alert, a receipt. Through the API it takes exactly one recipient and an Idempotency-Key; the text is your own. See transactional SMS.
  • A Sender ID is approved for one route. A transactional approval also sends promotional, except to Airtel until Airtel whitelists it: those numbers are left out and counted in sender_network_skipped_count. A promotional-only Sender ID used for transactional returns 403 sender_id_route_mismatch; one that is not approved at all returns 403 sender_id_not_approved.
  • A Sender ID created before the routes were separated reports sms_route "unclassified" and returns 403 sender_id_unclassified until Sendozi records its route.
  • Both routes cost the same.

Message length and pages

SMS is billed at ₦7.00 per page per recipient. A page is one message on the network: 160 characters in GSM-7, or 70 if the text contains any character outside GSM-7 - an emoji, a curly quote, a tone-marked letter. GSM-7 extension characters (^ { } \ [ ~ ] | €) count as two, and an emoji counts as two in Unicode.

Page boundaries
PagesGSM-7 charactersUnicode characters
11 to 1601 to 70
2161 to 30671 to 134
3307 to 459135 to 201
4460 to 612202 to 268
5613 to 765269 to 335
6766 to 918336 to 402
7919 to 1000403 to 469
Page boundaries

A longer message is refused with 400 invalid_request, never truncated. Messages are also checked against a short list of high-risk phrases; a match returns 400 message_policy_violation.

How a send is charged

  1. 1

    Reserved on acceptance

    A live send reserves estimated_cost_kobo from the wallet when it is accepted. If the balance does not cover it, the send returns 403 insufficient_balance and nothing is queued.

  2. 2

    Charged per recipient

    Each recipient is charged its price when it is handed to the network, and charged even if the network later reports it undelivered.

  3. 3

    Not charged

    A recipient refused before it reached the network, skipped because the number opted out after the send was accepted, or cancelled is not charged.

  4. 4

    Released

    When the batch finishes or is cancelled, whatever part of the reservation was not charged goes back to the wallet. actual_cost_kobo on the batch is the final charge.

Large sends

  • One request carries up to 1,000,000 recipients and at most 32 MB. A larger audience is several requests, each with its own Idempotency-Key.
  • A large request takes longer to accept, because every recipient is checked and written before the batch starts. Allow a client timeout of a few minutes, and retry with the same Idempotency-Key if it times out.
  • Recipients are handed to the network at a steady platform-wide rate, and every workspace's batches share it fairly, so a large campaign does not hold up another customer's send and your transactional SMS is always sent first.

Frequently asked questions

How do I send bulk SMS through the Sendozi API?
POST to /v1/sms/bulk (or /v1/sms/send) with a sender, a recipients array, the message and route: "promotional". The response returns a batch_id; follow it with GET /v1/message-batches/{id} and list its recipients with GET /v1/message-batches/{id}/recipients.
What is the maximum SMS length?
1000 characters in GSM-7, or 469 if the message contains any character outside GSM-7. Longer messages are rejected rather than truncated.
How many recipients can one request take?
Up to 1,000,000, in a body of at most 32 MB.
Can I send SMS to a number outside Nigeria?
No. Recipients must be Nigerian mobile numbers. Anything else is removed and counted in invalid_count.
Is transactional SMS priced differently from promotional SMS?
No. Both are ₦7.00 per page. The route decides how a message travels, not what it costs.

Try it in sandbox

A sk_test_ key validates every request without sending or charging anything.