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
- Create your Twatumi account and verify your email.
- Top up your wallet (EFT, admin-approved).
- Generate an API token from API Tokens in your dashboard.
- 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" }
}
| Code | HTTP | Meaning |
|---|---|---|
| INVALID_TOKEN | 401 | Token missing, unknown or revoked |
| TOKEN_EXPIRED | 401 | Token has passed its expiry date |
| INSUFFICIENT_SCOPE | 403 | Token lacks the required scope |
| ACCOUNT_SUSPENDED | 403 | Organisation is suspended |
| MESSAGE_NOT_FOUND | 404 | No message with that ID for your organisation |
| IDEMPOTENCY_KEY_REUSED | 409 | Concurrent duplicate request |
| INVALID_DESTINATION | 400 | Destination number could not be normalised |
| INVALID_MESSAGE | 400 | Message body missing or too long |
| VALIDATION_FAILED | 422 | Request failed field validation |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests |
| INSUFFICIENT_BALANCE | 402 | Wallet balance too low for this send |
| PROVIDER_UNAVAILABLE | 503 | No healthy SMS provider available |
| INTERNAL_ERROR | 500 | Unexpected 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);