SMSBulk SMS Tanzania: Top Providers Ranked by Price, Speed & Delivery
Isack Kimaro
Last Updated :
14th March 2026

By Inzari Masood
Product Specialist
Share this article with your friends
Notify Africa SMS API uses RESTful HTTPS endpoint https://api.notify.africa/api/v1/api/messages/send with Bearer token auth. Send single/bulk SMS in Tanzania via phone_number (255 format), message, sender_id. Code examples available in curl, Python (requests), Node.js (fetch), PHP (curl), Go (net/http). Official SDKs for Python, Node.js, PHP, Go simplify integration. Check status via GET /api/v1/api/messages/status/{messageId}. Full docs: https://docs.notify.africa/. Test with free credits after signup.
Modern applications rely heavily on automated messaging.
From OTP verification and transaction alerts to marketing campaigns and appointment reminders, SMS APIs allow software systems to communicate with users instantly.
Instead of sending SMS manually, developers integrate an SMS API gateway into their applications.
This enables:
automated notifications
real-time alerts
account verification messages
system-triggered SMS campaigns
For developers building applications in Tanzania, choosing the right SMS API is critical for reliable delivery, fast integration, and scalable messaging.
In this guide, you will learn:
how SMS APIs work
real SMS API code examples
how to send SMS using different programming languages
best practices for reliable SMS integration
All examples are designed around the Notify Africa SMS API, which is built specifically for businesses and developers sending SMS across Tanzanian networks.
You can explore the full developer documentation here:
https://notify.africa/bulk-sms/developers
Before integrating the Notify Africa SMS API, you need three things in place.
First, a Notify Africa account. Register at notify.africa. Your account gives you access to the dashboard, your API token, and your Sender ID management.
Second, your API token. Once logged in, navigate to your developer settings to retrieve your API token. This is a Bearer token used in the Authorization header of every API request. Store it as an environment variable — never hardcode it in your source files.
Third, a registered Sender ID. In Tanzania, commercial SMS must be sent under a registered Sender ID — a brand name that appears in place of a phone number on the recipient's handset. Notify Africa handles TCRA-compliant Sender ID registration for all clients. You will need your Sender ID's numeric identifier from your dashboard for API calls. Learn more about getting set up at notify.africa/bulk-sms.
The base URL for all Notify Africa API requests is:
All requests must use HTTPS. The API returns JSON responses.
The Notify Africa API uses Bearer token authentication. Every request must include an Authorization header with your API token.
The header format is:
Authorization: Bearer YOUR_API_TOKEN
Replace YOUR_API_TOKEN with the token from your Notify Africa dashboard. This applies to every example in this guide.
A note on security: never commit your API token to version control. Store it in an environment variable (.env file, server environment, or secrets manager) and reference it in your code. This is shown in every example below.
Sign up free → https://account.notify.africa/auth/register
Verify account and get API key from dashboard
Register sender ID (free, fast TCRA compliance—handled in dashboard)
Test with free credits provided on signup
No credit card needed to start coding.
PHP — USING THE OFFICIAL SDK
Install via Composer:
composer require ipfsoftwares/notify-africa-php:dev-main
Send a single SMS:
<?php
use NotifyAfrica\Sms\ClientConfig;
use NotifyAfrica\Sms\NotifyAfricaClient;
use NotifyAfrica\Sms\Request\SingleMessageRequest;
$config = new ClientConfig(
apiToken: getenv('NOTIFY_AFRICA_API_TOKEN'),
baseUri: 'https://api.notify.africa',
);
$client = new NotifyAfricaClient($config);
$response = $client->sendSingleMessage(
new SingleMessageRequest(
phoneNumber: '255754123456',
message: 'Habari! Your order #1042 has been confirmed. Thank you for shopping with us. - ShopName',
senderId: 'YOUR_SENDER_ID_ID',
)
);
echo 'Message ID: ' . $response->getMessageId();
PHP — RAW cURL (NO SDK)
<?php
$apiToken = getenv('NOTIFY_AFRICA_API_TOKEN');
$payload = json_encode([
'phone_number' => '255754123456',
'message' => 'Habari! Your order #1042 has been confirmed. Thank you. - ShopName',
'sender_id' => 'YOUR_SENDER_ID_ID',
]);
$ch = curl_init('https://api.notify.africa/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiToken,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_TIMEOUT => 30,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($httpCode === 200) {
echo 'Sent. Message ID: ' . $data['message_id'];
} else {
echo 'Error: ' . $data['message'];
}
PYTHON — USING REQUESTS
import os
import requests
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
url = 'https://api.notify.africa/v1/sms/send'
headers = {
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
}
payload = {
'phone_number': '255754123456',
'message': 'Habari! Your order #1042 has been confirmed. Thank you. - ShopName',
'sender_id': 'YOUR_SENDER_ID_ID',
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
response.raise_for_status()
data = response.json()
print(f"Sent. Message ID: {data['message_id']}")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e.response.status_code} - {e.response.json()}")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
JAVASCRIPT / NODE.JS — USING FETCH
const sendSMS = async () => {
const apiToken = process.env.NOTIFY_AFRICA_API_TOKEN;
const payload = {
phone_number: '255754123456',
message: 'Habari! Your order #1042 has been confirmed. Thank you. - ShopName',
sender_id: 'YOUR_SENDER_ID_ID',
};
try {
const response = await fetch('https://api.notify.africa/v1/sms/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error ${response.status}: ${error.message}`);
}
const data = await response.json();
console.log('Sent. Message ID:', data.message_id);
} catch (err) {
console.error('SMS send failed:', err.message);
}
};
sendSMS();
cURL — COMMAND LINE
curl -X POST https://api.notify.africa/v1/sms/send \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"phone_number": "255754123456",
"message": "Habari! Your order #1042 has been confirmed. Thank you. - ShopName",
"sender_id": "YOUR_SENDER_ID_ID"
}'PHP — USING THE OFFICIAL SDK
<?php
use NotifyAfrica\Sms\ClientConfig;
use NotifyAfrica\Sms\NotifyAfricaClient;
use NotifyAfrica\Sms\Request\BatchMessageRequest;
$config = new ClientConfig(
apiToken: getenv('NOTIFY_AFRICA_API_TOKEN'),
baseUri: 'https://api.notify.africa',
);
$client = new NotifyAfricaClient($config);
$response = $client->sendBatchMessages(
new BatchMessageRequest(
phoneNumbers: [
'255754123456',
'255784987654',
'255653112233',
'255620445566',
],
message: 'Promotion ya wiki hii: Punguzo la 20% kwa bidhaa zote. Karibu leo. - ShopName',
senderId: 'YOUR_SENDER_ID_ID',
)
);
echo 'Batch ID: ' . $response->getBatchId();
PYTHON — BULK SEND
import os
import requests
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
url = 'https://api.notify.africa/v1/sms/batch'
headers = {
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
'Accept': 'application/json',
}
payload = {
'phone_numbers': [
'255754123456',
'255784987654',
'255653112233',
'255620445566',
],
'message': 'Promotion ya wiki hii: Punguzo la 20% kwa bidhaa zote. Karibu leo. - ShopName',
'sender_id': 'YOUR_SENDER_ID_ID',
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=60)
response.raise_for_status()
data = response.json()
print(f"Batch sent. Batch ID: {data['batch_id']}")
except requests.exceptions.HTTPError as e:
print(f"Error: {e.response.json()}")
JAVASCRIPT — BULK SEND WITH CONTACT LIST FROM ARRAY
const sendBulkSMS = async (phoneNumbers, message) => {
const apiToken = process.env.NOTIFY_AFRICA_API_TOKEN;
const payload = {
phone_numbers: phoneNumbers,
message: message,
sender_id: 'YOUR_SENDER_ID_ID',
};
try {
const response = await fetch('https://api.notify.africa/v1/sms/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
console.log('Bulk send successful. Batch ID:', data.batch_id);
return data.batch_id;
} catch (err) {
console.error('Bulk send failed:', err.message);
}
};
const contacts = ['255754123456', '255784987654', '255653112233'];
const message = 'Promotion ya wiki hii: Punguzo la 20%. Karibu leo. - ShopName';
sendBulkSMS(contacts, message);
LOADING CONTACTS FROM A CSV FILE (PYTHON)
import os
import csv
import requests
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
def normalise_tz_number(number):
"""Normalise a Tanzanian phone number to 255XXXXXXXXX format."""
number = number.strip().replace(' ', '').replace('-', '').replace('+', '')
if number.startswith('0'):
number = '255' + number[1:]
return number
phone_numbers = []
with open('contacts.csv', newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
normalised = normalise_tz_number(row['phone'])
if len(normalised) == 12: # 255 + 9 digits
phone_numbers.append(normalised)
print(f"Loaded {len(phone_numbers)} valid numbers")
payload = {
'phone_numbers': phone_numbers,
'message': 'Promotion ya wiki hii: Punguzo la 20%. Karibu leo. - ShopName',
'sender_id': 'YOUR_SENDER_ID_ID',
}
response = requests.post(
'https://api.notify.africa/v1/sms/batch',
json=payload,
headers={
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
},
timeout=60
)
print(response.json())Start reaching your customers today
Send SMS, email and WhatsApp campaigns in minutes.
Start free trialPHP — OTP SEND WITH CODE GENERATION
<?php
use NotifyAfrica\Sms\ClientConfig;
use NotifyAfrica\Sms\NotifyAfricaClient;
use NotifyAfrica\Sms\Request\SingleMessageRequest;
function generateOtp(int $digits = 6): string {
return str_pad(
(string) random_int(0, (10 ** $digits) - 1),
$digits,
'0',
STR_PAD_LEFT
);
}
function sendOtp(string $phoneNumber, string $otp): bool {
$config = new ClientConfig(
apiToken: getenv('NOTIFY_AFRICA_API_TOKEN'),
baseUri: 'https://api.notify.africa',
);
$client = new NotifyAfricaClient($config);
$message = "Your verification code is {$otp}. "
. "It expires in 5 minutes. "
. "Do not share this code. - AppName";
try {
$client->sendSingleMessage(
new SingleMessageRequest(
phoneNumber: $phoneNumber,
message: $message,
senderId: 'YOUR_SENDER_ID_ID',
)
);
return true;
} catch (\Exception $e) {
error_log('OTP send failed: ' . $e->getMessage());
return false;
}
}
// Usage
$otp = generateOtp(6);
$sent = sendOtp('255754123456', $otp);
// Store $otp against the user session/phone number with an expiry timestamp
PYTHON — OTP WITH EXPIRY STORED IN REDIS
import os
import random
import redis
import requests
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
def generate_otp(digits=6):
return str(random.randint(10 ** (digits - 1), 10 ** digits - 1))
def send_otp(phone_number, otp):
message = (
f"Your code is {otp}. "
f"Valid for 5 minutes. "
f"Do not share. - AppName"
)
response = requests.post(
'https://api.notify.africa/v1/sms/send',
json={
'phone_number': phone_number,
'message': message,
'sender_id': 'YOUR_SENDER_ID_ID',
},
headers={
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
},
timeout=15
)
if response.status_code == 200:
# Store OTP in Redis with 5-minute expiry
redis_client.setex(f'otp:{phone_number}', 300, otp)
return True
return False
def verify_otp(phone_number, submitted_otp):
stored_otp = redis_client.get(f'otp:{phone_number}')
if stored_otp and stored_otp == submitted_otp:
redis_client.delete(f'otp:{phone_number}') # Invalidate after use
return True
return FalsePYTHON — PERSONALISED PAYMENT REMINDERS FROM A DATABASE
import os
import requests
import psycopg2
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
def send_payment_reminders():
conn = psycopg2.connect(os.environ.get('DATABASE_URL'))
cursor = conn.cursor()
cursor.execute("""
SELECT first_name, phone_number, amount_due, due_date
FROM loan_accounts
WHERE due_date = CURRENT_DATE + INTERVAL '1 day'
AND status = 'active'
""")
customers = cursor.fetchall()
results = {'sent': 0, 'failed': 0}
for first_name, phone_number, amount_due, due_date in customers:
message = (
f"Habari {first_name}, mkopo wako wa TZS {amount_due:,.0f} "
f"unastahili kulipwa {due_date.strftime('%d %B %Y')}. "
f"Lipa kupitia M-Pesa au benki. "
f"Maswali? Piga: 0754000000. - YourSACCO"
)
response = requests.post(
'https://api.notify.africa/v1/sms/send',
json={
'phone_number': phone_number,
'message': message,
'sender_id': 'YOUR_SENDER_ID_ID',
},
headers={
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
},
timeout=30
)
if response.status_code == 200:
results['sent'] += 1
else:
results['failed'] += 1
print(f"Failed for {phone_number}: {response.json()}")
conn.close()
print(f"Done. Sent: {results['sent']}, Failed: {results['failed']}")
send_payment_reminders()JAVASCRIPT — SCHEDULED SEND WITH ISO 8601 TIMESTAMP
const scheduleSmsCampaign = async (phoneNumbers, message, scheduledAt) => {
// scheduledAt must be ISO 8601 with timezone offset
// e.g. '2025-04-15T09:00:00+03:00' for 9am Tanzania time (EAT = UTC+3)
const apiToken = process.env.NOTIFY_AFRICA_API_TOKEN;
const payload = {
phone_numbers: phoneNumbers,
message: message,
sender_id: 'YOUR_SENDER_ID_ID',
scheduled_at: scheduledAt,
};
try {
const response = await fetch('https://api.notify.africa/v1/sms/batch', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (!response.ok) throw new Error(data.message);
console.log('Campaign scheduled. Batch ID:', data.batch_id);
return data.batch_id;
} catch (err) {
console.error('Schedule failed:', err.message);
}
};
// Schedule a Wednesday 9am campaign (Tanzania EAT = UTC+3)
const contacts = ['255754123456', '255784987654'];
const message = 'Ofa ya wiki: Punguzo la 15% kwa wateja wetu wote. Karibu leo. - ShopName';
scheduleSmsCampaign(contacts, message, '2025-04-16T09:00:00+03:00');Always include the +03:00 offset in your timestamp. Tanzania is East Africa Time (EAT), UTC+3. Omitting the offset will schedule delivery three hours early.
PYTHON — CHECK SINGLE MESSAGE STATUS
import os
import requests
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
def check_message_status(message_id):
url = f'https://api.notify.africa/v1/sms/status/{message_id}'
response = requests.get(
url,
headers={
'Authorization': f'Bearer {api_token}',
'Accept': 'application/json',
},
timeout=15
)
if response.status_code == 200:
data = response.json()
print(f"Status: {data['status']}")
print(f"Delivered at: {data.get('delivered_at', 'Not yet')}")
print(f"Network: {data.get('network', 'Unknown')}")
return data
else:
print(f"Error: {response.json()}")
return None
check_message_status('YOUR_MESSAGE_ID_HERE')
PHP — CHECK BATCH DELIVERY SUMMARY
<?php
function checkBatchStatus(string $batchId): array {
$apiToken = getenv('NOTIFY_AFRICA_API_TOKEN');
$ch = curl_init("https://api.notify.africa/v1/sms/batch/{$batchId}/status");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiToken,
'Accept: application/json',
],
CURLOPT_TIMEOUT => 15,
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
$deliveryRate = round(($response['delivered'] / $response['total']) * 100, 1);
echo "Total: {$response['total']}\n";
echo "Delivered: {$response['delivered']}\n";
echo "Failed: {$response['failed']}\n";
echo "Pending: {$response['pending']}\n";
echo "Delivery rate: {$deliveryRate}%\n";
return $response;
}
checkBatchStatus('YOUR_BATCH_ID_HERE');PHP — WEBHOOK ENDPOINT
<?php
// webhook.php — register this URL in your Notify Africa dashboard
header('Content-Type: application/json');
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
if (!$data) {
http_response_code(400);
exit;
}
// Return 200 IMMEDIATELY before any heavy processing
http_response_code(200);
echo json_encode(['received' => true]);
// Flush response to client before continuing
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
// Safe to take time here — client has already received 200
$messageId = $data['message_id'] ?? null;
$status = $data['status'] ?? null;
$phone = $data['phone_number'] ?? null;
$network = $data['network'] ?? null;
if ($messageId && $status) {
$pdo = new PDO(getenv('DATABASE_URL'));
$stmt = $pdo->prepare(
'UPDATE sms_logs
SET status = ?,
network = ?,
updated_at = NOW()
WHERE message_id = ?'
);
$stmt->execute([$status, $network, $messageId]);
}
NODE.JS — EXPRESS WEBHOOK HANDLER WITH IDEMPOTENCY CHECK
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/sms-delivery', async (req, res) => {
// Respond immediately — never make the provider wait
res.status(200).json({ received: true });
const { message_id, status, phone_number, network, delivered_at } = req.body;
if (!message_id || !status) return;
try {
// Idempotency check — prevent duplicate processing on retried webhooks
const existing = await db.query(
'SELECT id FROM sms_logs WHERE message_id = $1 AND status = $2',
[message_id, status]
);
if (existing.rows.length > 0) {
console.log(`Duplicate webhook for ${message_id}, skipping`);
return;
}
await db.query(
`UPDATE sms_logs
SET status = $1,
network = $2,
delivered_at = $3,
updated_at = NOW()
WHERE message_id = $4`,
[status, network, delivered_at, message_id]
);
console.log(`Delivery update: ${message_id} → ${status} on ${network}`);
} catch (err) {
console.error('Webhook processing error:', err.message);
}
});
app.listen(3000);PHP — COMPREHENSIVE ERROR HANDLING WITH THE OFFICIAL SDK
<?php
use NotifyAfrica\Sms\ClientConfig;
use NotifyAfrica\Sms\NotifyAfricaClient;
use NotifyAfrica\Sms\Request\SingleMessageRequest;
use NotifyAfrica\Sms\Exception\AuthenticationException;
use NotifyAfrica\Sms\Exception\ValidationException;
use NotifyAfrica\Sms\Exception\HttpRequestException;
function sendSmsWithFullErrorHandling(
string $phoneNumber,
string $message,
string $senderId
): array {
$config = new ClientConfig(
apiToken: getenv('NOTIFY_AFRICA_API_TOKEN'),
baseUri: 'https://api.notify.africa',
timeout: 30.0,
connectTimeout: 10.0,
);
$client = new NotifyAfricaClient($config);
try {
$response = $client->sendSingleMessage(
new SingleMessageRequest(
phoneNumber: $phoneNumber,
message: $message,
senderId: $senderId,
)
);
return [
'success' => true,
'message_id' => $response->getMessageId(),
];
} catch (AuthenticationException $e) {
// Token invalid or account suspended — do NOT retry, alert DevOps
error_log('[CRITICAL] Notify Africa auth failed: ' . $e->getMessage());
return ['success' => false, 'error' => 'authentication_failed'];
} catch (ValidationException $e) {
// Bad phone number, missing field — do NOT retry without fixing the data
error_log('[WARNING] Validation error for ' . $phoneNumber . ': ' . $e->getMessage());
return ['success' => false, 'error' => 'validation_error', 'detail' => $e->getMessage()];
} catch (HttpRequestException $e) {
// Network/timeout issue — safe to retry with backoff
error_log('[ERROR] HTTP error: ' . $e->getMessage());
return ['success' => false, 'error' => 'network_error'];
} catch (\Exception $e) {
error_log('[ERROR] Unexpected error: ' . $e->getMessage());
return ['success' => false, 'error' => 'unexpected_error'];
}
}
Start reaching your customers today
Send SMS, email and WhatsApp campaigns in minutes.
Start free trialimport os
import time
import requests
from requests.exceptions import RequestException
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')
def send_sms_with_retry(phone_number, message, sender_id, max_retries=3):
url = 'https://api.notify.africa/v1/sms/send'
headers = {
'Authorization': f'Bearer {api_token}',
'Content-Type': 'application/json',
}
payload = {
'phone_number': phone_number,
'message': message,
'sender_id': sender_id,
}
for attempt in range(1, max_retries + 1):
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
# Authentication error — do not retry
raise ValueError('Authentication failed. Check your API token.')
elif response.status_code == 422:
# Validation error — do not retry without fixing the data
raise ValueError(f"Validation error: {response.json()}")
elif response.status_code == 429:
# Rate limited — wait longer before retrying
wait_time = 2 ** attempt * 2
print(f"Rate limited. Waiting {wait_time}s (attempt {attempt}/{max_retries})")
time.sleep(wait_time)
else:
# Server error — retry with exponential backoff
wait_time = 2 ** attempt
print(f"Server error {response.status_code}. Retry {attempt}/{max_retries} in {wait_time}s")
time.sleep(wait_time)
except RequestException as e:
wait_time = 2 ** attempt
print(f"Network error on attempt {attempt}: {e}. Retrying in {wait_time}s")
time.sleep(wait_time)
raise Exception(f"Failed to send SMS after {max_retries} attempts")CREATING A .ENV FILE
# .env — never commit this file to version control
NOTIFY_AFRICA_API_TOKEN=your_api_token_here
DATABASE_URL=your_database_connection_string_hereAdd to .gitignore immediately:
echo ".env" >> .gitignorePYTHON — LOADING .ENV WITH PYTHON-DOTENV
pip install python-dotenv
from dotenv import load_dotenv
import os
load_dotenv()
api_token = os.environ.get('NOTIFY_AFRICA_API_TOKEN')PHP — LOADING .ENV WITH VLUCAS/PHPDOTENV
composer require vlucas/phpdotenv
<?php
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
$apiToken = $_ENV['NOTIFY_AFRICA_API_TOKEN'];
NODE.JS — LOADING .ENV WITH DOTENV
npm install dotenv
require('dotenv').config();
const apiToken = process.env.NOTIFY_AFRICA_API_TOKEN;For production servers, set environment variables directly on the server — through your hosting panel, Docker environment, or a secrets manager — rather than deploying a .env file.
What is the base URL for the Notify Africa SMS API?
The base URL is https://api.notify.africa. All endpoints use HTTPS. Developer documentation, including full endpoint paths, request formats, and response structures, is at notify.africa/bulk-sms/developers.
Is there an official SDK for the Notify Africa API?
Yes. An official PHP 8.1+ SDK is available via Composer:
composer require ipfsoftwares/notify-africa-php:dev-mainIt provides typed request and response objects, domain-specific exceptions for authentication, validation, and HTTP errors, and configurable timeouts. It is maintained by iPF Softwares, the ISO 27001:2022 certified company behind Notify Africa.
What phone number format does the Notify Africa API expect?
All Tanzanian numbers must be formatted as 255XXXXXXXXX — country code 255 followed by nine digits, no leading zero, no plus sign, no spaces. The local format 0754123456 becomes 255754123456. Here is the normalisation function used in the examples above:
def normalise_tz_number(number):
number = number.strip().replace(' ', '').replace('-', '').replace('+', '')
if number.startswith('0'):
number = '255' + number[1:]
return number
function normaliseTzNumber(string $number): string {
$number = preg_replace('/[\s\-\+]/', '', $number);
if (str_starts_with($number, '0')) {
$number = '255' . substr($number, 1);
}
return $number;
}
How do I get a Sender ID for my API integration?
Register at notify.africa. During onboarding, Notify Africa handles TCRA-compliant Sender ID registration across all four Tanzanian networks. Once registered, your Sender ID is assigned a numeric identifier which you pass as the sender_id parameter in API calls.
Can I send personalised messages via the API?
Yes. The API accepts a plain text message body, so you build each personalised string in your application before the API call. See the personalised payment reminder example earlier in this guide for a complete database-driven implementation.
What is the difference between single send and batch send?
The single send endpoint accepts one phone_number. The batch endpoint accepts an array of phone_numbers and processes them efficiently as a group, returning a single batch_id. For any campaign with more than a handful of recipients, always use the batch endpoint. Looping the single endpoint is inefficient and hits rate limits quickly.
How do I handle errors correctly in production?
Match your response strategy to the error type. A 401 means stop and fix your credentials — do not retry. A 422 means your data is wrong — do not retry without cleaning it. A 429 means you are rate-limited — retry with exponential backoff. A 5xx means a server issue — retry with backoff. The PHP SDK exceptions map directly to these categories. See the error handling section above for complete examples.
Integrating SMS into your Tanzanian app or system doesn’t have to be complicated. With Notify Africa’s clear endpoints, ready code examples in curl/Python/Node.js/PHP/Go, and official SDKs, you can send reliable messages—OTPs, alerts, reminders—in no time.
Enjoy 99.9% delivery, TCRA compliance, and hands-on local support that understands Tanzanian networks and needs.
Ready to code?
→ Get your API key + free credits: https://account.notify.africa/auth/register
→ Dive into full docs: https://docs.notify.africa/
→ Need help with your integration? Book a free session: https://notify.africa/contact
Let’s get your SMS flowing reliably today. Your next feature is just a few lines of code away!
A solution by iPF Softwares, an ISO 27001:2022 certified software development company.

.webp)