Skip to main content

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

Metadata

Release files for mojawave 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mojawave 1.0.1
File Size Uploaded Provenance
mojawave-1.0.1.tar.gz 17.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mojawave 1.0.1
File Interpreter ABI Platform
mojawave-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 34.1 kB

Release files / mojawave-1.0.1.tar.gz

Download URL mojawave-1.0.1.tar.gz
Size 17.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c256a2f8a35cc41dc3c556528436f719367f2a55988c1eaaf061cff7a26f8161
BLAKE2b-256 checksum
How to use checksums
fa4f1c99a493e133d0d62d471c4c4ed46199c221cd8d43b085256aaf26c2164b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / mojawave-1.0.1-py3-none-any.whl

Download URL mojawave-1.0.1-py3-none-any.whl
Size 16.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9381c2427f090ba1537142d9b0b8688466419d560cb54a43be97ea9fb1611216
BLAKE2b-256 checksum
How to use checksums
ecb518ecf6424bdebb47d0adc6f9ebe4ac31b7299cf5cdcaaf9350292a47086e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for mojawave 1.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page