Send SMS from your own system
A plain JSON-over-HTTPS API built for South African businesses. Sign in with your key, send up to 500 messages in one call, and get delivery results and replies posted straight back to you.
Quick start
From zero to your first message in three steps.
"testMode": true to any send to check the call and see the cost — nothing is sent and nothing is charged.GETAuthentication
Send your client ID and secret as HTTP Basic authentication. You get back a token valid for 24 hours; use it on every other call as Authorization: Bearer <token>. When a call answers 401, sign in again.
# exchange your key for a 24-hour token
curl -u CLIENT_ID:SECRET https://api.staging.all-telecoms.co.za/v1/Authentication
{"token": "eyJhbGciOi...", "schema": "JWT", "expiresInMinutes": 1440}const res = await fetch("https://api.staging.all-telecoms.co.za/v1/Authentication", {
headers: { Authorization: "Basic " + btoa(`${CLIENT_ID}:${SECRET}`) }
});
const { token } = await res.json();$ch = curl_init("https://api.staging.all-telecoms.co.za/v1/Authentication");
curl_setopt($ch, CURLOPT_USERPWD, "$CLIENT_ID:$SECRET");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$token = json_decode(curl_exec($ch))->token;import requests
r = requests.get("https://api.staging.all-telecoms.co.za/v1/Authentication", auth=(CLIENT_ID, SECRET))
token = r.json()["token"]At most 60 sign-ins per 15 minutes from one IP address — keep the token and reuse it.
POSTSend messages
Post 1 to 500 messages in a single call. We reply with how many will send, the credits used, and your balance afterward.
curl -X POST https://api.staging.all-telecoms.co.za/v1/BulkMessages \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sendOptions": {
"senderId": "MyShop"
},
"messages": [
{
"destination": "0821234567",
"content": "Your order 1001 is ready.",
"customerId": "order-1001"
},
{
"destination": "0831234567",
"content": "Your order 1002 is ready.",
"customerId": "order-1002"
}
]
}'const res = await fetch("https://api.staging.all-telecoms.co.za/v1/BulkMessages", {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
"sendOptions": {
"senderId": "MyShop"
},
"messages": [
{
"destination": "0821234567",
"content": "Your order 1001 is ready.",
"customerId": "order-1001"
},
{
"destination": "0831234567",
"content": "Your order 1002 is ready.",
"customerId": "order-1002"
}
]
})
});
const report = await res.json();$ch = curl_init("https://api.staging.all-telecoms.co.za/v1/BulkMessages");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer $token", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{
"sendOptions": {
"senderId": "MyShop"
},
"messages": [
{
"destination": "0821234567",
"content": "Your order 1001 is ready.",
"customerId": "order-1001"
},
{
"destination": "0831234567",
"content": "Your order 1002 is ready.",
"customerId": "order-1002"
}
]
}');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$report = json_decode(curl_exec($ch));import requests
res = requests.post(
"https://api.staging.all-telecoms.co.za/v1/BulkMessages",
headers={"Authorization": f"Bearer {token}"},
json={
"sendOptions": {
"senderId": "MyShop"
},
"messages": [
{
"destination": "0821234567",
"content": "Your order 1001 is ready.",
"customerId": "order-1001"
},
{
"destination": "0831234567",
"content": "Your order 1002 is ready.",
"customerId": "order-1002"
}
]
})
report = res.json()The response
{
"eventId": 712,
"messages": 2,
"parts": 2,
"cost": 2,
"remainingBalance": 1250.4,
"sample": "Your order 1001 is ready.",
"costBreakdown": [
{
"quantity": 2,
"cost": 2,
"network": "all"
}
],
"errorReport": {
"noNetwork": 0,
"noContents": 0,
"duplicates": 0,
"optedOuts": 0,
"faults": []
}
}| Field | What it is |
|---|---|
messages | 1 to 500 messages per call. |
destination | The phone number. Accepts 0821234567, 27821234567 or +27821234567. |
content | The text, up to 3 SMS parts (459 characters, or 201 with special characters). |
customerId | Optional: your own reference (up to 100 characters). It comes back with the delivery result and with replies. |
sendOptions.senderId | Optional: the sender name or number (up to 15 characters), if one is set up for your account. |
sendOptions.testMode | Optional: true checks the call and works out the cost, but sends and charges nothing. |
Try it live
Test mode · nothing is sentEdit the request and run it. Preview prices it with no key; Run in test mode signs in with your own key and prices it against the live API. Neither sends a real SMS or charges credit.
GETBalance
Check your remaining credit at any time.
curl -H "Authorization: Bearer TOKEN" https://api.staging.all-telecoms.co.za/v1/Balance
{"balance": 1250.40, "currency": "ZAR"}Delivery results & replies
Give us your system's HTTPS address and we POST JSON to it the moment each message reaches its final result, and whenever someone replies. Answer with any 2xx within 10 seconds; otherwise we retry over about a day (1, 5, 15, 30 min, then 1, 2, 4, 8, 12 h). Redirects aren't followed.
{
"id": "1843",
"type": "delivery",
"messageId": "99120",
"customerId": "order-1001",
"sendId": "4410",
"eventId": "712",
"phoneNumber": "+27821234567",
"status": "DELIVERED",
"networkStatus": "DELIVRD",
"mcc": "655",
"mnc": "01",
"handsetTime": "2026-10-01T08:15:01Z",
"parts": 1,
"time": "2026-10-01T08:15:02Z"
}status is DELIVERED or FAILED. networkStatus, mcc/mnc (the mobile network) and handsetTime are what the network reported, when known.
{
"id": "1844",
"type": "reply",
"replyId": "551",
"phoneNumber": "+27821234567",
"text": "STOP",
"optOut": true,
"replyToMessageId": "99120",
"replyToCustomerId": "order-1001",
"time": "2026-10-01T08:20:40Z"
}optOut is true, we've already stopped sending to that person from your account — you don't have to do anything.Checking it came from us
Every call carries an X-AT-Signature header: t=<unix seconds>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <t>.<raw request body> with your signing secret (in Settings, starts with whsec_). Refuse calls whose t is more than 5 minutes old. The X-AT-Delivery header is the notification's id: the same one can arrive twice, so ignore an id you've already handled.
import { createHmac, timingSafeEqual } from "node:crypto";
function genuine(rawBody, header, secret) {
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "") ?? [];
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest();
return timingSafeEqual(expected, Buffer.from(v1, "hex"));
}Error codes
Errors answer with an HTTP status and {"errorCode": ..., "errorMessage": "..."}.
| Status | Code | Meaning | What to do |
|---|---|---|---|
| 400 | 1000–1006 | Something in the call isn't right (the message says what). | Fix the call. Nothing was sent. |
| 401 | 1401 | Wrong client ID or secret, or the token has expired. | Sign in again. |
| 402 | 1402 | Not enough credit for the whole call. | Top up. Nothing was sent. |
| 403 | 1403 | The account isn't set up for sending yet. | Contact us. |
| 403 | 1404 | The call came from an IP address not allowed for this key. | Add the address under Allowed from. |
| 403 | 1405 | The account's company details for invoices are missing (accounts on account only). | An account owner adds them under Settings. Nothing was sent. |
| 403 | 1406 | Trial account: only the trial message can be sent until the account is activated. | Contact us to activate the account. |
| 429 | 1429 | Too many sign-ins from one address. | Wait 15 minutes; reuse your token. |
| 500 | 1500 | Something went wrong on our side. | Send the same call again (safe to retry). |
Number formats
Phone numbers can be written as 0821234567, 27821234567 or +27821234567 — we normalise them for you. Each account sends to the numbers of one country.