Skip to content
+254 741 686 242 SMS Login
Frostwood Bulk SMS

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-1
PHP
$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 Accepted
Node.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 === 202

Personalised 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 pathScopePurpose
GET /balancebalance:readAvailable and reserved credits
POST /smsmessages:sendSimplest send: to, message, optional sender_id, reference. Returns 202.
POST /messagesmessages:sendSend to up to 100 recipients with per-recipient values. Requires Idempotency-Key. Returns 202.
GET /messages/{id}messages:readSubmission, delivery and billing status of one message
GET /campaigns/{id}messages:readCounts 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);