BulkSMS

API reference

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

v1.0 https://bulksms.oasistech.co.tz

Quickstart

Send your first SMS in three steps.

  1. 1Sign in and create an API key at /api-keys.
  2. 2Get one of your sender IDs approved.
  3. 3Call POST /api/sms with your message.
$ curl -X POST https://bulksms.oasistech.co.tz/api/sms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+255712345678",
    "sender": "ACME",
    "message": "Habari Mohammed, asante kwa kununua nasi."
  }'

Authentication

Every request needs a Bearer token. Create one in the dashboard under API keys.

Authorization: Bearer YOUR_API_KEY
  • Tokens are scoped per tenant. They never expire unless you set a TTL.
  • Optionally restrict each key to specific IPs or CIDR ranges in the dashboard.
  • Lost a key? Revoke it and create a new one — keys aren't re-shown after creation.

Base URL & versioning

All endpoints accept JSON and return JSON.

Production
https://bulksms.oasistech.co.tz
Versioned alias
https://bulksms.oasistech.co.tz/v1

Use /v1/sms in production. Responses to /v1 URLs include a Sunset header when an endpoint is retired.

POST /api/sms

Send SMS

Send one or many messages in a single call. Per-recipient personalisation is supported via params.

Request body

FieldTypeRequiredNotes
tostring | string[]yesOne phone or an array. E.164 (e.g. +255712345678).
senderstringyesAn approved sender ID for your tenant. 3–11 characters.
messagestringyesUp to 1600 chars. Supports {placeholder}.
paramsobjectnoReplaces {key} placeholders in the message body.
idempotency_keystringnoCan also be sent as an Idempotency-Key header.
scheduled_atstringnoISO-8601 datetime, 30 sec – 90 days from now. Credits reserved, response is 202, returns scheduled_id. Cancellable from the dashboard.

Single recipient

curl -X POST https://bulksms.oasistech.co.tz/api/sms \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+255712345678",
    "sender": "ACME",
    "message": "Your verification code is 492715."
  }'

Batch + personalisation

{
  "to": [
    {"number": "+255712345678", "params": {"name":"Amina"}},
    {"number": "+255754901234", "params": {"name":"Juma"}}
  ],
  "sender": "ACME",
  "message": "Habari {name}, asante kwa kununua nasi."
}

Response — 200 OK

{
  "status": "ok",
  "results": [
    { "to": "+255712345678", "status": "sent",
      "credits_used": 1, "segments": 1,
      "provider_ref": "12345678" }
  ],
  "credits_used": 1,
  "balance_after": 142316
}
Gates checked in order: tenant active → KYC approved → sender ID approved → message valid → recipients not in the regulator DNC list → enough credits. Anything that fails returns a specific error (see Errors).
GET /api/balance

Check balance

Returns the current credit balance for the authenticated tenant.

curl https://bulksms.oasistech.co.tz/api/balance \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "credits": 142316
}
GET /api/logs

List messages

Paginated list of your sent SMS, newest first.

Query parameters

ParamDefaultNotes
typesmsFixed.
page11-indexed.
limit501–100.
searchMatches recipient, sender, or message body.
statussent, failed, pending, delivered, undelivered.
fromYYYY-MM-DD
toYYYY-MM-DD
curl "https://bulksms.oasistech.co.tz/api/logs?status=failed&from=2026-05-01" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "data": [
    {
      "id": 84211,
      "recipient": "+255712345678",
      "sender": "ACME",
      "message": "Your code is 492715.",
      "status": "sent",
      "credits_used": 1,
      "segments": 1,
      "encoding": "gsm",
      "provider_ref": "12345678",
      "created_at": "2026-05-17T07:08:50Z"
    }
  ],
  "page": 1, "limit": 50,
  "total": 1, "total_pages": 1
}
GET /api/sender-ids

List sender IDs

Returns your sender IDs and their current approval status.

curl "https://bulksms.oasistech.co.tz/api/sender-ids?status=approved" \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "sender_ids": [
    { "id": 12, "sender": "ACME", "country": "TZ",
      "status": "approved", "reviewed_at": "2026-05-10T14:32:00Z" }
  ],
  "total": 1
}

Requesting a new sender ID is a one-time review and is done from the dashboard, not the API.

Webhooks

Get notified the moment a message is delivered, fails, or a recipient opts out. Add and manage webhook endpoints from the dashboard under Webhooks.

Event types

  • sms.deliveredCarrier confirmed delivery.
  • sms.failedSend failed (invalid number, blocked, etc).
  • sms.expiredCarrier didn't confirm before TTL.
  • credits.lowBalance dropped below your alert threshold.
  • recipient.opted_outRecipient replied STOP.

Example payload

{
  "event": "sms.delivered",
  "id": 84211,
  "recipient": "+255712345678",
  "sender": "ACME",
  "provider_ref": "12345678",
  "delivered_at": "2026-05-17T07:08:51Z"
}

Verify the signature

Every delivery includes an X-BulkSMS-Signature header. It is an HMAC-SHA256 over the raw request body, using the secret shown once when you created the webhook.

// Node.js
const crypto = require('crypto');

app.post('/bulksms-webhook', (req, res) => {
  const sig = req.headers['x-bulksms-signature'];
  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(req.rawBody)
    .digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  // process req.body…
  res.status(200).end();
});
// PHP
$sig = $_SERVER['HTTP_X_BULKSMS_SIGNATURE'] ?? '';
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $WEBHOOK_SECRET);
if (!hash_equals($expected, $sig)) http_response_code(401);
Retries. Non-200 responses are retried with exponential backoff for ~24 hours, then marked failed. Always respond 2xx as soon as you've persisted the event — do the rest of the work asynchronously.

Errors

All errors return JSON with an error string and an HTTP status code.

StatusMeaningTypical cause
400Bad RequestMissing or malformed field.
401UnauthorizedMissing or invalid bearer token.
402Payment RequiredNot enough credits. Top up.
403ForbiddenSender ID not approved · KYC not approved · token scope missing · IP not allowlisted.
404Not FoundUnknown endpoint or method.
409ConflictIdempotency key already processed.
429Too Many RequestsRate limited. Check Retry-After.
502Bad GatewayUpstream carrier rejected the message. Detail in body.
503Service UnavailablePlatform provider not configured or paused.

Rate limits

Limits are applied per API key per minute.

  • /api/sms60 requests / minute (batches OK — counted per request, not per recipient).
  • /api/balance · /api/logs60 / minute.

429 responses include a Retry-After header (seconds). Back off, then retry.

Idempotency

Send the same request twice and only one SMS goes out.

Idempotency-Key: order-1023-confirmation

Keys are scoped per tenant. A retry with the same key returns 409 if the original was already charged. Use a key per logical operation (an order ID, a verification attempt), not per HTTP retry.

Message segments

You're billed per segment per recipient.

Encoding1 segment ≤Multi-segment chunk
GSM-7 (English)160153
Unicode (Swahili diacritics, emoji)7063

A single non-GSM character switches the whole message to Unicode encoding. The response tells you exactly how many segments and credits the send used — trust that, not your own count.