← Back to dashboard
africas-talkingcommsfresh

Africa's Talking Integration Guide

What is Africa's Talking?

The real model

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.

Text
 █████╗ ███████╗██████╗ ██╗ ██████╗ █████╗ ███████╗
██╔══██╗██╔════╝██╔══██╗██║██╔════╝██╔══██╗██╔════╝
███████║█████╗  ██████╔╝██║██║     ███████║███████╗
██╔══██║██╔══╝  ██╔══██╗██║██║     ██╔══██║╚════██║
██║  ██║██║     ██║  ██║██║╚██████╗██║  ██║███████║
╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝ ╚═════╝╚═╝  ╚═╝╚══════╝

████████╗ █████╗ ██╗     ██╗  ██╗██╗███╗   ██╗ ██████╗
╚══██╔══╝██╔══██╗██║     ██║ ██╔╝██║████╗  ██║██╔════╝
   ██║   ███████║██║     █████╔╝ ██║██╔██╗ ██║██║  ███╗
   ██║   ██╔══██║██║     ██╔═██╗ ██║██║╚██╗██║██║   ██║
   ██║   ██║  ██║███████╗██║  ██╗██║██║ ╚████║╚██████╔╝
   ╚═╝   ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝╚═╝╚═╝  ╚═══╝ ╚═════╝

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) or END … (terminate). Reaches feature phones with zero data.
  • Voice / Airtime / Mobile Data — call flows, airtime top-ups, data bundles.

Official Documentation


1. Credentials

Get username + apiKey from the dashboard. For local dev use the sandbox (username sandbox, sandbox API key).

Bash
AT_USERNAME=sandbox          # your live app username in production
AT_API_KEY=...               # server-side only — never ship to the browser
EnvironmentBase URL
Livehttps://api.africastalking.com/version1
Sandboxhttps://api.sandbox.africastalking.com/version1

2. Install + initialize (Node)

Bash
npm install --save africastalking      # JS/TS
pip install africastalking             # Python
JavaScript
const client = require("africastalking")({
  apiKey: process.env.AT_API_KEY,
  username: process.env.AT_USERNAME, // 'sandbox' for testing
});
const { SMS, USSD } = client;

3. Send SMS

JavaScript
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):

Bash
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:

JavaScript
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.

JavaScript
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):

Bash
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):

Bash
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:

JavaScript
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.

JavaScript
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 version1 REST host: Voice is voice.africastalking.com/call and Mobile Data is bundles.africastalking.com/mobile/data/request (sandbox: voice.sandbox… / bundles.sandbox…). Airtime also flips amount format depending on layer: the SDK takes a numeric amount plus separate currencyCode (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, every Queued/Sent status 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 is 254XXXXXXXXX (no +). Normalize per-API — don't reuse one formatter blindly. See MPESA_PATTERNS.md.
  • Security: AT_API_KEY stays 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.