A comprehensive guide to implementing SMS functionality using Africa's Talking SDK in Node.js/TypeScript, covering environment setup, single and bulk SMS endpoints, phone number normalization, batch processing, and production deployment considerations.
A hands-on guide to working with Africa's Talking SDK to send transactional messages and a controlled bulk batch flow. By the end of this guide, you’ll understand the full SMS delivery flow from your application to a real mobile phone. You will: Set up a minimal but production-relevant TypeScript backend Connect it to Africa’s Talking SMS API using their official SDK Build a POST /send-one endpoint (for transactional messages like OTPs) Build a POST /send-bulk endpoint (for campaigns or notifications) Learn how to safely move from sandbox testing to real production sending 1) Prerequisites to get started a) Africa’s Talking Account. You need an account to access Africas Talking API. Go to: https://account.africastalking.com/ Sign up using your email and verify your account Once logged in, you’ll see a dashboard with products like SMS, Voice, Airtime, etc. b) Sandbox API Key (for testing) Africa’s Talking provides a sandbox environment so you can test without sending real SMS or being charged. To get it: Go to Settings → API Key Generate a new key Copy it immediately (you won’t see it again, its only shown once) This key is what your app uses to authenticate API requests. c) Username (Sandbox vs Production) The username tells Africa’s Talking which environment to route your request to. Using the wrong combination (sandbox key + production username) will cause authentication errors. In sandbox mode, you will use AT_USERNAME=sandbox In production, you use your actual account username (e.g. appname) d) Sender ID / Shortcode This is the name or number that appears as the sender of the SMS for example alphanumerics like SMS_APP or shortcodes like 40123 In sandbox testing, you will use: AT_SENDER_ID=Sandbox In production, your sender ID must be officially applied for through the Product Request menu option after which it will be approved by telecom providers. This approval process can take time (around 3 days to 1 week based on my previous applications) and may require the following: Business registration details - registration certificate and KRA PIN Contact Information for notifications Use-case explanation (e.g. alerts, marketing, OTP) With Africas Talking it costs KES 7,000 to register for each telecom provider (so KES 14,000 for Airtel and Safaricom) e) Node.js 18+ Make sure you have Node installed, run node -v to confirm If not, Download from https://nodejs.org/ Node is preferred because Africa’s Talking SDK supports it well and integrates naturally with TypeScript and Express 2) Minimal Project Setup Run the following commands to prepare the environment to run the requests. You will create a clean workspace for your SMS project, initialize a Node.js project and install the necessary dependencies needed to get stuff running. mkdir at-sms-starter cd at-sms-starter npm init -y npm install africastalking express dotenv npm install -D typescript ts-node @types/node @types/express npx tsc --init africastalking → official SDK to talk to the SMS API express → lightweight web server for endpoints dotenv → loads environment variables from .env npm install -D typescript ts-node @types/node @types/express → Adds TypeScript support and enables running .ts files directly without compiling manually In your package.json ensure you have this: { "scripts": { "dev": "ts-node src/at-sms.ts" } } This will allow you to run your app with npm run dev Next create the following files which wil have the main file and environment variables like AT_USERNAME and AT_SENDER_ID: src/at-sms.ts .env 3) .env Environment variables template and field explanations AT_API_KEY=your_sandbox_or_live_api_key AT_USERNAME=sandbox AT_SENDER_ID=SandboxSender PORT=5000 SMS_BATCH_SIZE=20 SMS_BATCH_DELAY_MS=500 What these fields do AT_API_KEY: authentication key used in API calls AT_USERNAME: decides your account context (sandbox for tests) AT_SENDER_ID: sender identity recipients will see SMS_BATCH_SIZE: recipients to process per batch (for bulk sms) SMS_BATCH_DELAY_MS: pause between batches to avoid spikes 4) Main File with the logic Add the following code block to your src/at-sms.ts file that we created above; // Load environment variables from .env file into process.env import "dotenv/config"; // Import Express types and the Express library import express, { Request, Response } from "express"; // Import the Africa's Talking SDK import AfricasTalking from "africastalking"; const app = express(); app.use(express.json()); /** * Safely read an environment variable. * Throws an error if the variable is missing – this prevents the app from * running with incomplete configuration. / function required(name: string): string { const value = process.env[name]; if (!value) throw new Error(Missing env var: ${name}); return value; } /* * Convert any Kenyan phone number into the international format required * by Africa's Talking API (e.g., +2547XXXXXXXX). * * Why? The API expects numbers starting with +254. Users might enter: * - 0712345678 (local) * - 254712345678 (without +) * - +254712345678 (already correct) * * This function handles all three cases thorugh normalization. / function normalizeKenyanPhone(phone: string): string { // Remove spaces and dashes (e.g., "0712-345-678" -> "0712345678") const cleaned = phone.replace(/\s+/g, "").replace(/-/g, ""); // Already international format with +? if (cleaned.startsWith("+254")) return cleaned; // Starts with 254 but missing the leading +? if (cleaned.startsWith("254")) return +${cleaned}; // Starts with 0 (common local writing) -> replace leading 0 with +254 if (cleaned.startsWith("0")) return +254${cleaned.slice(1)}; // Fallback – return as is (maybe it's already correct, or an error will occur later) return cleaned; } /* * Simple sleep/pause function. * Used to wait between SMS batches so we don't flood the API and within * the API rate limits / function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } // The SDK needs your API key and username. We read them from .env const at = AfricasTalking({ apiKey: required("AT_API_KEY"), username: required("AT_USERNAME")}); // Grab the SMS service from the initialised SDK const sms = at.SMS; // -------------------------------------------------------------- // CORE SMS SENDING FUNCTION // -------------------------------------------------------------- /* * Sends a single SMS using the Africa's Talking SDK. * Handles phone normalisation and message line-ending conversion. */ async function sendOneSms(to: string, message: string) { const senderId = required("AT_SENDER_ID"); // Convert the phone number to the format the API expects const normalizedTo = normalizeKenyanPhone(to); // Africa's Talking recommends using \r\n (CRLF) as line endings. // This replace ensures any \n becomes \r\n. const normalizedMessage = message.replace(/\r?\n/g, "\r\n"); const response = await sms.send({ to: normalizedTo, message: normalizedMessage, from: senderId}); return response; // Contains status, message ID, etc. } // -------------------------------------------------------------- // EXPRESS ROUTE: SEND ONE SMS // -------------------------------------------------------------- app.post("/send-one", async (req: Request, res: Response) => { try { // Extract 'to' and 'message' from the JSON request body const { to, message } = req.body as { to?: string; message?: string }; // Validate input – both fields are required if (!to || !message) { return res.status(400).json({ ok: false, message: "to and message are required" }); } // Send the SMS using our core function const result = await sendOneSms(to, message); return res.status(200).json({ ok: true, result }); } catch (error: any) { // If anything fails, send a clear error message back to the client return res.status(500).json({ ok: false, message: error?.message ?? "Unknown error", response: error?.response?.data ?? null, // Sometimes the SDK provides extra error details }); } }); // -------------------------------------------------------------- // EXPRESS ROUTE: BULK SMS (WITH BATCHING & DELAY) // -------------------------------------------------------------- app.post("/send-bulk", async (req: Request, res: Response) => { // Read batch settings from .env (with sensible defaults) const batchSize = Number(process.env.SMS_BATCH_SIZE || 20); const batchDelayMs = Number(process.env.SMS_BATCH_DELAY_MS || 500); try { // Extract array of phone numbers and the common message const { phones, message } = req.body as { phones?: string[]; message?: string }; // Validate inputs if (!phones || phones.length === 0 || !message) { return res.status(400).json({ ok: false, message: "phones (array) and message are required"}); } // We'll store the result for every phone (success or failure) const results: Array<{ phone: string; success: boolean; data?: any; error?: any }> = []; // ---------------------------------------------------------- // BATCH PROCESSING LOOP // ---------------------------------------------------------- // We split the phones array into chunks of size 'batchSize'. // For each batch we send SMS one by one, then wait before the next batch. for (let i = 0; i < phones.length; i += batchSize) { // Get the current batch (e.g., phones[0..19], then 20..39, ...) const batch = phones.slice(i, i + batchSize); // Send an SMS to each phone in the batch (sequentially, not parallel) for (const phone of batch) { try { const data = await sendOneSms(phone, message); results.push({ phone, success: true, data }); } catch (error: any) { // If this specific phone fails, record the error but continue with others results.push({ phone, success: false, error: error?.response?.data ?? error?.message ?? "Failed"}); } } // After finishing a batch, wait (unless it was the last batch) if (i + batchSize < phones.length) { await sleep(batchDelayMs); } } // ---------------------------------------------------------- // SEND SUMMARY BACK TO CLIENT // ---------------------------------------------------------- const sent = results.filter((r) => r.success).length; const failed = results.length - sent; return res.status(200).json({ ok: true, summary: { total: results.length, sent, failed, batchSize, batchDelayMs }, results, // Detailed per-phone result }); } catch (error: any) { return res.status(500).json({ ok: false, message: error?.message ?? "Unknown error", response: error?.response?.data ?? null}); } }); // -------------------------------------------------------------- // START THE SERVER // -------------------------------------------------------------- const port = Number(process.env.PORT || 4200); app.listen(port, () => { console.log(AT SMS starter running on http://localhost:${port}); console.log(Single send: POST http://localhost:${port}/send-one); console.log(Bulk send: POST http://localhost:${port}/send-bulk); }); 5) Run and Test the app Start the server with npm run dev You should see the console messages indicating the server is running. Test sending one SMS (replace the phone number with a any number) On Windows (Command Prompt) run curl -X POST http://localhost:4200/send-one ^ -H "Content-Type: application/json" ^ -d "{\"to\":\"0712345678\",\"message\":\"Hello from AT sandbox\"}" On macOS / Linux / Git Bash run curl -X POST http://localhost:4200/send-one \ -H "Content-Type: application/json" \ -d '{"to":"0712345678","message":"Hello from AT sandbox"}' Test Sending bulk SMS curl -X POST http://localhost:4200/send-bulk \ -H "Content-Type: application/json" \ -d '{"phones":["0712345678","0722000000"],"message":"Monthly reminder: your account summary is ready."}' Note: In sandbox mode, messages are not actually delivered to real phones. On the africas Talking Dashboard under Bulk SMS -> Outbox, you will be able to view all messages sent through your account (sandbox/production) and their statuses whether successful or not. 6) When to Introduce Queues (BullMQ, RabbitMQ, etc.) This guide uses a simple loop because it is easier for beginners to follow. Move to a queue approach when: recipient lists become large (hundreds/thousands) request-response timeout becomes likely (HTTP requests can time out after ~30 seconds) you need retry persistence across server restarts multiple admins can trigger campaigns concurrently At that stage, keep the same sendOneSms function and move the orchestration into worker jobs. 8) Troubleshooting Symptom Likely cause Fix 401 or auth errors Invalid key/username pair Confirm both are from same environment Message rejected Sender ID not approved Use approved sender in production API says success but user reports no SMS Network/operator delivery delay Check AT message status and logs Too many failures in bulk Rate pressure or invalid numbers Reduce batch size, validate numbers first You do not need a complex architecture to get started. A clean single-file starter teaches the exact moving parts: credentials, send API, formatting, and rate-aware bulk execution. Once this is stable, you can scale safely with queue workers and observability. Africas Talking also have guides and tutorials on their site which you can use for further assistance and reference - Africa's Talking Developer Docs

Comments
Please log in or register to join the discussion