Home Features Developers Pricing Documentation
Login Get Started

Twatumi Documentation

Twatumi is Namibian SMS infrastructure for modern applications. Send transactional SMS, OTP codes and notifications from your website, mobile app or backend system through one versioned REST API.

The customer dashboard is administrative only — you manage your wallet, API tokens, webhooks and reporting there. All SMS sending happens through the API described below.

Quick Start

  1. Create your Twatumi account and verify your email.
  2. Top up your wallet (EFT, admin-approved).
  3. Generate an API token from API Tokens in your dashboard.
  4. Send your first request to http://twatumi.local/v1/messages.
curl -X POST http://twatumi.local/v1/messages \
-H "Authorization: Bearer tw_live_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"to": "264811234567", "message": "Your verification code is 483921"}'

Authentication

Every request must include your API token as a Bearer token:

Authorization: Bearer tw_live_xxxxxxxxxxxxxxxxxxxxxxxxx

Twatumi never stores your raw token — only a cryptographic hash and a display prefix. Copy your token when it is generated; it cannot be shown again.

API Tokens

Tokens are environment-scoped and permission-scoped:

  • tw_live_... — sends real SMS and spends your wallet balance.
  • tw_test_... — authenticates and validates normally, creates test message logs, simulates delivery, and never spends wallet funds or contacts a real provider.

Scopes: sms:send, sms:read, balance:read, webhooks:manage. Every request is checked against the token's granted scopes.

Sending SMS

POST http://twatumi.local/v1/messages

{
    "to": "264811234567",
    "message": "Your verification code is 483921",
    "reference": "LOGIN-48291",
    "callback_url": "https://example.com/twatumi/callback"
}

Response:

{
    "success": true,
    "message": "SMS accepted for delivery.",
    "data": {
        "message_id": "sms_01jabc123xyz",
        "status": "queued",
        "to": "264811234567",
        "reference": "LOGIN-48291",
        "segments": 1,
        "cost": 0.50,
        "currency": "NAD"
    }
}

Messages are queued and delivered asynchronously — the provider is never contacted inline during the request. Send an Idempotency-Key header to safely retry a request without sending (or being charged for) the SMS twice.

Message Status

GET http://twatumi.local/v1/messages/{message_id}

{
    "success": true,
    "data": {
        "message_id": "sms_01jabc123xyz",
        "to": "264811234567",
        "status": "delivered",
        "reference": "LOGIN-48291",
        "segments": 1,
        "cost": 0.50,
        "currency": "NAD",
        "sent_on": "2026-09-16T10:20:02+02:00",
        "delivered_on": "2026-09-16T10:20:06+02:00"
    }
}

Status values: queued, processing, sent, delivered, failed, rejected, cancelled.

Balance

GET http://twatumi.local/v1/balance

{
    "success": true,
    "data": { "balance": 485.72, "currency": "NAD" }
}

Webhooks

Configure an endpoint in your dashboard and subscribe to: sms.queued, sms.sent, sms.delivered, sms.failed, sms.rejected, wallet.low_balance.

Every delivery is signed with HMAC-SHA256 and includes:

X-Twatumi-Event: sms.delivered
X-Twatumi-Signature: 5f9e1c...
X-Twatumi-Delivery: whd_01jabc...
X-Twatumi-Timestamp: 1758012345

Verify a delivery by recomputing HMAC_SHA256("{timestamp}.{raw_body}", your_webhook_secret) and comparing it to X-Twatumi-Signature. Failed deliveries retry with exponential backoff and can be retried manually from the dashboard.

Test Mode

Requests made with a tw_test_ token authenticate and validate exactly like production, create a message log clearly marked TEST, and simulate delivery — but never contact a real SMS provider and never spend your wallet balance.

Error Codes

Errors follow a standard envelope:

{
    "success": false,
    "message": "Insufficient wallet balance.",
    "error": { "code": "INSUFFICIENT_BALANCE" }
}
CodeHTTPMeaning
INVALID_TOKEN401Token missing, unknown or revoked
TOKEN_EXPIRED401Token has passed its expiry date
INSUFFICIENT_SCOPE403Token lacks the required scope
ACCOUNT_SUSPENDED403Organisation is suspended
MESSAGE_NOT_FOUND404No message with that ID for your organisation
IDEMPOTENCY_KEY_REUSED409Concurrent duplicate request
INVALID_DESTINATION400Destination number could not be normalised
INVALID_MESSAGE400Message body missing or too long
VALIDATION_FAILED422Request failed field validation
RATE_LIMIT_EXCEEDED429Too many requests
INSUFFICIENT_BALANCE402Wallet balance too low for this send
PROVIDER_UNAVAILABLE503No healthy SMS provider available
INTERNAL_ERROR500Unexpected server error

Rate Limits

Limits apply per API token, per organisation and per IP address, based on your pricing plan (default 60 requests/minute). Every response includes:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1758012400

Phone Numbers

Namibian numbers are normalised automatically: 0811234567, +264811234567 and 264811234567 are all stored and billed as 264811234567.

SMS Segments

GSM-7 messages: 160 characters in a single segment, 153 characters per segment once multipart. Unicode messages: 70 characters single segment, 67 characters per segment multipart. You are billed per segment.

Security

  • API tokens are hashed at rest and shown once at creation.
  • Webhook payloads are signed with HMAC-SHA256.
  • Organisation data is strictly isolated — every query is scoped to your organisation.
  • Optional TOTP two-factor authentication is available for your dashboard login.

Code Examples

cURL

curl -X POST http://twatumi.local/v1/messages \
-H "Authorization: Bearer tw_live_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"to": "264811234567", "message": "Your verification code is 483921"}'

PHP

$ch = curl_init('http://twatumi.local/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer tw_live_xxxxxxxxx',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'to' => '264811234567',
        'message' => 'Your verification code is 483921',
    ]),
]);
$response = json_decode(curl_exec($ch), true);

CodeIgniter 4

$response = service('curlrequest')->post('http://twatumi.local/v1/messages', [
    'headers' => [
        'Authorization' => 'Bearer ' . env('TWATUMI_API_TOKEN'),
        'Content-Type'  => 'application/json',
    ],
    'json' => ['to' => '264811234567', 'message' => 'Your verification code is 483921'],
]);
$data = json_decode($response->getBody(), true);

JavaScript

const res = await fetch('http://twatumi.local/v1/messages', {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer tw_live_xxxxxxxxx',
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ to: '264811234567', message: 'Your verification code is 483921' }),
});
const data = await res.json();

Node.js

const https = require('https');

const body = JSON.stringify({ to: '264811234567', message: 'Your verification code is 483921' });
const req = https.request('http://twatumi.local/v1/messages', {
    method: 'POST',
    headers: {
        'Authorization': 'Bearer tw_live_xxxxxxxxx',
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
    },
}, (res) => { res.on('data', (d) => process.stdout.write(d)); });
req.write(body);
req.end();

Python

import requests

response = requests.post(
    "http://twatumi.local/v1/messages",
    headers={"Authorization": "Bearer tw_live_xxxxxxxxx"},
    json={"to": "264811234567", "message": "Your verification code is 483921"},
)
data = response.json()

C#

using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", "tw_live_xxxxxxxxx");

var payload = new { to = "264811234567", message = "Your verification code is 483921" };
var response = await client.PostAsJsonAsync("http://twatumi.local/v1/messages", payload);
var data = await response.Content.ReadAsStringAsync();

Flutter (Dart)

final response = await http.post(
    Uri.parse('http://twatumi.local/v1/messages'),
    headers: {
        'Authorization': 'Bearer tw_live_xxxxxxxxx',
        'Content-Type': 'application/json',
    },
    body: jsonEncode({
        'to': '264811234567',
        'message': 'Your verification code is 483921',
    }),
);
final data = jsonDecode(response.body);