Send SMS to MTN, Airtel and other Ugandan numbers from your own system. All requests use HTTPS and JSON.
Get your API key from Settings > API Settings (turn API access on there too) and send it on every request:
Authorization: Bearer sms_live_xxxxxxxx
Limits: 300 requests per minute per account, of which 120 may be sends.
POST https://app.sms.ug/api/sms
Content-Type: application/json
Idempotency-Key: invoice-1043 (optional, recommended)
{
"title": "Fees reminder",
"message": "Dear {name|Parent}, the fees balance is UGX {balance}.",
"contacts": ["0772123456", "+256701234567"]
}
{placeholders} are filled from contacts you imported with headings; each person is charged for their own text.409.200 OK
{
"status": "success",
"state": "sent", // "queued" for large sends - they go out within a minute or two
"token": "6f1c2a9e-...", // use with GET /sms/{token}
"recipients": 2,
"skipped_opted_out": 0,
"sms_parts": 1,
"encoding": "gsm7",
"sender_id": "SMSUG",
"cost": 50,
"balance": 9950,
"network_breakdown": {"mtn": 1, "airtel": 1, "others": 0}
}
POST https://app.sms.ug/api/ (without /sms) also works, for existing integrations.
GET https://app.sms.ug/api/sms/{token}?page=1
{
"status": "success",
"token": "6f1c2a9e-...",
"state": "sent",
"summary": {"delivered": 1, "submitted": 1},
"recipients": [
{"phone": "256772123456", "status": "delivered", "network": "mtn", "sms_parts": 1, "cost": 25, "updated_at": "2026-10-15T09:01:12+03:00"}
]
}
Recipient status: queued, scheduled, review, sending, submitted (handed to the network), delivered, failed (refunded), cancelled. 500 recipients per page.
Message state review means some words in it need a quick check by SMSUG first: you are charged, nothing is sent until it is approved, and a rejected message (rejected) is refunded in full. Messages with content that isn't allowed are refused with 400 restricted_content and not charged.
GET https://app.sms.ug/api/sms?from=2026-10-01&to=2026-10-31&page=1
50 per page, newest first. Defaults to the current month.
GET https://app.sms.ug/api/balance
{"status": "success", "balance": 9950, "currency": "UGX", "rates_per_sms": {"mtn": 25, "airtel": 25, "others": 25}}
Set a URL under Settings > API Settings > Webhook. Each delivery report is POSTed to it as JSON:
{"event": "delivery.updated", "created": 1792040000, "message_token": "6f1c2a9e-...", "phone": "256772123456", "status": "delivered"}
Reply with any 2xx status within 10 seconds. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 6 h.
Every request has a header X-SMSUG-Signature: t=<unix time>,v1=<signature>. Check it before trusting the body:
<?php
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_SMSUG_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, 'YOUR_WEBHOOK_SECRET');
if(!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) $sig['t']) > 300){
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// ... update your records ...
| HTTP | error | Meaning |
|---|---|---|
| 400 | missing_fields, invalid_json, invalid_contacts, too_many_contacts, message_too_long, restricted_content, invalid_title | Fix the request. |
| 401 | unauthorized | Missing or wrong API key. |
| 402 | insufficient_balance | Top up and retry. Nothing was charged. |
| 403 | api_disabled, account_suspended | Turn API access on in Settings / contact support. |
| 404 | not_found | Unknown endpoint or message token. |
| 409 | idempotency_key_reused, request_in_progress | See Idempotency-Key above. |
| 429 | rate_limited | Slow down and retry. |
| 502 | gateway_failed | The network rejected it. Nothing was charged; safe to retry. |