BulkSMS

BulkSMS API

Send SMS from your application with one HTTP call. JSON in, JSON out.

https://bulksms.oasistech.co.tz/v1 Bearer API key JSON v1.0

Quickstart

Your account needs approved KYC, an approved sender ID and credits. Then:

  1. 1Create an API key in the portal: API keys.
  2. 2Send a message. Examples use our default sender BULK SMS TZ — use your own approved sender ID.
curl -X POST https://bulksms.oasistech.co.tz/v1/sms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: otp-48213-1" \
  -d '{"to": "+255712345678", "sender": "BULK SMS TZ", "message": "Your BulkSMS code is 482913."}'

Authentication

Send your key on every request: Authorization: Bearer YOUR_API_KEY. Keys are shown once, expire after 365 days by default, and can be limited to listed IP addresses.

ScopeAllows
messages:sendSend, schedule and quote — POST /v1/sms. Enough for OTP.
messages:readMessage history — GET /v1/logs
balance:readBalance — GET /v1/balance

/api/... works too and is the same endpoint as /v1/.... There is no /sms/send.

POST/v1/sms

Send SMS

One request sends one message to one or many numbers.

FieldTypeDescription
to requiredstring · arrayA number, an array of numbers, or a comma-separated string. 0712345678 and +255712345678 both work.
sender requiredstringYour approved sender ID, exactly as registered.
message requiredstringThe text. May contain {placeholders}.
paramsobjectValues for placeholders, e.g. {"name": "Amina"}.
scheduled_atISO-8601Send later: 30 seconds to 90 days ahead. Answers 202 scheduled.
dry_runbooleantrue = show the cost, send nothing.

Several recipients, personalised

{
  "to": [
    {"number": "0712345678", "params": {"name": "Amina"}},
    {"number": "0754901234", "params": {"name": "Juma"}}
  ],
  "sender": "BULK SMS TZ",
  "message": "Habari {name}, your order is ready."
}

Responses

HTTPstatusMeaning
200sentAccepted by the network gateway.
202pending · processingGateway slow to confirm — most likely sent. Do not resend.
202scheduledQueued for scheduled_at.
202processing + backgroundOver 10,000 recipients: accepted, going out now. Results in logs and webhooks. Do not resend.
502failed · partialRefused by the gateway. Failed recipients are not charged.

One recipient — 200

{
  "number": "+255712345678",
  "status": "sent",
  "sms_log_id": 123456,
  "credits_used": 1,
  "sms_response": {"msg_id": "1880000001", "number": "255712345678", "str_response": "Send Successful"},
  "provider_ref": "1880000001",
  "credit_info": {"total_recipients": 1, "credits_charged": 1, "balance_after": 6, "sandbox": false}
}

Several recipients: {"status", "sms_log_id", "recipients": [ ...one object per number... ], "credit_info"}. A failed recipient has "status": "failed" and an error.

More than 10,000 recipients — 202

{
  "status": "processing",
  "background": true,
  "sms_log_id": 123460,
  "recipients_count": 12000,
  "background_notice": "Large send accepted: 12,000 recipients are going out now. ...",
  "credit_info": {"total_recipients": 12000, "credits_charged": 12000, "balance_after": 3000, "sandbox": false}
}

Answered at once; the list goes out right after. Follow sms_log_id in GET /v1/logs or with webhooks. Refused numbers are refunded.

Idempotency-Key

Send one with every message, so a retry can never send twice.

HeaderIdempotency-Key (or body field idempotency_key)
Length1–64 characters (maximum 64), visible ASCII, no spaces — e.g. a UUID
Same key, same requestReturns the original response again with "idempotent_replay": true. Nothing is sent or charged twice.
Same key, different request422 — use a new key for every new message
Kept for30 days. After that the key answers 409 (already sent once).
Refused requests400 · 401 · 402 · 403 · 429 do not use up the key — fix and retry with the same key.

Timeout or network error? Retry with the same key. Allow up to 180 seconds for a send.

Errors

Errors return {"error": "..."}, sometimes with a hint. Nothing is sent or charged.

HTTPMeaningWhat to do
400Missing field, invalid number, sender format or key formatFix the request
401Missing, invalid or expired API keyCheck the key
402Not enough creditsTop up
403KYC not approved, sender not approved, scope missing or IP not allowedSee error
409Idempotency-Key already used — the message was sent onceDo not resend
415Missing Content-Type: application/jsonAdd the header
422All numbers opted out, or key reused for a different requestSee error
429Too many requestsWait Retry-After seconds, retry with the same key
503Gateway temporarily unavailableRetry later with the same key
GET/v1/balancebalance:read

Balance

{"credits": 1234}
GET/v1/logs?type=smsmessages:read

Messages

Your sent messages, newest first. Filters: page, limit (max 100), search, status, from, to (YYYY-MM-DD).

{
  "page": 1, "limit": 50, "total": 1, "total_pages": 1,
  "data": [
    {"id": 123456, "recipient": "+255712345678", "sender": "BULK SMS TZ", "message": "Your BulkSMS code is 482913.",
     "status": "sent", "credits_used": 1, "segments": 1, "source": "api", "created_at": "2026-10-08 09:15:02"}
  ]
}

id is the sms_log_id from the send. A send to several numbers is one row; its per-number results are in recipients_json.

GET/v1/sender-ids

Sender IDs

Your sender ID requests. Request new ones in the portal.

{"sender_ids": [{"id": 42, "sender": "BULK SMS TZ", "status": "approved", "live": 1, "created_at": "2026-10-01 10:00:00"}],
 "page": 1, "limit": 25, "total": 1, "total_pages": 1}

Webhooks

Get an HTTP POST on your server when a message is sent or fails.

POST /v1/webhooks?action=create
{"url": "https://example.com/bulksms-hook", "events": "sms.delivered,sms.pending,sms.failed,credits.low"}

→ 201 {"id": 7, "url": "https://example.com/bulksms-hook", "events": "...", "secret": "save-this-secret"}
EventWhen
sms.deliveredA recipient was accepted by the gateway
sms.pendingGateway slow to confirm — most likely sent
sms.failedA recipient was refused (not charged)
credits.lowBalance fell below 500 credits

Payload

{"id": "evt_3f9c1a2b7d4e5f6a7b8c9d0e", "type": "sms.delivered", "created_at": "2026-10-08T09:15:03+03:00", "tenant_id": 101,
 "data": {"sms_log_id": 123456, "recipient": "+255712345678", "sender": "BULK SMS TZ", "status": "sent",
          "credits_used": 1, "provider_ref": "1880000001", "error": null}}

Verify the signature

Header X-BulkSMS-Signature = HMAC-SHA256 (hex) of the raw body, keyed with your secret.

const crypto = require("crypto");
const sig = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.get("X-BulkSMS-Signature") || ""));

Answer 2xx within 10 seconds. Otherwise we retry after 1 min, 5 min, 30 min, 2 h and 12 h. Use X-BulkSMS-Event-Id to skip duplicates.

Limits & cost

Rate limit300 send requests per minute per account. Put many numbers in one request.
Client timeoutAllow 180 seconds for a send
Large sendsOver 10,000 recipients: answered at once (202, background: true), sent right after
Standard text160 characters per SMS part; 153 per part when longer
Unicode70 per part; 63 when longer — emoji, accents, curly quotes ‘ ’ “ ”, dashes – —
Cost1 credit × parts × recipients. dry_run shows it first.

AI assistants

Give your coding assistant (Claude, ChatGPT, Copilot, Cursor) one of these files — they describe the whole API.

https://bulksms.oasistech.co.tz/llms-full.txt

Changelog

  • 2026-10-08 — Over 10,000 recipients: answered at once (202, background: true). Allow 180 s for a send (was 100). Webhooks now go out after the reply.
  • 2026-10-08 — Same Idempotency-Key with a different request → 422. Keys 1–64 characters. Unconfirmed sends answer 202 pending and fire sms.pending. Sends to several numbers return sms_log_id.
  • 2026-09 — A repeated Idempotency-Key returns the original response. Limit raised to 300 requests per minute.