API reference · v1
Build messaging into your product.
JSON over HTTPS with Bearer API keys. Create live and test keys in your workspace under Developers → API keys.
Authentication
Send your key as Authorization: Bearer jms_live_…. Keys starting with jms_test_ behave identically but never charge your wallet or deliver messages — messages are recorded with the status sandbox. Each key has scopes (for example sms:send, campaigns:write), an optional IP allowlist and a per-minute rate limit.
Send one SMS
POST/api/v1/sms/send
to accepts local numbers for your account country (0712345678) or international format (+256712345678). reference is a UUID you generate; retrying with the same reference never sends twice. Omit sender_id to use the shared sender.
curl -X POST https://jmsurfsms.co.ke/api/v1/sms/send \
-H "Authorization: Bearer $JMSURF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"0712345678","message":"Your code is 482915","reference":"8f6241c0-24df-4ce2-b39b-9ef85eab198f","sender_id":"ACMELTD"}'{
"id": "2b0d7c1e-…",
"reference": "8f6241c0-24df-4ce2-b39b-9ef85eab198f",
"to": "+254712345678",
"status": "queued",
"segments": 1,
"encoding": "GSM-7",
"cost_kes": 0.7,
"sender_id": "ACMELTD",
"test": false
}Statuses: queued (accepted by the carrier), delivered, failed (refunded when the carrier rejects it), unknown (outcome uncertain — do not resend; it is reconciled for you).
Bulk campaigns
POST/api/v1/sms/bulk
Queue up to 100,000 recipients in one request from to (numbers), recipients (with names and merge fields) and/or group_ids. Merge fields: {first_name}, {name}, {phone} or any key in fields, with fallbacks such as {first_name|Customer}. Duplicates, blocked numbers and disabled destinations are skipped and reported.
{
"name": "March statements",
"message": "Hi {first_name|there}, your balance is KES {balance}. Pay via M-Pesa paybill 123456.",
"recipients": [
{ "phone": "0712345678", "name": "Amina Wanjiku", "fields": { "balance": "1,250" } },
{ "phone": "0733000111", "fields": { "balance": "480" } }
],
"send_at": "2026-10-01T09:00:00+03:00",
"shorten_links": true
}{ "campaign_id": "a1f…", "recipients": 2, "skipped": { "duplicates": 0, "invalid": 0, "blocked": 0, "unsupported_destination": 0 }, "estimated_cost_kes": 2.8, "send_at": "2026-10-01T06:00:00.000Z", "test": false }Message status
GET/api/v1/sms/{id}
Returns the message with its status, cost, network, failure reason and delivery time. Scope: sms:read.
List messages
GET/api/v1/messages?reference=…&status=…&campaign_id=…&limit=50&before=…
Newest first. Page with before set to the last created_at you received.
Campaign progress
GET/api/v1/campaigns/{id}
Recipients, pending, accepted, delivered, failed and cost so far.
Balance
GET/api/v1/balance
{ "balance_kes": 1520.4, "currency": "KES", "rate_kes_per_sms": 0.7, "sms_remaining": 2172 }Contacts
GET/api/v1/contacts?search=…&limit=100&offset=0
POST/api/v1/contacts
{ "phone": "0712345678", "name": "Amina Wanjiku", "fields": { "branch": "Westlands" }, "group_ids": ["…"] }Webhooks
Add HTTPS endpoints under Developers → Webhooks. Events: sms.queued, sms.delivered, sms.failed, sms.unknown, sms.inbound, payment.completed. Each request carries x-jmsurf-signature: sha256=…, an HMAC-SHA256 of the raw body with your endpoint secret. Failed deliveries retry with exponential backoff up to five times.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return header?.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Errors & limits
Errors return { "error": { "code", "message" } }.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request / invalid_recipient | Check the body and phone number |
| 401 | unauthorized / invalid_key | Missing, wrong or revoked key |
| 402 | insufficient_credit | Top up your wallet |
| 403 | insufficient_scope / ip_not_allowed / account_suspended | Key or account restrictions |
| 409 | reference_conflict | Reference reused for a different message |
| 422 | recipient_blocked / destination_disabled / sender_not_approved | The message cannot be sent as requested |
| 429 | rate_limited | Slow down; retry after 60 seconds |
| 503 | sending_paused | Sending is temporarily paused platform-wide |
Need help? Contact us or open a ticket from your workspace.