Africa's Talking Integration Guide
What is Africa's Talking?
REST + webhooks for SMS / USSD / Voice / Airtime, with telco-direct delivery.
Send SMS via POST `/version1/messaging` with `to`, `from`, `message`. USSD is webhook-driven — your endpoint receives each step of a session keyed by `sessionId`, returns `CON ...` (more steps) or `END ...` (terminal). Voice is callback-driven similarly with XML response menus (think Twiml). Airtime is fire-and-forget transfers via API. Get an Application Username + API key from the dashboard; sandbox app is free. For codeAmani, AT is the offline-channel companion — when M-Pesa STK push lands on a feature phone, USSD is how you confirm. SMS receipts mirror every payment.
Five Africa's Talking primitives
Telco-grade channels — works on the phones your users actually have.
█████╗ ███████╗██████╗ ██╗ ██████╗ █████╗ ███████╗
██╔══██╗██╔════╝██╔══██╗██║██╔════╝██╔══██╗██╔════╝
███████║█████╗ ██████╔╝██║██║ ███████║███████╗
██╔══██║██╔══╝ ██╔══██╗██║██║ ██╔══██║╚════██║
██║ ██║██║ ██║ ██║██║╚██████╗██║ ██║███████║
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝
████████╗ █████╗ ██╗ ██╗ ██╗██╗███╗ ██╗ ██████╗
╚══██╔══╝██╔══██╗██║ ██║ ██╔╝██║████╗ ██║██╔════╝
██║ ███████║██║ █████╔╝ ██║██╔██╗ ██║██║ ███╗
██║ ██╔══██║██║ ██╔═██╗ ██║██║╚██╗██║██║ ██║
██║ ██║ ██║███████╗██║ ██╗██║██║ ╚████║╚██████╔╝
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝Africa's Talking Integration Guide
Focus: Pan-African communications — SMS, USSD, Voice, Airtime, Mobile Data, and WhatsApp — reaching 300M+ subscribers across Africa. USSD is the standout: it works on feature phones with no smartphone or data plan, making it codeAmani's reach-everyone channel alongside M-Pesa.
Overview
Africa's Talking (AT) is a REST API with official africastalking SDKs for Node.js and
Python. Authenticate with your app username + apiKey (header apiKey). A free
sandbox (username literally sandbox) mirrors production for testing. Services exposed
by the SDK: SMS, VOICE, AIRTIME, MOBILE_DATA, USSD, TOKEN, INSIGHTS,
WHATSAPP, APPLICATION.
Here is the big picture at a glance — one authenticated REST API fanning out to every channel that reaches your users:
- SMS — transactional (OTP, alerts) + bulk, with delivery callbacks.
- USSD — synchronous session API; your server replies plain text
CON …(continue) orEND …(terminate). Reaches feature phones with zero data. - Voice / Airtime / Mobile Data — call flows, airtime top-ups, data bundles.
Official Documentation
| Resource | URL |
|---|---|
| Developer portal | https://developers.africastalking.com/ |
| SMS overview | https://developers.africastalking.com/docs/sms/overview |
| USSD overview | https://developers.africastalking.com/docs/ussd/overview |
| Voice overview | https://developers.africastalking.com/docs/voice/overview |
| Voice actions | https://developers.africastalking.com/docs/voice/actions/overview |
| Airtime sending | https://developers.africastalking.com/docs/airtime/sending |
| Mobile Data sending | https://developers.africastalking.com/docs/data/sending |
| Auth headers | https://developers.africastalking.com/docs/request_headers |
| Node SDK | https://github.com/africastalkingltd/africastalking-node.js |
| Python SDK | https://github.com/AfricasTalkingLtd/africastalking-python |
1. Credentials
Get username + apiKey from the dashboard. For
local dev use the sandbox (username sandbox, sandbox API key).
AT_USERNAME=sandbox # your live app username in production
AT_API_KEY=... # server-side only — never ship to the browser| Environment | Base URL |
|---|---|
| Live | https://api.africastalking.com/version1 |
| Sandbox | https://api.sandbox.africastalking.com/version1 |
2. Install + initialize (Node)
npm install --save africastalking # JS/TS
pip install africastalking # Pythonconst client = require("africastalking")({
apiKey: process.env.AT_API_KEY,
username: process.env.AT_USERNAME, // 'sandbox' for testing
});
const { SMS, USSD } = client;3. Send SMS
await SMS.send({
to: ["+254711XXXYYY", "+254733YYYZZZ"], // note: +254… (international, WITH +)
message: "Your codeAmani OTP is 123456",
// from: "MYSENDERID", // registered short code / alphanumeric sender ID
});Raw REST equivalent (header auth):
curl -X POST https://api.africastalking.com/version1/messaging/bulk \
-H "apiKey: $AT_API_KEY" -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"username":"my_app","phoneNumbers":["+254711XXXYYY"],"message":"Hello","senderId":"ABC"}'The response SMSMessageData.Recipients[].statusCode reports per-number status
(101 Sent, 403 InvalidPhoneNumber, 405 InsufficientBalance, etc.) — status/statusCode
mean accepted for sending, not delivered; use a delivery callback for delivery state.
4. Handle a USSD session
AT POSTs sessionId, phoneNumber, networkCode, serviceCode, text to your callback URL.
Reply with plain text — CON keeps the session open, END closes it. The Node SDK wraps
this as Express middleware:
Picture the back-and-forth — each dial POSTs to you, and your CON/END reply decides whether the menu keeps going or wraps up:
const express = require("express");
const app = express();
app.post("/ussd", USSD((params, sendResponse) => {
const { text, phoneNumber } = params; // text = "" first hit, then "1", "2*50", …
if (text === "") {
sendResponse({ response: "Welcome\n1. Balance\n2. Buy airtime", endSession: false });
} else if (text === "1") {
sendResponse({ response: "Balance: KES 250.00", endSession: true });
} else {
sendResponse({ response: "Done!", endSession: true });
}
}));Register the callback URL (HTTPS) for your service code in the dashboard. For local testing, tunnel with
ngrok http 3000(same as the M-Pesa callback workflow).
5. Voice, Airtime & Mobile Data
Beyond SMS and USSD, the same username + apiKey unlocks three more reach-everyone
channels. Note the separate base hosts: Voice lives on voice.africastalking.com,
Mobile Data on bundles.africastalking.com, while Airtime stays on the version1 REST host.
Airtime — send a top-up
Initialize the service and send airtime to one or more recipients. Each recipient needs a
phoneNumber, currencyCode, and amount. Optional maxNumRetry retries failed sends and
idempotencyKey prevents duplicate top-ups on retry.
const { AIRTIME } = client;
await AIRTIME.send({
recipients: [
{ phoneNumber: "+254711XXXYYY", currencyCode: "KES", amount: 50 },
{ phoneNumber: "+254733YYYZZZ", currencyCode: "KES", amount: 100 },
],
maxNumRetry: 3, // optional: retry failed deliveries
idempotencyKey: "airtime-txn-001", // optional: dedupe duplicate sends
});Raw REST equivalent (note the Idempotency-Key header and amount as a "KES 100.00" string):
curl -X POST https://api.africastalking.com/version1/airtime/send \
-H "apiKey: $AT_API_KEY" -H "Content-Type: application/json" -H "Accept: application/json" \
-H "Idempotency-Key: airtime-txn-001" \
-d '{"username":"my_app","maxNumRetry":2,"recipients":[{"phoneNumber":"+254711XXXYYY","amount":"KES 110.00"}]}'The response responses[].status is Sent/Success (accepted), not delivered — like SMS, the
final state arrives via the airtime status callback. numSent, totalAmount, and
totalDiscount summarize the batch.
Voice — outbound call with actions
Two ways to drive a call. Outbound: POST to voice.africastalking.com/call with from
(your AT number), to (recipients), and a callActions list (Say, Play, Dial):
curl -X POST https://voice.africastalking.com/call \
-H "apiKey: $AT_API_KEY" -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"username":"my_app",
"from":"+254730XXXXXX",
"to":["+254711XXXYYY"],
"callActions":[
{"actionType":"Say","text":"Your codeAmani OTP is 2 6 9 6"},
{"actionType":"Play","url":"https://example.com/jingle.mp3"}
]
}'Inbound / IVR: when AT POSTs to your registered voice callback URL, reply with XML voice
actions. The Node SDK's VOICE.ActionBuilder builds that XML with a fluent chain — here a menu
that collects a keypad digit:
const { ActionBuilder } = client.VOICE;
app.post("/voice", express.urlencoded({ extended: false }), (req, res) => {
const xml = new ActionBuilder()
.say("Welcome to codeAmani. Press 1 for balance, 2 for support.")
.getDigits(
{ say: { text: "Enter your choice followed by hash." } },
{ numDigits: 1, finishOnKey: "#", timeout: 10, callbackUrl: "https://myapp.com/voice/handle" }
)
.build();
res.set("Content-Type", "application/xml");
res.send(xml); // <Response><Say>…</Say><GetDigits …/></Response>
});Available actions: Say, Play, GetDigits, Dial, Record, Enqueue, Dequeue,
Redirect, Reject, Conference. The outbound /call response returns an entries[] list
with per-number status (Queued, InvalidPhoneNumber, DestinationNotSupported,
InsufficientCredit) and a sessionId.
Mobile Data — send a bundle
Send data bundles via MOBILE_DATA.send (REST: POST bundles.africastalking.com/mobile/data/request).
Each recipient needs quantity, unit (MB or GB), and validity
(Day, Week, BiWeek, Month, or Quarterly). productName must match the data product
configured on your AT account.
const { MOBILE_DATA } = client;
await MOBILE_DATA.send({
productName: "Mobile Data", // must match your AT product name exactly
recipients: [
{ phoneNumber: "+254711XXXYYY", quantity: 500, unit: "MB", validity: "Month",
metadata: { customerId: "CUS001", reason: "loyalty-reward" } },
{ phoneNumber: "+254733YYYZZZ", quantity: 1, unit: "GB", validity: "Week",
metadata: { customerId: "CUS002" } },
],
idempotencyKey: "data-txn-001", // optional: dedupe duplicate sends
});The response entries[] gives per-number provider, status (Queued), transactionId, and
value (the KES cost). Final delivery state arrives via the mobile data status callback.
Gotcha — three different hosts, two amount formats. Voice and Mobile Data do not use the
version1REST host: Voice isvoice.africastalking.com/calland Mobile Data isbundles.africastalking.com/mobile/data/request(sandbox:voice.sandbox…/bundles.sandbox…). Airtime also flips amount format depending on layer: the SDK takes a numericamountplus separatecurrencyCode(amount: 50, currencyCode: "KES"), but the raw REST body wants a single"KES 100.00"string. Mixing these up is the most common 4xx. As with SMS, everyQueued/Sentstatus means accepted, not delivered — wire up the per-service status callbacks for the real outcome.
codeAmani notes
- First-class African reach: USSD hits feature phones with no data — pair it with SMS (OTP/receipts) and M-Pesa (Daraja) for an end-to-end Kenyan flow: prompt via USSD, charge via M-Pesa STK Push, confirm via SMS.
- Phone format trap: AT wants
+254XXXXXXXXX(international, with+); the repo's Daraja rule is254XXXXXXXXX(no+). Normalize per-API — don't reuse one formatter blindly. SeeMPESA_PATTERNS.md. - Security:
AT_API_KEYstays server-side (.env.local/ Vercel env). Verify the source of incoming USSD/delivery callbacks and treat the payload as untrusted input. - Sandbox first: username
sandbox+ the simulator before going live; sender IDs and USSD codes require registration/approval for production. - Low-bandwidth: SMS/USSD are the most reliable channels on 2G/3G — favor them for critical notifications over data-dependent push.