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.

v1.0
All systems operational
Base URL: https://api.mortarbulksms.com/v1
💡

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.

HTTP 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.

GET /api-keys

List all API keys for your account.

POST /api-keys

Create a new API key.

ParameterTypeRequiredDescription
namestringRequiredA label for this key (e.g. "Production")
DELETE /api-keys/:id

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.

POST /sms

Request Parameters

ParameterTypeDescription
tostringRequiredRecipient phone number in E.164 format (e.g. +256741234567)
fromstringRequiredSender ID — up to 11 alphanumeric characters
messagestringRequiredMessage content. 160 chars = 1 SMS. Each additional 153 chars = +1 SMS.
typestringOptional"sms" (default) or "flash"
scheduled_atstringOptionalISO 8601 datetime to schedule delivery (e.g. 2026-05-01T10:00:00+03:00)
callback_urlstringOptionalURL to receive delivery status webhooks

Response

200 OK 400 Bad Request 401 Unauthorized
Response · 200 OK
{
  "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.

POST /sms/bulk

Request Body

ParameterTypeDescription
recipientsarrayRequiredArray of recipient objects: {to, message?}. Max 10,000.
fromstringRequiredSender ID used for all messages in this batch
messagestringOptionalDefault message if no per-recipient message is provided
scheduled_atstringOptionalISO 8601 datetime for scheduled delivery
Request Body Example
{
  "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" }
  ]
}
Response · 200 OK
{
  "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.

Flash SMS Request
{
  "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).

Scheduled SMS
{
  "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.

GET /sms/:id
Response · 200 OK
{
  "id": "msg_01J8KP2QRST5UVWX",
  "status": "delivered",
  "to": "+256741234567",
  "network": "airtel",
  "delivered_at": "2026-04-29T09:14:03+03:00",
  "latency_ms": 2840
}

Status Values

StatusDescription
queuedMessage accepted and waiting to be sent
sentSubmitted to the network
deliveredConfirmed delivery to the handset
failedDelivery failed (see error_code)
scheduledQueued 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.

Webhook Payload
{
  "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

EventTriggered when
message.queuedMessage accepted into queue
message.sentSubmitted to mobile network
message.deliveredHandset delivery confirmed
message.failedDelivery failed

Error Codes

All errors return a JSON object with error and message fields.

Error Response
{
  "error": "insufficient_balance",
  "message": "Your wallet balance is too low to send this message.",
  "balance": 0
}
HTTPError CodeDescription
400invalid_numberPhone number format invalid or not supported
400message_too_longMessage exceeds 1600 characters (10 SMS parts)
400invalid_sender_idSender ID exceeds 11 characters or contains invalid chars
401unauthorizedMissing or invalid API key
402insufficient_balanceWallet balance too low
429rate_limit_exceededToo many requests — see Rate Limits
500server_errorInternal server error — retry with backoff

Rate Limits

API requests are rate-limited per API key.

EndpointLimit
POST /sms100 requests / minute
POST /sms/bulk10 requests / minute
GET /sms/:id300 requests / minute
All others60 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

NetworkCountryPrefixesRate
Airtel UgandaUganda 🇺🇬+256 70X, 74X, 75XUGX 35/SMS
MTN UgandaUganda 🇺🇬+256 77X, 78X, 76X, 39XUGX 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)

JavaScript
// 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

Python
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
<?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
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
curl https://api.mortarbulksms.com/v1/sms/msg_01J8KP2QRST5UVWX \
  -H "Authorization: Bearer YOUR_API_KEY"

Send bulk SMS

cURL
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" }
    ]
  }'
🚀
Ready to start?

Create your free account and start sending at UGX 35 per SMS.

Get your API key →