Official Python SDK for the MojaWave API — SMS and transactional messaging for Tanzania.
Project description
MojaWave Python SDK
A thin, typed Python client for the MojaWave REST API — send SMS (single, bulk, OTP), check credit balances, and verify webhooks across Tanzania's telco networks (Vodacom, Tigo, Airtel, Halotel).
pip install mojawave
Requires Python 3.8+.
Quickstart
from mojawave import MojaWave
client = MojaWave(api_key="sk_live_mw_...") # or set MOJAWAVE_API_KEY
msg = client.sms.send(
to="+255753276939",
sender="MojaWave",
message="Hello from Mojawave! Your verification code is 1234.",
)
print(msg.id, msg.status)
The client reads MOJAWAVE_API_KEY from the environment when api_key is
omitted. Use an sk_test_mw_ key for the sandbox — no real messages are sent
and no charges apply.
Never expose your live API key in client-side code. Use environment variables and server-side requests only.
Sending SMS
Single message
msg = client.sms.send(
to="+255712345678",
sender="MojaWave", # sender ID (≤11 alphanumeric chars); defaults to MojaWave
message="Your code is 1234.",
webhook_url="https://example.com/webhooks/sms", # optional delivery receipts
schedule_at="2026-07-01T09:00:00Z", # optional ISO-8601 schedule
metadata={"customer_id": "cust98765"}, # optional, echoed back
tags=["onboarding", "verification"], # optional
)
Look up a message
msg = client.sms.get("89b82624-f1a2-4f5e-85b5-102e79a06779")
if msg.delivered:
print("Delivered at", msg.timeline.delivered_at)
elif msg.failed:
print("Failed:", msg.failure_reason)
Bulk send (up to 10,000 recipients)
Bulk jobs run asynchronously — you get a job back immediately, then poll it.
job = client.sms.bulk(
name="Marketing Campaign Q1",
sender="MojaWave",
message="Hello {name}, your code is {code}",
recipients=[
{"to": "+255712345678", "personalization": {"name": "John", "code": "ABC123"}},
{"to": "+255712345679", "personalization": {"name": "Jane", "code": "XYZ789"}},
"+255712345680", # a bare string works too (no personalization)
],
webhook_url="https://example.com/webhooks",
)
print(job.id, job.status, job.total_recipients)
# Poll for progress
job = client.sms.get_bulk(job.id)
print(f"{job.progress_percent}% — {job.sent_count} sent")
Unicode messages have a 70-character per-segment limit (vs. 160 for plain SMS). Plan message length accordingly.
Credits
balances = client.credits.balance()
print(balances.sms.balance, balances.sms.is_low_balance)
print(balances.email.balance)
Webhooks
MojaWave signs every webhook with an X-MojaWave-Signature header (HMAC-SHA256
of the raw body). Always verify against the raw request bytes — parsing to
JSON first can change whitespace and break the check.
from mojawave import construct_event, WebhookVerificationError, SIGNATURE_HEADER
# Flask / Django view
signature = request.headers.get(SIGNATURE_HEADER)
try:
event = construct_event(request.get_data(), signature, WEBHOOK_SECRET)
except WebhookVerificationError:
return "Forbidden", 403
if event.type == "message.delivered":
...
Event types: message.sent, message.delivered, message.failed,
credits.low. See examples/flask_webhook.py for a full handler.
If you only need a boolean, use verify_signature(payload, signature, secret).
Error handling
Every documented HTTP status maps to a typed exception. All inherit from
MojaWaveError.
| Exception | HTTP | Code |
|---|---|---|
InvalidRequestError |
400 | invalid_request |
AuthenticationError |
401 | unauthorized |
InsufficientBalanceError |
402 | insufficient_balance |
UnprocessableError |
422 | unprocessable |
RateLimitError |
429 | rate_limit_exceeded |
ServerError |
5xx | server_error |
APIConnectionError / APITimeoutError |
— | transport failures |
from mojawave import InsufficientBalanceError, RateLimitError
try:
client.sms.send(to="+255712345678", message="hi")
except InsufficientBalanceError:
... # top up
except RateLimitError as e:
time.sleep(e.retry_after or 1)
The client automatically retries 429 and 5xx responses with exponential
backoff (honouring Retry-After), controlled by max_retries (default 2).
Configuration
client = MojaWave(
api_key="sk_live_mw_...",
environment="live", # or "sandbox"
timeout=30.0, # seconds
max_retries=2,
base_url="https://api.mojawave.com/v1",
)
Rate-limit headers from the most recent response are available on
client.rate_limit (.limit, .remaining, .reset). The client is usable as
a context manager to ensure the HTTP session is closed:
with MojaWave() as client:
client.sms.send(to="+255712345678", message="hi")
Development
pip install -e ".[dev]"
pytest
mypy
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mojawave-1.0.0.tar.gz.
File metadata
- Download URL: mojawave-1.0.0.tar.gz
- Upload date:
- Size: 14.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
83149871ebeadd180072010f0de698f795511424a267983dadcc85b277896cfa
|
|
| MD5 |
b93e6f63d83e6caf628cc1460a95117f
|
|
| BLAKE2b-256 |
26776a28f0a843ec226a999caefac470fb6759743c23e2bc0f539ad83a5780e3
|
File details
Details for the file mojawave-1.0.0-py3-none-any.whl.
File metadata
- Download URL: mojawave-1.0.0-py3-none-any.whl
- Upload date:
- Size: 16.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8402b4e807a9f8dfe6a5cd513ac052d11ee63a1525c5793a73ef728cead7c1e
|
|
| MD5 |
b130b3eb88ca5b2fc0ade298cd551fcb
|
|
| BLAKE2b-256 |
04c5bcc30f9b4af3c48586789174286638363063b3c4d59b6318c34d27ff9d9c
|