SMSUG API Reference

Back to app

Send SMS to MTN, Airtel and other Ugandan numbers from your own system. All requests use HTTPS and JSON.

Authentication

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.

Send SMS

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"]
}
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.

Check a message's delivery

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.

List messages

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.

Balance

GET https://app.sms.ug/api/balance

{"status": "success", "balance": 9950, "currency": "UGX", "rates_per_sms": {"mtn": 25, "airtel": 25, "others": 25}}

Webhooks (delivery reports)

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

Errors

HTTPerrorMeaning
400missing_fields, invalid_json, invalid_contacts, too_many_contacts, message_too_long, restricted_content, invalid_titleFix the request.
401unauthorizedMissing or wrong API key.
402insufficient_balanceTop up and retry. Nothing was charged.
403api_disabled, account_suspendedTurn API access on in Settings / contact support.
404not_foundUnknown endpoint or message token.
409idempotency_key_reused, request_in_progressSee Idempotency-Key above.
429rate_limitedSlow down and retry.
502gateway_failedThe network rejected it. Nothing was charged; safe to retry.