API Documentation

Integrate SMS Localhost into your application with our REST API.

Using WhatsApp? There's a dedicated guide. Connecting your number, agents, templates, campaigns and the WhatsApp REST API

Authentication

All API requests require an API key passed via the X-API-KEY header.

curl -H "X-API-KEY: your_api_key_here" \ https://sms.localhost.co.zw/api/v1/sms/history/
import requests headers = {"X-API-KEY": "your_api_key_here"} response = requests.get( "https://sms.localhost.co.zw/api/v1/sms/history/", headers=headers ) print(response.json())
const response = await fetch( "https://sms.localhost.co.zw/api/v1/sms/history/", { headers: { "X-API-KEY": "your_api_key_here" } } ); const data = await response.json(); console.log(data);
$ch = curl_init("https://sms.localhost.co.zw/api/v1/sms/history/"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "X-API-KEY: your_api_key_here" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true);
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://sms.localhost.co.zw/api/v1/sms/history/")) .header("X-API-KEY", "your_api_key_here") .build(); HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println(response.body());

Generate API keys from your API Keys page in the dashboard.

Auth

GET POST /api/v1/auth/keys/

List active API keys or create a new one.

POST body: name

DELETE /api/v1/auth/keys/{key_id}/

Revoke an API key.

SMS

POST /api/v1/sms/send/

Send an SMS message.

Body: to (phone number), sender (approved Sender ID), message

curl -X POST https://sms.localhost.co.zw/api/v1/sms/send/ \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"to": "0771234567", "sender": "MyBrand", "message": "Hello!"}'
import requests response = requests.post( "https://sms.localhost.co.zw/api/v1/sms/send/", headers={ "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, json={ "to": "0771234567", "sender": "MyBrand", "message": "Hello!" } ) print(response.json()) # {"message_id": "...", "channel": "sms", "status": "sent", ...}
const response = await fetch( "https://sms.localhost.co.zw/api/v1/sms/send/", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ to: "0771234567", sender: "MyBrand", message: "Hello!", }), } ); const data = await response.json(); console.log(data); // {message_id: "...", channel: "sms", status: "sent", ...}
$ch = curl_init("https://sms.localhost.co.zw/api/v1/sms/send/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "X-API-KEY: your_api_key", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "to" => "0771234567", "sender" => "MyBrand", "message" => "Hello!", ]), CURLOPT_RETURNTRANSFER => true, ]); $response = curl_exec($ch); $data = json_decode($response, true); print_r($data);
String json = """ {"to":"0771234567","sender":"MyBrand","message":"Hello!"} """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://sms.localhost.co.zw/api/v1/sms/send/")) .header("X-API-KEY", "your_api_key") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> response = client.send( request, HttpResponse.BodyHandlers.ofString() ); System.out.println(response.body());

Success response:

{ "message_id": "a1b2c3d4-...", "channel": "sms", "status": "sent", "to": "0771234567", "sender": "MyBrand", "sms_credits": 142 }
GET /api/v1/sms/history/

Retrieve the last 100 sent messages.

Optional query: ?channel=sms

curl https://sms.localhost.co.zw/api/v1/sms/history/ \ -H "X-API-KEY: your_api_key"
import requests response = requests.get( "https://sms.localhost.co.zw/api/v1/sms/history/", headers={"X-API-KEY": "your_api_key"} ) messages = response.json()["messages"] for msg in messages: print(f"{msg['recipient']}: {msg['status']}")
const response = await fetch( "https://sms.localhost.co.zw/api/v1/sms/history/", { headers: { "X-API-KEY": "your_api_key" } } ); const { messages } = await response.json(); messages.forEach(msg => console.log(`${msg.recipient}: ${msg.status}`) );
$ch = curl_init("https://sms.localhost.co.zw/api/v1/sms/history/"); curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-KEY: your_api_key"]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true); foreach ($data["messages"] as $msg) { echo $msg["recipient"] . ": " . $msg["status"] . "\n"; }
GET POST /api/v1/sms/senders/

List your Sender IDs or request a new one.

POST body: sender_name (3-11 alphanumeric characters)

GET /api/v1/sms/testers/

List your verified tester numbers.

Contacts

GET POST /api/v1/contacts/

List contacts or create a new one.

POST body: phone, first_name, last_name (optional), email (optional)

# Create a contact curl -X POST https://sms.localhost.co.zw/api/v1/contacts/ \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{"phone": "0771234567", "first_name": "John", "last_name": "Doe"}'
import requests # Create a contact response = requests.post( "https://sms.localhost.co.zw/api/v1/contacts/", headers={ "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, json={ "phone": "0771234567", "first_name": "John", "last_name": "Doe", } ) print(response.json())
// Create a contact const response = await fetch( "https://sms.localhost.co.zw/api/v1/contacts/", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ phone: "0771234567", first_name: "John", last_name: "Doe", }), } ); console.log(await response.json());
// Create a contact $ch = curl_init("https://sms.localhost.co.zw/api/v1/contacts/"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "X-API-KEY: your_api_key", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "phone" => "0771234567", "first_name" => "John", "last_name" => "Doe", ]), CURLOPT_RETURNTRANSFER => true, ]); $data = json_decode(curl_exec($ch), true);
GET PUT DELETE /api/v1/contacts/{contact_id}/

Retrieve, update, or delete a single contact.

GET POST /api/v1/contacts/groups/

List contact groups or create a new one.

Campaigns

GET POST /api/v1/campaigns/

List campaigns or create a new one.

POST body: name, message, sender_name, group_id

curl -X POST https://sms.localhost.co.zw/api/v1/campaigns/ \ -H "X-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "name": "March Promo", "message": "Hi {name}, enjoy 20% off this week!", "sender_name": "MyBrand", "group_id": "your_group_uuid" }'
import requests response = requests.post( "https://sms.localhost.co.zw/api/v1/campaigns/", headers={ "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, json={ "name": "March Promo", "message": "Hi {name}, enjoy 20% off this week!", "sender_name": "MyBrand", "group_id": "your_group_uuid", } ) campaign = response.json() print(f"Campaign created: {campaign['id']}")
const response = await fetch( "https://sms.localhost.co.zw/api/v1/campaigns/", { method: "POST", headers: { "X-API-KEY": "your_api_key", "Content-Type": "application/json", }, body: JSON.stringify({ name: "March Promo", message: "Hi {name}, enjoy 20% off this week!", sender_name: "MyBrand", group_id: "your_group_uuid", }), } ); const campaign = await response.json(); console.log(`Campaign created: ${campaign.id}`);
POST /api/v1/campaigns/{campaign_id}/send/

Launch a campaign for sending.

POST /api/v1/campaigns/{campaign_id}/pause/

Pause a running campaign.

POST /api/v1/campaigns/{campaign_id}/cancel/

Cancel a campaign.

Billing

GET /api/v1/billing/balance/

Get your current SMS credit balance.

curl https://sms.localhost.co.zw/api/v1/billing/balance/ \ -H "X-API-KEY: your_api_key"
import requests response = requests.get( "https://sms.localhost.co.zw/api/v1/billing/balance/", headers={"X-API-KEY": "your_api_key"} ) data = response.json() print(f"Credits remaining: {data['sms_credits']}")
const response = await fetch( "https://sms.localhost.co.zw/api/v1/billing/balance/", { headers: { "X-API-KEY": "your_api_key" } } ); const data = await response.json(); console.log(`Credits remaining: ${data.sms_credits}`);
$ch = curl_init("https://sms.localhost.co.zw/api/v1/billing/balance/"); curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-KEY: your_api_key"]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $data = json_decode(curl_exec($ch), true); echo "Credits remaining: " . $data["sms_credits"];
GET /api/v1/billing/transactions/

View your transaction history.

LinksAPI BETA

LinksAPI automatically shortens links in your outgoing messages using branded rerout.co short links. Shorter links use fewer 160-character SMS segments — so you spend fewer credits — and every link gets click analytics. It applies to SMS sent from the dashboard, the Send API, and campaigns, plus free-text WhatsApp messages.

There is nothing to call — it runs server-side at send time. Turn it on per organization at LinksAPI in your dashboard; from then on any https:// link in your message is replaced automatically before it is sent and billed.

Link modes

  • Per URL (shared) — one short link per destination, reused across recipients. Most efficient; analytics are aggregated per link. URLs too short to save space are left untouched.
  • Per recipient (trackable) — a unique short link per recipient, so clicks can be attributed individually. Every link is shortened (even short ones) for full tracking.

One-time links

In per-recipient mode you can enable one-time links: each link stops redirecting after its first click — ideal for single-use survey or verification links. The first click is served, then the link is deactivated automatically.

Analytics

Total clicks plus top countries and referrers are shown for every link on your LinksAPI page. Click data is aggregated and never exposes recipients' raw IP addresses.

MNO Status

Check the live delivery status of Zimbabwean mobile network operators (NetOne, Econet, Telecel) so your application can react to carrier-side incidents — e.g. pause sends, switch fallback routes, or surface a banner to your own users. You can also subscribe to webhook events that fire the moment a status changes (see Webhooks).

GET /api/v1/status/

Returns the current status for every MNO. Possible state values: operational, degraded, outage, maintenance.

curl https://sms.localhost.co.zw/api/v1/status/ \ -H "X-API-KEY: your_api_key"
import requests statuses = requests.get( "https://sms.localhost.co.zw/api/v1/status/", headers={"X-API-KEY": "your_api_key"}, ).json() for s in statuses: if s["state"] != "operational": print(f"{s['mno_display']} is {s['state']}: {s['note']}")
const res = await fetch( "https://sms.localhost.co.zw/api/v1/status/", { headers: { "X-API-KEY": "your_api_key" } } ); const statuses = await res.json(); statuses .filter(s => s.state !== "operational") .forEach(s => console.warn(`${s.mno_display}: ${s.state}`));

Sample response

[ {"mno":"netone", "mno_display":"NetOne", "state":"outage", "state_display":"Outage", "note":"NetOne core network issue, ETA 2h", "updated_at":"2026-05-12T10:00:00Z"}, {"mno":"econet", "mno_display":"Econet", "state":"operational", "state_display":"Operational", "note":"", "updated_at":"2026-05-10T08:32:11Z"}, {"mno":"telecel", "mno_display":"Telecel", "state":"operational", "state_display":"Operational", "note":"", "updated_at":"2026-05-10T08:32:11Z"} ]
GET /api/v1/status/incidents/

History of state transitions. Optional query params: mno=netone|econet|telecel, limit=N (default 50, max 200). Incidents with ended_at: null are still in progress.

curl "https://sms.localhost.co.zw/api/v1/status/incidents/?mno=netone&limit=10" \ -H "X-API-KEY: your_api_key"

Webhooks

Webhooks let us POST events to your server the instant they happen — no polling required. Register an endpoint at Settings → Webhooks, tick the events you care about, and copy the signing secret we generate (shown only once).

How it works

  1. Create a webhook with an HTTPS URL on your server.
  2. Pick which events to subscribe to (or leave empty to receive all). Available events:
    • sms.delivered — an SMS reached its recipient
    • sms.failed — an SMS failed delivery
    • mno.status.changed — an MNO's delivery state changed (outage, degraded, recovered, maintenance)
  3. We POST a JSON body to your URL with an X-Webhook-Signature header (HMAC-SHA256 of the raw body, hex-encoded, using your secret).
  4. Respond with HTTP 2xx within 10 seconds. Non-2xx responses are retried once after 10 seconds; after that the delivery is logged as failed.

MNO status payload

POST https://your-server.com/webhook Content-Type: application/json X-Webhook-Signature: 4f9c1a...e7 { "event": "mno.status.changed", "mno": "netone", "previous_state": "operational", "state": "outage", "note": "NetOne core network issue, ETA 2h", "incident_id": "5b6f2e4c-...", "timestamp": "2026-05-12T10:00:00Z" }

Verifying the signature

Always verify X-Webhook-Signature against the raw request body — not a re-serialized JSON — using your webhook's secret:

import hmac, hashlib from flask import Flask, request, abort SECRET = b"your_webhook_secret" app = Flask(__name__) @app.post("/webhook") def webhook(): expected = hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest() got = request.headers.get("X-Webhook-Signature", "") if not hmac.compare_digest(expected, got): abort(401) event = request.get_json() if event["event"] == "mno.status.changed" and event["state"] in ("outage", "degraded"): pause_sends_for(event["mno"]) return "", 204
const express = require("express"); const crypto = require("crypto"); const app = express(); const SECRET = "your_webhook_secret"; app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex"); const got = req.headers["x-webhook-signature"] || ""; if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) return res.sendStatus(401); const event = JSON.parse(req.body.toString()); if (event.event === "mno.status.changed" && ["outage", "degraded"].includes(event.state)) { pauseSendsFor(event.mno); } res.sendStatus(204); });
$secret = "your_webhook_secret"; $body = file_get_contents("php://input"); $expected = hash_hmac("sha256", $body, $secret); $got = $_SERVER["HTTP_X_WEBHOOK_SIGNATURE"] ?? ""; if (!hash_equals($expected, $got)) { http_response_code(401); exit; } $event = json_decode($body, true); if ($event["event"] === "mno.status.changed" && in_array($event["state"], ["outage", "degraded"])) { pause_sends_for($event["mno"]); } http_response_code(204);

Rate Limits

Endpoint Limit
/api/v1/sms/send/ 60 requests per minute (per user)

Error Responses

All errors return a JSON object with an error key.

{"error": "Insufficient SMS credits."}
Status Meaning
400 Bad request — check your request body
401 Unauthorized — invalid or missing API key
402 Insufficient SMS credits
403 Sender not approved or insufficient permissions
404 Resource not found
409 Conflict — resource already exists
429 Rate limit exceeded — slow down