Integrate with AI Assistants
A single page written for coding agents (Cursor, Claude, ChatGPT, Copilot): everything needed to ship a working SendAfrica SMS integration — env setup, complete Python & TypeScript recipes, webhook verification, and error handling.
This page is designed to be pasted into or fetched by an AI coding assistant. It contains everything an agent needs to produce a complete, production-safe integration where the developer only supplies their API key. Deeper reference lives in Send SMS, Errors, Rate Limits & Idempotency, and Webhooks.
#Facts the agent must know
- Base URL:
https://api.sendafrica.online— all endpoints under/v1. - Server-to-server auth: header
X-API-Key: SA-.... The key is shown once at creation; store it in an environment variable, never in code. - Every response uses the envelope
{ success, data, error, request_id, timestamp }. Logrequest_idfor support. - Phone numbers: Tanzanian formats
0712345678and+255712345678are both accepted and normalized to E.164 automatically. Non-TZ numbers are rejected. - Sending is synchronous: a successful response means the message is submitted and has
status: "sent". Final delivery arrives later via webhook (delivered/failed). - Billing is per SMS part in credits (160 chars GSM-7, 70 chars UCS-2 — emoji force UCS-2). Insufficient credits fail with
402 insufficient_credits. - Sender ID: omit it and messages send from the platform default SendAfrika. Custom sender IDs must be registered first — unregistered ones are silently swapped for the fallback sender. See Sender IDs.
- Retries are safe when you pass an
Idempotency-Keyheader (or the SDK'sidempotencyKeyoption); replays return the cached response for 24 h. - Machine-readable contract: fetch openapi.json (OpenAPI 3.1) to generate typed clients or validate requests.
#Environment setup
SENDAFRICA_API_KEY=SA-your-key-here
SENDAFRICA_WEBHOOK_SECRET=whsec_your-webhook-secretNever commit real keys. Load them from the environment; if a key leaks, revoke it from the dashboard immediately.
#Python — complete recipe
pip install sendafricaimport os
from sendafrica import SendAfrica
from sendafrica.exceptions import (
AuthenticationError,
InsufficientCreditsError,
InvalidPhoneError,
RateLimitError,
ServerError,
)
# Reads SENDAFRICA_API_KEY from the environment automatically
client = SendAfrica()
result = client.sms.send(
to="0712345678", # normalized to +255712345678
message="Your OTP is 482910. It expires in 5 minutes.",
# sender="MYBRAND", # optional — must be registered
)
print(result.message_id) # "SA-..."
print(result.status) # "sent"
print(result.credits_used) # 1
balance = client.credits.balance()
print(balance.balance) # remaining creditsThe SDK retries 429/5xx automatically (exponential backoff, max 3). Handle these exceptions explicitly:
from sendafrica.exceptions import (
AuthenticationError,
InsufficientCreditsError,
InvalidPhoneError,
RateLimitError,
SendAfricaError,
)
try:
client.sms.send(to="0712345678", message="Hello")
except InvalidPhoneError:
... # reject the input — not a transient error
except InsufficientCreditsError:
... # top up before retrying
except RateLimitError as e:
... # honor e.retry_after seconds
except AuthenticationError:
... # key revoked or wrong
except SendAfricaError as e:
print(e.status_code, e.request_id) # include request_id in support tickets#TypeScript / Node — complete recipe
npm install sendafricaimport { SendAfricaClient } from "sendafrica";
import { normalizeTzPhone, getSmsPartInfo } from "sendafrica";
const client = new SendAfricaClient({
apiKey: process.env.SENDAFRICA_API_KEY!, // required
timeoutMs: 15_000,
});
const result = await client.sendSms(
{
to: normalizeTzPhone("0712345678"), // "+255712345678"
message: "Your OTP is 482910. It expires in 5 minutes.",
// from: "MYBRAND", // optional — must be registered
},
{ idempotencyKey: "otp-482910" }, // makes retries safe
);
console.log(result.messageId); // "SA-..."
console.log(result.status); // "sent"
console.log(result.creditsUsed); // 1
const balance = await client.getBalance();
console.log(balance.balance);
// Preview parts/credits without sending (zero network calls)
console.log(getSmsPartInfo("Hello 😊")); // { encoding: "UCS-2", parts: 1, ... }#Other official SDKs
If the target stack isn't Python or TypeScript, prefer one of these — they share the same resource model, env-var key resolution (SENDAFRICA_API_KEY), and retry behavior:
| SDK | Install | Requires |
|---|---|---|
| C# (.NET) | dotnet add package SendAfrica | .NET 8 / Std 2.0 |
| PHP | composer require sendafrica/sendafrica | PHP 7.4+ |
| C++ | conan install sendafrica/1.0.1 | C++17, CMake 3.16+ |
| Dart | pub (coming soon) | Dart 3+ |
#Any other language — use the REST API directly
For stacks without an SDK (Go, Java, Ruby, Rust, Elixir, …), call the plain HTTP API — it's a single JSON POST. Generate a typed client from openapi.json with any OpenAPI generator, or use this reference request:
curl -s -X POST https://api.sendafrica.online/v1/sms/ \
-H "X-API-Key: $SENDAFRICA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: otp-482910" \
-d '{
"to": "0712345678",
"message": "Your OTP is 482910. It expires in 5 minutes."
}'{
"success": true,
"data": {
"message_id": "SA-f3b1c2d49e8a4f2bb1c2d3e4f5a6b7c8",
"status": "sent",
"cost": "TZS 35.00",
"credits_used": 1
},
"request_id": "dfffa252-4781-43ff-8e1a-bf01a754d66a",
"timestamp": "2026-06-11T16:24:05Z"
}- Check
successfirst; on failure readerror.codeand map it per the table below. - Retry only with the same
Idempotency-Key; honorRetry-Afteron429. - Bulk sends:
POST /v1/sms/bulkwith{ "to": [...], "message": "..." }(max 100 numbers). - Balance:
GET /v1/credits/balance. Message logs:GET /v1/sms/logs?status=delivered.
#Receiving delivery webhooks
Configure your webhook URL in the dashboard, then verify the X-SendAfrica-Signature header (HMAC-SHA256 of the raw body with your webhook secret) before trusting any payload. Events include sms.delivered and sms.failed; deduplicate by event/message ID because replays can occur.
from sendafrica import SendAfrica
client = SendAfrica() # reads SENDAFRICA_WEBHOOK_SECRET too
@app.post("/webhooks/sendafrica")
async def webhook(request):
event = client.webhooks.parse(
await request.body(),
signature=request.headers.get("X-SendAfrica-Signature"),
) # raises WebhookSignatureError on mismatch
if event.type == "sms.delivered":
await mark_delivered(event.message_id)
elif event.type == "sms.failed":
await alert_ops(event.message_id, event.data)
return {"ok": True}#HTTP error codes worth handling
| Status | Code | Agent action |
|---|---|---|
| 401 | invalid_api_key / token_expired | Check key/env var; never retry as-is |
| 402 | insufficient_credits | Surface a top-up prompt; don't retry |
| 403 | invalid_phone_number | Reject the input; don't retry |
| 429 | rate_limit_exceeded | Wait Retry-After seconds, then retry once |
| 500–503 | gateway_error etc. | Retry with backoff + same Idempotency-Key |
Checklist for the agent
Load the key from the environment → normalize/validate TZ numbers → send with an idempotency key → treat sent as submitted, not delivered → verify webhook signatures before parsing → handle the five error rows above → log request_id on failures.