Documentation
API Reference
The MortarBulkSMS API lets you send SMS and Flash messages to Airtel and MTN Uganda numbers programmatically. It's a JSON REST API accessible over HTTPS.
New to MortarBulkSMS? Start with the Quickstart guide to get your first message sent in under 5 minutes.
Quickstart
Get your first SMS sent in minutes. No complex setup required.
Create your account
Sign up at mortarbulksms.com. Takes 30 seconds. No credit card required.
Generate an API Key
Go to Settings → API Keys in your dashboard and click "Generate New Key". Copy your key — it won't be shown again.
Top up your wallet
Go to Wallet → Top Up. Fund via MTN or Airtel mobile money. Minimum load is UGX 5,000 (SMS credits at UGX 35/SMS).
Send your first SMS
Make a POST request to /sms with your API key in the Authorization header.
curl -X POST https://api.mortarbulksms.com/v1/sms \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+256741234567", "from": "MORTAR", "message": "Hello from MortarBulkSMS!" }'
Authentication
All API requests must be authenticated using a Bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Keep your API keys secret. Never expose them in client-side code, commit them to git, or share them publicly. Use environment variables.
If an API key is missing or invalid, you'll receive a 401 Unauthorized response.
API Keys
Generate and manage API keys from your dashboard under Settings → API Keys. You can create multiple keys — e.g. one for production, one for testing.
List all API keys for your account.
Create a new API key.
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Required | A label for this key (e.g. "Production") |
Revoke an API key. This action is irreversible.
Send SMS
Send a single SMS to any Airtel or MTN Uganda number. Cost is UGX 35 per 160-character SMS. Longer messages are split into multiple parts, each billed at UGX 35.
Request Parameters
| Parameter | Type | Description | |
|---|---|---|---|
| to | string | Required | Recipient phone number in E.164 format (e.g. +256741234567) |
| from | string | Required | Sender ID — up to 11 alphanumeric characters |
| message | string | Required | Message content. 160 chars = 1 SMS. Each additional 153 chars = +1 SMS. |
| type | string | Optional | "sms" (default) or "flash" |
| scheduled_at | string | Optional | ISO 8601 datetime to schedule delivery (e.g. 2026-05-01T10:00:00+03:00) |
| callback_url | string | Optional | URL to receive delivery status webhooks |
Response
{
"id": "msg_01J8KP2QRST5UVWX",
"status": "queued",
"to": "+256741234567",
"from": "MORTAR",
"network": "airtel",
"message_length": 26,
"parts": 1,
"cost": 35,
"currency": "UGX",
"created_at": "2026-04-29T09:14:00+03:00"
}
Send Bulk SMS
Send the same message (or personalized variants) to multiple recipients in a single API call. Up to 10,000 recipients per request.
Request Body
| Parameter | Type | Description | |
|---|---|---|---|
| recipients | array | Required | Array of recipient objects: {to, message?}. Max 10,000. |
| from | string | Required | Sender ID used for all messages in this batch |
| message | string | Optional | Default message if no per-recipient message is provided |
| scheduled_at | string | Optional | ISO 8601 datetime for scheduled delivery |
{
"from": "MORTAR",
"message": "Hello {name}, your account is ready!",
"recipients": [
{ "to": "+256741234567", "name": "Alice" },
{ "to": "+256752345678", "name": "Bob" },
{ "to": "+256700111222", "message": "Custom message for Carol" }
]
}
{
"batch_id": "batch_01J8KP9XZ",
"total": 3,
"queued": 3,
"failed": 0,
"estimated_cost": 90,
"currency": "UGX"
}
Flash Messages
Flash messages appear directly on the recipient's screen without being stored. Set type to "flash" in any send request.
Flash messages are billed at the same rate — UGX 35 per 160 chars.
{
"to": "+256741234567",
"from": "MORTAR",
"message": "Your OTP is 8823. Do not share this code.",
"type": "flash"
}
Scheduled Delivery
Include a scheduled_at ISO 8601 timestamp to queue a message for future delivery. Use +03:00 for East Africa Time (EAT).
{
"to": "+256741234567",
"from": "MORTAR",
"message": "Your appointment is tomorrow at 9am.",
"scheduled_at": "2026-05-01T08:00:00+03:00"
}
Message Status
Retrieve the current delivery status of any sent message using its ID.
{
"id": "msg_01J8KP2QRST5UVWX",
"status": "delivered",
"to": "+256741234567",
"network": "airtel",
"delivered_at": "2026-04-29T09:14:03+03:00",
"latency_ms": 2840
}
Status Values
| Status | Description |
|---|---|
| queued | Message accepted and waiting to be sent |
| sent | Submitted to the network |
| delivered | Confirmed delivery to the handset |
| failed | Delivery failed (see error_code) |
| scheduled | Queued for future delivery |
Webhooks
Receive real-time delivery status updates by providing a callback_url in your send request. MortarBulkSMS will POST a JSON payload to your URL on every status change.
{
"event": "message.delivered",
"message_id": "msg_01J8KP2QRST5UVWX",
"to": "+256741234567",
"status": "delivered",
"network": "airtel",
"delivered_at": "2026-04-29T09:14:03+03:00",
"latency_ms": 2840
}
Respond with HTTP 200 to acknowledge the webhook. We'll retry up to 5 times with exponential backoff if your endpoint doesn't respond.
Webhook Events
| Event | Triggered when |
|---|---|
| message.queued | Message accepted into queue |
| message.sent | Submitted to mobile network |
| message.delivered | Handset delivery confirmed |
| message.failed | Delivery failed |
Error Codes
All errors return a JSON object with error and message fields.
{
"error": "insufficient_balance",
"message": "Your wallet balance is too low to send this message.",
"balance": 0
}
| HTTP | Error Code | Description |
|---|---|---|
| 400 | invalid_number | Phone number format invalid or not supported |
| 400 | message_too_long | Message exceeds 1600 characters (10 SMS parts) |
| 400 | invalid_sender_id | Sender ID exceeds 11 characters or contains invalid chars |
| 401 | unauthorized | Missing or invalid API key |
| 402 | insufficient_balance | Wallet balance too low |
| 429 | rate_limit_exceeded | Too many requests — see Rate Limits |
| 500 | server_error | Internal server error — retry with backoff |
Rate Limits
API requests are rate-limited per API key.
| Endpoint | Limit |
|---|---|
| POST /sms | 100 requests / minute |
| POST /sms/bulk | 10 requests / minute |
| GET /sms/:id | 300 requests / minute |
| All others | 60 requests / minute |
When you exceed a limit, you'll receive a 429 Too Many Requests response. Check the Retry-After header for the wait time in seconds.
Network Coverage
| Network | Country | Prefixes | Rate |
|---|---|---|---|
| Airtel Uganda | Uganda 🇺🇬 | +256 70X, 74X, 75X | UGX 35/SMS |
| MTN Uganda | Uganda 🇺🇬 | +256 77X, 78X, 76X, 39X | UGX 35/SMS |
More networks and countries are coming soon. Contact us if you need coverage in a specific market.
JavaScript Examples
Send SMS (Node.js / fetch)
// Node.js — using built-in fetch (v18+) const MORTAR_API_KEY = process.env.MORTAR_API_KEY; async function sendSMS({ to, from, message }) { const res = await fetch('https://api.mortarbulksms.com/v1/sms', { method: 'POST', headers: { 'Authorization': `Bearer ${MORTAR_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to, from, message }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.message); } return res.json(); // { id, status, cost, ... } } // Usage sendSMS({ to: '+256741234567', from: 'MORTAR', message: 'Your OTP is 4521. Valid 5 mins.', }).then(console.log);
Python Examples
import os import requests API_KEY = os.environ['MORTAR_API_KEY'] BASE_URL = 'https://api.mortarbulksms.com/v1' def send_sms(to, sender, message, flash=False): payload = { 'to': to, 'from': sender, 'message': message, 'type': 'flash' if flash else 'sms', } r = requests.post( f'{BASE_URL}/sms', headers={'Authorization': f'Bearer {API_KEY}'}, json=payload, ) r.raise_for_status() return r.json() # Example result = send_sms( '+256741234567', 'MORTAR', 'Hello from Python!' ) print(f"Sent! ID: {result['id']}, Cost: UGX {result['cost']}")
PHP Examples
<?php function sendSMS($to, $from, $message) { $apiKey = getenv('MORTAR_API_KEY'); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => 'https://api.mortarbulksms.com/v1/sms', CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'to' => $to, 'from' => $from, 'message' => $message, ]), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($status !== 200) { throw new RuntimeException("SMS failed: {$body}"); } return json_decode($body, true); } // Usage $result = sendSMS('+256741234567', 'MORTAR', 'Hello!'); echo "Sent! ID: {$result['id']}\n";
cURL Examples
Send a single SMS
curl -X POST https://api.mortarbulksms.com/v1/sms \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "+256741234567", "from": "MORTAR", "message": "Hello from MortarBulkSMS!" }'
Check message status
curl https://api.mortarbulksms.com/v1/sms/msg_01J8KP2QRST5UVWX \ -H "Authorization: Bearer YOUR_API_KEY"
Send bulk SMS
curl -X POST https://api.mortarbulksms.com/v1/sms/bulk \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "MORTAR", "message": "Hello {name}!", "recipients": [ { "to": "+256741234567", "name": "Alice" }, { "to": "+256752345678", "name": "Bob" } ] }'