API reference · v1
Frostwood Bulk SMS API.
Everything you need to send SMS from your own software.
Base URL https://sms.frostwood.co.ke/api/v1. JSON in and out, UTC ISO 8601 timestamps, an X-Request-ID header on every response. The machine-readable contract is the OpenAPI file.
Authentication
Create a key under Developers in your dashboard and send it as Authorization: Bearer <key>, or as an X-API-Key header if your system cannot set Authorization. Each key has scopes: messages:send, messages:read, balance:read.
Quick start for CRMs and other systems
POST /sms is the simplest call, for one-time codes, account creation and other notifications. Send JSON or form fields: to (a number, or several separated by commas), message, and optionally sender_id (needed only if your account has more than one) and reference (a retry with the same reference is never sent twice). Transactional messages use a priority queue, so they are not held up by bulk campaigns.
cURL
curl -X POST https://sms.frostwood.co.ke/api/v1/sms \
-H "X-API-Key: fw_live_xxxx_..." \
-d to=0712345678 \
-d "message=Your verification code is 482913. It expires in 5 minutes." \
-d reference=otp-user-1042-attempt-1PHP
$ch = curl_init('https://sms.frostwood.co.ke/api/v1/sms');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['X-API-Key: '.getenv('FROSTWOOD_API_KEY'), 'Accept: application/json'],
CURLOPT_POSTFIELDS => http_build_query([
'to' => '0712345678',
'message' => "Welcome {$name}! Your account is ready.",
'reference' => "signup-{$userId}",
]),
]);
$result = json_decode(curl_exec($ch), true); // 202: queued; $result['messages'][0]['id']Python
import os, requests
r = requests.post("https://sms.frostwood.co.ke/api/v1/sms",
headers={"X-API-Key": os.environ["FROSTWOOD_API_KEY"]},
data={"to": "0712345678", "message": "Your code is 482913.", "reference": "otp-1042-1"},
timeout=15)
r.raise_for_status() # 202 AcceptedNode.js
const res = await fetch("https://sms.frostwood.co.ke/api/v1/sms", {
method: "POST",
headers: { "X-API-Key": process.env.FROSTWOOD_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ to: "0712345678", message: "Your code is 482913.", reference: "otp-1042-1" }),
});
const data = await res.json(); // res.status === 202Personalised messages
Write fields in braces, for example Dear {Firstname}, your account has been credited with KES {amount}. Names ignore case, spaces and underscores. With POST /messages, give each recipient its own values:
{
"sender_id": "YOURBRAND",
"body": "Dear {Firstname}, your account has been credited with KES {amount}.",
"recipients": [
{"to": "0712345678", "variables": {"Firstname": "Jane", "amount": "5,000"}},
{"to": "0722000111", "variables": {"Firstname": "Otieno", "amount": "1,250"}}
],
"fallbacks": {"Firstname": "Member"}
}
A recipient with a missing value and no fallback is left out and listed in excluded, never sent a blank. Promotional messages ("category": "marketing") are only sent from 6:00 am to 6:00 pm East Africa Time; outside those hours they are scheduled for 6:00 am.
Endpoints
| Method and path | Scope | Purpose |
|---|---|---|
| GET /balance | balance:read | Available and reserved credits |
| POST /sms | messages:send | Simplest send: to, message, optional sender_id, reference. Returns 202. |
| POST /messages | messages:send | Send to up to 100 recipients with per-recipient values. Requires Idempotency-Key. Returns 202. |
| GET /messages/{id} | messages:read | Submission, delivery and billing status of one message |
| GET /campaigns/{id} | messages:read | Counts for a request's messages |
POST /messages body
{
"sender_id": "YOURBRAND", // approved sender name or id
"category": "transactional", // transactional, alert or marketing
"recipients": ["0712345678", "+254110123456"],
"body": "Hello {first_name}, your code is 4821.", // or "template_id"
"fallbacks": {"first_name": "there"},
"client_reference": "order-10482",
"send_at": null // optional ISO 8601 time to schedule
}
Errors
{"error": {"code": "insufficient_credits", "message": "...", "field_errors": {}, "request_id": "req_..."}}
Codes: validation_failed (422), insufficient_credits (402), sender_not_approved (422), account_restricted (403), duplicate_conflict (409, the same Idempotency-Key with different content), throttled (429, see Retry-After), provider_temporarily_unavailable (503), not_found (404).
Webhooks
Events: message.accepted, message.rejected, message.delivered, message.undelivered, campaign.completed. Each POST body has event_id, event_type, occurred_at and data. Verify the Frostwood-Signature header t=<unix>,v1=<hex> where v1 = HMAC_SHA256(secret, t + "." + raw_body), reject old timestamps, and deduplicate by event_id. We retry on network errors, 429 and 5xx for 24 hours.
// PHP
[$t, $v1] = sscanf($_SERVER['HTTP_FROSTWOOD_SIGNATURE'], 't=%d,v1=%s');
$valid = abs(time() - $t) < 300 && hash_equals(hash_hmac('sha256', $t.'.'.file_get_contents('php://input'), $secret), $v1);