API Reference

Build with TumaSend

A single, consistent API to send SMS, WhatsApp messages, transactional email, and OTP codes to customers across Malawi and beyond.

SMS WhatsApp Email OTP / 2FA
Base URL
https://gateway.tumasend.com
HTTPS only
Security

Authentication

Every request must include your API key in the x-api-key header. Each key is scoped to a single asset — an SMS Sender ID, WhatsApp number, Email domain, or OTP service.

Header x-api-key
Live key ts_live_••••••••••••••••••••••••••••••••
Test key ts_test_••••••••••••••••••••••••••••••••
Keep keys secret. Never expose them in client-side code, public repos, or logs. Rotate immediately from Settings → API Keys if compromised.

Scopes

ScopeDescription
sms:sendSend SMS via an approved Sender ID
whatsapp:sendSend WhatsApp messages via a registered number
email:sendSend transactional email from a verified domain
otp:sendIssue and verify OTP codes via an OTP service

Base URL & Versioning

All endpoints are prefixed with /api/v1/. We never break backward compatibility within a major version.

VersionStatusPrefix
v1Current/api/v1/

Rate Limits

Limits are per API key on a sliding window. Exceeding a limit returns 429.

WindowDefault limit
Per second10
Per minute300
Per day50,000

Environments

Key prefixModeDeliveryCredits
ts_live_LiveDelivered to real recipientsDeducted
ts_test_TestSimulated — not deliveredFree
Messaging

Send SMS

Send one message to multiple recipients in a single request. All recipients are batched together and a batch_id is returned for delivery tracking.

POST /api/v1/send/sms
sms:send

Request Body

FieldTypeDescription
fromstringrequiredSender name matching the Sender ID bound to your key.
recipientsstring[]requiredE.164 numbers — Malawi format: +265XXXXXXXXX. Max 1,000.
messagestringrequiredMessage body. Max 160 chars single SMS, up to 918 chars concatenated.
cURL
Node.js
Python
PHP
curl -X POST https://gateway.tumasend.com/api/v1/send/sms \
  -H "Content-Type: application/json" \
  -H "x-api-key: ts_live_your_key" \
  -d '{
    "from": "TumaSend",
    "recipients": ["+265991234567", "+265881234567"],
    "message": "Hello! Your order is ready for pickup."
  }'
const res = await fetch('https://gateway.tumasend.com/api/v1/send/sms', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'ts_live_your_key',
  },
  body: JSON.stringify({
    from: 'TumaSend',
    recipients: ['+265991234567'],
    message: 'Hello! Your order is ready.',
  }),
});
const { batch_id, credits_remaining } = await res.json();
import requests

data = requests.post(
    "https://gateway.tumasend.com/api/v1/send/sms",
    headers={"x-api-key": "ts_live_your_key"},
    json={
        "from": "TumaSend",
        "recipients": ["+265991234567"],
        "message": "Hello! Your order is ready.",
    },
).json()
print(data["batch_id"])
$ch = curl_init('https://gateway.tumasend.com/api/v1/send/sms');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'x-api-key: ts_live_your_key',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'from'       => 'TumaSend',
        'recipients' => ['+265991234567'],
        'message'    => 'Hello! Your order is ready.',
    ]),
]);
$data = json_decode(curl_exec($ch), true);

Response  201

{
  "batch_id": "550e8400-e29b-41d4-a716-446655440000",
  "success": true,
  "total_recipients": 2,
  "queued": 2,
  "invalid_recipients": [],
  "credits_used": 2,
  "credits_remaining": 998,
  "environment": "live"
}

Send WhatsApp

Send a WhatsApp text to recipients with WhatsApp installed. Numbers must be E.164 format.

POST /api/v1/send/whatsapp
whatsapp:send

Request Body

FieldTypeDescription
recipientsstring[]requiredE.164 phone numbers. Max 1,000.
messagestringrequiredMessage text. Supports *bold*, _italic_ WhatsApp formatting.
cURL
Node.js
curl -X POST https://gateway.tumasend.com/api/v1/send/whatsapp \
  -H "Content-Type: application/json" \
  -H "x-api-key: ts_live_your_key" \
  -d '{"recipients":["+265991234567"],"message":"*Order #1234* is ready!"}'
await fetch('https://gateway.tumasend.com/api/v1/send/whatsapp', {
  method: 'POST',
  headers: { 'x-api-key': 'ts_live_your_key', 'Content-Type': 'application/json' },
  body: JSON.stringify({ recipients: ['+265991234567'], message: '*Order #1234* is ready!' }),
});

Send Email

Send transactional email from a TumaSend-verified domain. Supports HTML/text bodies, CC/BCC, reply-to, and analytics tags.

POST /api/v1/send/email
email:send

Request Body

FieldTypeDescription
tostring | string[]requiredRecipient email(s). Max 50.
fromstringrequiredSender address on your verified domain. Accepts Name <addr@domain.com>.
subjectstringrequiredEmail subject line.
htmlstringoptional*HTML body. At least one of html or text required.
textstringoptional*Plain text fallback.
reply_tostringoptionalReply-To address.
cc / bccstring[]optionalCC and BCC recipients.
tagsobjectoptionalKey/value tags for analytics, e.g. {"flow":"order-confirm"}.
cURL
Node.js
curl -X POST https://gateway.tumasend.com/api/v1/send/email \
  -H "Content-Type: application/json" \
  -H "x-api-key: ts_live_your_key" \
  -d '{
    "to": "customer@example.com",
    "from": "orders@yourdomain.com",
    "subject": "Your order is confirmed",
    "html": "<h1>Confirmed!</h1><p>Thanks for your order.</p>"
  }'
const res = await fetch('https://gateway.tumasend.com/api/v1/send/email', {
  method: 'POST',
  headers: { 'x-api-key': 'ts_live_your_key', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    to: 'customer@example.com',
    from: 'orders@yourdomain.com',
    subject: 'Your order is confirmed',
    html: '<h1>Confirmed!</h1>',
    tags: { flow: 'order-confirm' },
  }),
});
OTP Service

Send OTP

Generate and deliver a one-time password via SMS, WhatsApp, or Email. The code is stored server-side and expires automatically.

POST /api/v1/otp/send
otp:send

Request Body

FieldTypeDescription
recipientstringrequiredPhone (E.164) or email address.
channelstringoptionalsms (default) · whatsapp · email
expiry_secondsintegeroptionalOTP lifetime. Default 300 (5 min). Max 3600.
lengthintegeroptionalDigit count. Default 6. Range 4–8.
cURL
Node.js
curl -X POST https://gateway.tumasend.com/api/v1/otp/send \
  -H "Content-Type: application/json" \
  -H "x-api-key: ts_live_your_key" \
  -d '{"recipient":"+265991234567","channel":"sms","length":6}'
const { otp_id } = await fetch('https://gateway.tumasend.com/api/v1/otp/send', {
  method: 'POST',
  headers: { 'x-api-key': 'ts_live_your_key', 'Content-Type': 'application/json' },
  body: JSON.stringify({ recipient: '+265991234567', channel: 'sms' }),
}).then(r => r.json());
// Store otp_id — you'll need it for /otp/verify

Response  201

{ "otp_id": "otp_abc123def456", "channel": "sms", "expires_at": "2026-06-04T09:05:00Z" }

Verify OTP

Confirm a code submitted by the user. A verified OTP cannot be reused.

POST /api/v1/otp/verify
otp:send

Request Body

FieldTypeDescription
otp_idstringrequiredThe otp_id returned by Send OTP.
codestringrequiredThe 4–8 digit code the user entered.
cURL
Node.js
curl -X POST https://gateway.tumasend.com/api/v1/otp/verify \
  -H "Content-Type: application/json" \
  -H "x-api-key: ts_live_your_key" \
  -d '{"otp_id":"otp_abc123def456","code":"482910"}'
const { verified } = await fetch('https://gateway.tumasend.com/api/v1/otp/verify', {
  method: 'POST',
  headers: { 'x-api-key': 'ts_live_your_key', 'Content-Type': 'application/json' },
  body: JSON.stringify({ otp_id: 'otp_abc123def456', code: '482910' }),
}).then(r => r.json());

if (verified) { /* identity confirmed */ }
Account

Get Balance

Retrieve the current credit balance for all channels on your tenant account.

GET /api/v1/balance
any scope
cURL
curl https://gateway.tumasend.com/api/v1/balance \
  -H "x-api-key: ts_live_your_key"

Response  200

{ "sms_credits": 1000, "whatsapp_credits": 500, "email_credits": 2000, "environment": "live" }

List Batches

List sent message batches, ordered by most recent. Use the batch_id from a send response to retrieve delivery status.

GET /api/v1/batches
any scope

Query Parameters

ParamTypeDescription
pageintegerPage number. Default 1.
limitintegerResults per page. Default 20, max 100.
channelstringFilter: sms · whatsapp · email
Webhooks

Webhooks

TumaSend sends signed HTTP POST requests to your endpoint when events occur. Configure your URL from Settings → Webhooks.

Verify every request using the X-TumaSend-Signature header (HMAC-SHA256). Reject any request that fails validation.

Signature Verification

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

function verify(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected)
  );
}
import hmac, hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Event Reference

Event typeWhen it fires
message.deliveredSMS or WhatsApp confirmed delivered to handset
message.failedDelivery failed (invalid number, network error)
message.bouncedEmail hard-bounced
email.unsubscribedRecipient clicked unsubscribe
email.domain.verifiedAll DNS records verified for an email domain
otp.verifiedOTP code successfully verified

Payload shape

{
  "event_type": "message.delivered",
  "created_at": "2026-06-04T08:32:00Z",
  "payload": {
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "recipient": "+265991234567",
    "channel": "sms",
    "status": "delivered"
  }
}
Reference

Error Codes

All errors return a consistent JSON shape. The error field is machine-readable; message is human-readable.

{ "error": "insufficient_credits", "message": "Not enough SMS credits", "credits_required": 5 }

HTTP Status Codes

StatusMeaning
200Request succeeded
201Resource created (message sent / batch queued)
400Validation error in request body
401API key missing, invalid, or revoked
402Insufficient credits
403Key scope doesn't cover this endpoint
429Rate limit exceeded
500Internal server error

Error Strings

StringStatusDescription
unauthorized401API key missing or invalid
scope_not_allowed403Key doesn't have required scope
insufficient_credits402Not enough credits for the operation
rate_limit_exceeded429Too many requests in current window
invalid_recipients400One or more phone numbers invalid
content_blocked400Message blocked by content moderation
kyc_required403KYC must be approved before sending
otp_expired400OTP expired or already used
otp_invalid400OTP code does not match

SDKs & Quickstart

No SDK? No problem. The API is straightforward — here's a minimal Node.js wrapper to get started in seconds.

// Zero dependencies — just native fetch (Node 18+)
class TumaSend {
  constructor(apiKey, base = 'https://gateway.tumasend.com') {
    this._key  = apiKey;
    this._base = base;
  }

  async _post(path, body) {
    const res = await fetch(this._base + path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'x-api-key': this._key },
      body: JSON.stringify(body),
    });
    const data = await res.json();
    if (!res.ok) throw new Error(data.message || data.error);
    return data;
  }

  sms(from, recipients, message) {
    return this._post('/api/v1/send/sms', { from, recipients, message });
  }
  whatsapp(recipients, message) {
    return this._post('/api/v1/send/whatsapp', { recipients, message });
  }
  sendOtp(recipient, channel = 'sms') {
    return this._post('/api/v1/otp/send', { recipient, channel });
  }
  verifyOtp(otpId, code) {
    return this._post('/api/v1/otp/verify', { otp_id: otpId, code });
  }
}

// Usage
const ts = new TumaSend('ts_live_your_key');
await ts.sms('TumaSend', ['+265991234567'], 'Hello from TumaSend!');
const { otp_id } = await ts.sendOtp('+265991234567');
const { verified } = await ts.verifyOtp(otp_id, userEnteredCode);