Skip to Content
SDKsPython

Python SDK

The official Python client for the SendAfrica REST API. It provides SendAfrica (sync, requests-based) and AsyncSendAfrica (async, httpx-based) clients with typed dataclass models, full phone normalization, local SMS part analysis, and a thin CLI.

  • Package: sendafrica — Python 3.9+
  • Dependencies: requests (always); httpx for async
  • Source: github.com/SendAfrica/Python-SDK

Install

pip install sendafrica

For async support (FastAPI, asyncio workers):

pip install sendafrica[async]

For the full SDK guide, see the official docs at sdk.sendafrica.online/sdks/python .

Quickstart

import os from sendafrica import SendAfrica client = SendAfrica(api_key=os.environ["SENDAFRICA_API_KEY"]) result = client.sms.send( to="0712345678", message="Your verification code is 4921.", ) print(result.message_id, result.status, result.credits_used)

Place the API key in SENDAFRICA_API_KEY or pass it directly to the constructor. Keep credentials server-side and out of version control.

Client setup

from sendafrica import SendAfrica, AsyncSendAfrica client = SendAfrica( api_key="SA-...", # or omit to read SENDAFRICA_API_KEY base_url="https://api.sendafrica.online/v1", timeout=10, # request timeout in seconds max_retries=3, # retry on 429 / 5xx / network errors debug=True, # prints request/response summaries webhook_secret="whsec_...", # used by client.webhooks for signature verification ) # Async variant — same resource API, but every call is awaitable async_client = AsyncSendAfrica(api_key="SA-...") await async_client.sms.send(to="0712345678", message="Async send") await async_client.aclose() # close httpx transport when done

The async client requires httpx (pip install sendafrica[async]). Without it, AsyncSendAfrica raises ImportError at construction.

Sending SMS

Single message

Phone numbers are normalized to E.164 automatically; invalid numbers raise InvalidPhoneError before any network call:

result = client.sms.send( to="0712345678", message="Welcome to SendAfrica!", sender="SENDAFRICA", # optional registered sender ID ) print(result.message_id, result.credits_used)
ParameterTypeRequiredNotes
tostringYesAccepted formats: 0712345678, 255..., +255...
messagestringYesRequired
senderstringNoRegistered sender ID; omits from from the payload when absent

Returns an SMSResult dataclass: message_id, status, credits_used, cost, to.

Client-side send many

N distinct messages, rate limited, with per-message failure collection:

results = client.sms.send_many( messages=[ {"to": "0712345678", "message": "Hi Ali"}, {"to": "0765432109", "message": "Hi Zainab"}, ], rate_limit_per_sec=10.0, # default 10 sender="MYBRAND", # applied to all unless per-message overrides ) print(results.sent_count, results.failed_count) for r in results.results: print(r.message_id, r.to) for f in results.failed: print(f["index"], f["to"], f["error"])

Server-side bulk SMS

Send one message to many recipients in a single API call. The SDK caps at 100 recipients; the server returns partial failures per recipient and refunds them individually.

bulk = client.sms.bulk( to=["0712345678", "0765432109"], message="Black Friday starts now!", sender="MYBRAND", ) print(bulk.total, bulk.sent, bulk.failed) for r in bulk.results: print(r.to, r.status, r.message_id, r.error)

Message logs

logs = client.sms.logs( page=1, per_page=25, status="delivered", # pending | sent | delivered | failed search="0712345678", date_from="2026-09-01T00:00:00Z", ) print(logs.total, logs.page, logs.total_pages) for entry in logs.items: print(entry.to_phone, entry.status, entry.credits_used, entry.sent_at)

Client-side SMS analysis

analysis = client.sms.analyze("Hello world from SendAfrica 🇹🇿") print(analysis.encoding, analysis.parts, analysis.credits) # UCS-2 1 1 (emoji forces Unicode encoding)

Returns an SMSAnalysis dataclass: encoding (“GSM-7” or “UCS-2”), characters, septets, parts, credits. No network call — runs entirely locally.

Credits

balance = client.credits.balance() print(balance.balance, balance.account_id) history = client.credits.history(page=1, per_page=25) for txn in history: print(txn.type, txn.amount, txn.balance_after, txn.description)

Rates

The rate card is public (no auth required).

rates = client.rates.list() for r in rates: print(r.name, r.iso2, r.rate_tzs) tz = client.rates.get("TZ") print(tz.name, tz.rate_tzs)

RateInfo dataclasses carry name, iso2, dial_code, rate_tzs, and an optional provider_bulk_sms snapshot of Africa’s Talking reference pricing.

Sender IDs

reqs = client.sender_ids.requirements() print(reqs.eligibility.can_request) print([r.name for r in reqs.countries]) usable = client.sender_ids.usable() for sid in usable: print(sid.name, sid.type, sid.is_default) list = client.sender_ids.list() new_id = client.sender_ids.create( name="MYBRAND", country="TZ", purpose="Marketing", sample_message="Welcome to MYBRAND", documents=[{"requirement_uid": "...", "filename": "doc.pdf", "content_base64": "..."}], ) print(new_id.id, new_id.status) one = client.sender_ids.get(new_id.id) client.sender_ids.set_default(new_id.id)

Payments (vouchers)

rate = client.payments.rate() print(rate.min_amount_tzs, [(t.max_amount_tzs, t.rate_tzs_per_credit) for t in rate.tiers]) payment = client.payments.create( amount=10000, # TZS — converted to credits server-side provider="snippe", # or "manual" phone="0712345678", # required for mobile-money providers ) print(payment.id, payment.status, payment.credit_amount)

Declared-payer OTP flow (for payer numbers different from the account phone):

client.payments.send_declared_phone_otp("0765432109") client.payments.verify_declared_phone_otp("0765432109", "4921")

Webhooks

Parse and optionally verify an incoming delivery event. The signature is an HMAC-SHA256 hex digest of the raw body in the X-SendAfrica-Signature header.

from flask import request, jsonify @app.route("/webhooks/sms", methods=["POST"]) def handle_webhook(): payload = request.get_data(as_text=True) signature = request.headers.get("X-SendAfrica-Signature", "") try: event = client.webhooks.parse(payload, signature=signature) except Exception: return jsonify(error="invalid signature"), 401 print(event.type, event.message_id, event.data) return jsonify(ok=True), 200

When a webhook_secret was provided at client construction, passing secret= to parse is optional. You can verify a raw payload without parsing with:

client.webhooks._verify_signature(raw_body, signature, secret)

CLI

After install, a sendafrica command is available:

export SENDAFRICA_API_KEY="SA-..." sendafrica balance sendafrica sms send --to 0712345678 --message "Hello from CLI" --sender MYBRAND sendafrica sms logs --status delivered --per-page 50 sendafrica rates sendafrica sender-ids list sendafrica sender-ids requirements sendafrica sender-ids usable

The CLI reads the API key from the environment. Errors are reported to stderr and exit with code 1.

Async (FastAPI example)

from fastapi import FastAPI from sendafrica import AsyncSendAfrica app = FastAPI() client = AsyncSendAfrica(api_key="SA-...") @app.on_event("shutdown") async def shutdown(): await client.aclose() @app.post("/send") async def send(): result = await client.sms.send(to="0712345678", message="Async hello") return {"message_id": result.message_id}

Error handling

All SDK errors inherit from SendAfricaError, so a broad catch is always safe:

from sendafrica import SendAfricaError try: client.sms.send(to="invalid", message="test") except SendAfricaError as e: print(e.status_code, e.request_id, e.message)
ExceptionHTTPMeaning
AuthenticationError401Missing or invalid API key
ValidationError400/422Bad payload, missing fields
InvalidPhoneError—Fails phone validation before network call
InsufficientCreditsError402Not enough SMS credits
RateLimitError429Rate limited; has retry_after attribute
NotFoundError404Resource does not exist
ServerError5xxTransient API failure
APIConnectionError—Network failure or timeout (requests transport)
WebhookSignatureError—Signature verification failed

Every exception carries .status_code, .request_id, and .message (available on all; .status_code is None for local transport errors).

Retries and backoff

Both transports retry 429/5xx responses and network-level requests.RequestError up to max_retries times (default 3). Backoff starts at 500 ms and doubles to a maximum of 8 s. The transport honors Retry-After on 429 responses. Default timeout is 10 seconds.

Reference

Resource / MethodEndpointReturns
sms.send(to, message, *, sender)POST /smsSMSResult
sms.send_many(messages, *, sender, rate_limit_per_sec)Loops POST /smsBulkSMSResult
sms.bulk(to, message, *, sender)POST /sms/bulkBulkSendResult
sms.logs(page, per_page, status, search, date_from)GET /sms/logsMessageLogListResponse
sms.analyze(message)— (local)SMSAnalysis
credits.balance()GET /credits/balanceCreditBalance
credits.history(page, per_page)GET /credits/historyList[CreditTransaction]
rates.list()GET /ratesList[RateInfo]
rates.get(country)GET /rates/{country}RateInfo
sender_ids.requirements()GET /sender-ids/requirementsSenderIDRequirements
sender_ids.usable(*, provider)GET /sender-ids/usableList[UsableSenderID]
sender_ids.list()GET /sender-idsList[SenderID]
sender_ids.create(...)POST /sender-idsSenderID
sender_ids.get(sender_id)GET /sender-ids/{id}SenderID
sender_ids.set_default(sender_id)PUT /sender-ids/defaultSenderID
payments.create(amount, provider, ...)POST /vouchersPayment
payments.rate()GET /vouchers/rateVoucherRate
webhooks.parse(payload, *, signature, secret)— (verify + parse)WebhookEvent

See the webhooks and SMS API references for the underlying REST contract.

Last updated on