The Zino SMS REST API lets your software send SMS, check delivery and read your balance. It speaks JSON over HTTPS. Base URL: https://YOUR-DOMAIN/api/v1
Create an API key in your dashboard under API keys. Send it as a Bearer token on every request. Keys are shown once, so store them in your server's private config, never in browser code.
Authorization: Bearer zsk_live_xxxxxxxxxxxxxxxx
While your account is connected to the sandbox provider, no real SMS leaves our servers. Use these numbers to test every outcome:
| Number ends with | Result |
|---|---|
| any other digits | Accepted, then delivered a few seconds later |
1111 | Accepted, then reported failed (handset unreachable) |
0000 | Rejected immediately (invalid destination) |
POST/v1/messages
| Field | Type | Description |
|---|---|---|
to | string | Recipient. Local (0772123456) or international (+256772123456). |
message | string | Text to send. |
from | string, optional | An approved sender ID. Defaults to the shared sender. |
client_ref | string, optional | Your own reference. Sending the same ref to the same number twice returns the first message instead of sending again. |
schedule_at | ISO 8601, optional | Send later, e.g. 2026-12-01T09:00:00+03:00. |
curl -X POST https://YOUR-DOMAIN/api/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"0772123456","message":"Hello from Zino SMS","client_ref":"order-1042"}'
$ch = curl_init('https://YOUR-DOMAIN/api/v1/messages');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['to' => '0772123456', 'message' => 'Hello', 'client_ref' => 'order-1042'])]);
$res = json_decode(curl_exec($ch), true);
const res = await fetch('https://YOUR-DOMAIN/api/v1/messages', {
method: 'POST',
headers: { Authorization: 'Bearer ' + key, 'Content-Type': 'application/json' },
body: JSON.stringify({ to: '0772123456', message: 'Hello', client_ref: 'order-1042' })
});
import requests
r = requests.post("https://YOUR-DOMAIN/api/v1/messages",
headers={"Authorization": f"Bearer {key}"},
json={"to": "0772123456", "message": "Hello", "client_ref": "order-1042"})
print(r.json())Response 201:
{
"ok": true,
"message": {
"id": "msg_ad1a650a249e0b1d", "to": "+256772123456", "from": "ZinoSMS",
"status": "sent", "segments": 1, "credits": 1, "encoding": "GSM-7",
"client_ref": "order-1042", "error": null, "created_at": "2026-11-01 14:43:04"
},
"balance": 2839
}Statuses: queued (waiting or scheduled), sent (accepted by the carrier network), delivered, failed (credits refunded), cancelled.
POST/v1/messages/bulk up to 1,000 messages per request. Credits for the whole request are checked first, so it either fits your balance or returns 402.
# Same text to many numbers
{ "to": ["0772111111", "0772222222"], "message": "Shop closed Monday", "from": "MyShop" }
# Different text per number
{ "messages": [ {"to": "0772111111", "message": "Hi Amina, your order is ready", "client_ref": "o-1"},
{"to": "0772222222", "message": "Hi Brian, your order is ready", "client_ref": "o-2"} ] }Invalid numbers are skipped and listed under rejected; you are only charged for accepted messages.
GET/v1/messages/{id} accepts the message id or your client_ref.
GET/v1/messages?status=delivered&page=1&per=50&since=2026-11-01
POST/v1/quote body {"to": "...", "message": "..."} returns encoding, segments and credits without sending.
GET/v1/balance
{ "ok": true, "credits": 2839, "low_balance_threshold": 50, "plan": "Business", "subscription": "active", "renews_at": "2026-11-22 13:51:47" }GET/v1/sender-ids lists your IDs and their approval status. Request new ones in the dashboard.
Add an endpoint in the dashboard under Webhooks. We send a POST with JSON whenever a message finishes. Events: message.delivered, message.failed.
{ "event": "message.delivered", "created_at": "2026-11-01T14:43:09+00:00",
"data": { "id": "msg_ad1a650a249e0b1d", "to": "+256772123456", "status": "delivered", "client_ref": "order-1042" } }Verify the signature header X-ZinoSMS-Signature: sha256=<hex>, which is the HMAC-SHA256 of the raw request body using your endpoint secret. Reply with any 2xx status. Endpoints that keep failing are disabled after 20 consecutive failures.
$expected = 'sha256=' . hash_hmac('sha256', file_get_contents('php://input'), $secret);
if (!hash_equals($expected, $_SERVER['HTTP_X_ZINOSMS_SIGNATURE'] ?? '')) { http_response_code(401); exit; }Errors return a non-2xx status and {"ok": false, "error": "code", "message": "..."}.
| HTTP | error | Meaning |
|---|---|---|
| 401 | missing_api_key / invalid_api_key | Key missing, wrong or revoked |
| 402 | insufficient_credits | Top up your wallet |
| 403 | sender_not_approved / account_suspended | Sender ID not approved, or account suspended |
| 404 | not_found | Unknown message or endpoint |
| 422 | missing_field / invalid_number / invalid_sender | Fix the request |
| 429 | rate_limited | Slow down, see Retry-After |
| 500 | server_error | Try again shortly |
Each key is limited per minute according to your plan (30 on Free, up to 1,000 on Enterprise). Every response includes X-RateLimit-Limit. When exceeded you receive 429 with a Retry-After header.
Messages using the standard GSM-7 alphabet fit 160 characters in one segment, 153 per segment when longer. Messages with emoji or special letters use Unicode: 70 characters, then 67 per segment. Each segment costs the destination's credit rate (1 credit for Uganda). Failed messages are refunded.
POST/v1/partner/schools
{ "code": "school-123", "name": "Example Secondary School", "credits": 500 }
# credits is optional and is moved from your partner wallet. The response contains the school's own api_key, shown once.POST/v1/partner/schools/{code}/credits body {"credits": 200}. Negative numbers reclaim credits back to the partner wallet.
GET/v1/partner/schools GET/v1/partner/schools/{code} POST/v1/partner/schools/{code}/keys (rotate the school key)
POST/v1/partner/send
{ "school_code": "school-123",
"type": "fees", # report_card | fees | notice (used for reporting)
"from": "Eduscape",
"messages": [ { "to": "0772123456", "message": "Fees balance reminder for Term 1", "client_ref": "fee-2026-t1-0042" } ] }The school's wallet is charged. Responses include school_balance. Delivery webhooks registered on the partner account include a school_code field in data. A school can also use its own key with the normal /v1/messages endpoints; schools may use their partner's approved sender IDs.