Skip to main content

Mista Python SDK

Official Python client for the Mista Messaging, Verify and Voice APIs. Full API reference: https://docs.mista.io

pip install mista

Requires Python 3.9+. The only dependency is httpx. Both a synchronous (Mista) and an asynchronous (AsyncMista) client are included.

Quickstart

from mista import Mista

mista = Mista(token="...")  # or set MISTA_API_TOKEN

message = mista.sms.send(to="250780000001", sender_id="YourBrand", message="Your order has shipped")
print(message["uid"], message["status"])

Get your API token in the dashboard under Settings → API. Responses are plain dicts with the same snake_case keys as the docs (typed as TypedDicts in mista.types).

Async:

import asyncio
from mista import AsyncMista

async def main() -> None:
    async with AsyncMista() as mista:
        balance = await mista.account.balance()
        print(balance["remaining_unit"])

asyncio.run(main())

Every method below works the same way on AsyncMista; just await it.

SMS

sms.send sends one message to one recipient. To reach several numbers, or to schedule a send, use a campaign.

message = mista.sms.send(
    to="250780000001",
    sender_id="YourBrand",
    message="Hello",
    type="plain",  # plain | unicode | voice | mms | whatsapp | viber | otp
)
latest = mista.logs.get(message["uid"])
latest["status"]         # "Queued" | "Sent" | "Delivered" | "Undelivered" | "Expired" | "Rejected" | "Failed"
latest["status_detail"]  # why it failed, e.g. "Undelivered (handset unreachable)"; None otherwise

Queued means the carrier has not accepted the message yet and Sent means it was accepted and is waiting for the handset's delivery report. The other five statuses are final. To be told when a message reaches one, register a delivery report webhook instead of polling.

Campaigns

from datetime import datetime

# Broadcast: one message, up to 10,000 numbers
mista.campaigns.bulk(
    sender_id="LOYALTY",
    recipients=["250780000001", "250780000002"],
    message="Double points this weekend!",
    schedule_time=datetime(2026, 12, 24, 9, 0),  # or "2026-12-24 09:00"; account timezone
)

# Personalized: one message per number
mista.campaigns.bulk(
    sender_id="LOYALTY",
    recipients=[
        {"to": "250780000001", "message": "Hi Alice, you have 120 points."},
        {"to": "250780000002", "message": "Hi Bob, you have 45 points."},
    ],
)

# Everyone in one or more contact groups
mista.campaigns.send_to_groups(group_uids=["grp_uid"], sender_id="YourBrand", message="Hi!")

campaign = mista.campaigns.get("campaign_uid")

Message logs

page = mista.logs.list(start_date="2026-10-01", status="Delivered", per_page=50)
page.items        # this page
page.meta.total   # total matches

for message in page:  # walks every remaining page (use `async for` with AsyncMista)
    print(message["uid"], message["status"])

Filters: page, per_page, start_date, end_date (Y-m-d), sender_id, status, sms_type. When nothing matches, you get an empty page.

Account

balance = mista.account.balance()  # {"remaining_unit": ..., "expired_on": ...}
me = mista.account.me()

Contact groups and contacts

group = mista.contact_groups.create("Developers")
mista.contact_groups.list()
mista.contact_groups.get(group["uid"])
mista.contact_groups.update(group["uid"], "Developers KGL")

contact = mista.contacts.create(
    group["uid"],
    phone="250780000001",
    first_name="Alice",
    last_name="Uwase",
    fields={"CITY": "Kigali"},  # custom fields, keyed by the group's field tag
)
mista.contacts.list(group["uid"])
mista.contacts.get(group["uid"], contact["uid"])
mista.contacts.update(group["uid"], contact["uid"], phone="250780000001", first_name="Alicia")
mista.contacts.delete(group["uid"], contact["uid"])

mista.contact_groups.delete(group["uid"])  # also deletes its contacts

Verify (OTP)

verification = mista.verify.start(to="+250780000001", channel="sms")

result = mista.verify.check(sid=verification["sid"], code="123456")
if result["verified"]:
    ...  # signed in
else:
    print(result["reason"])  # e.g. "invalid_code"; a wrong code does not raise

mista.verify.get(verification["sid"])

Voice

token = mista.voice.access_token(platform="ios")["token"]
numbers = mista.voice.numbers()
calls = mista.voice.calls.list(filter="missed", per_page=20)
call = mista.voice.calls.get("call_uid")

Delivery report webhooks

Mista POSTs a signed JSON event to your URL when a message is delivered (message.delivered) or fails (message.failed, with status Undelivered, Expired, Rejected or Failed).

webhook = mista.webhooks.set(url="https://example.com/webhooks/mista")
webhook["secret"]  # "whsec_..." - store it; you need it to verify requests

mista.webhooks.test()  # sends a signed webhook.test event now: {"delivered", "status_code", "error"}
mista.webhooks.get()
mista.webhooks.set(url="https://example.com/webhooks/mista", rotate_secret=True)
mista.webhooks.delete()

Verify every request with the raw body before trusting it:

import os
from flask import Flask, request
from mista import WebhookVerificationError, verify_webhook

app = Flask(__name__)

@app.post("/webhooks/mista")
def mista_webhook():
    try:
        event = verify_webhook(request.get_data(), request.headers.get("Mista-Signature"), os.environ["MISTA_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return "", 400
    if event["type"] == "message.delivered":
        ...  # mark event["data"]["uid"] as delivered
    elif event["type"] == "message.failed":
        ...  # event["data"]["status"] is Undelivered | Expired | Rejected | Failed; reason in status_detail
    return "", 200

With FastAPI, pass await request.body(). verify_webhook checks the HMAC-SHA256 signature and rejects events older than 5 minutes (tolerance=seconds to change, 0 to disable). Answer with any 2xx within 10 seconds; otherwise Mista retries up to 5 more times over about 3 hours. Retries keep the same event["id"], so use it to ignore duplicates. You can also register the URL in the dashboard under Developers.

Errors

Every failure raises a subclass of mista.MistaError:

Exception When
BadRequestError 400, e.g. an invalid phone number
AuthenticationError 401, missing or wrong token
PermissionDeniedError 403
NotFoundError 404
ValidationError 422; field problems are in error.errors
RateLimitError 429 after retries; see error.retry_after
ServerError 5xx
APIError any other API error, including a 200 whose body says "status": "error" (e.g. a contact already in the group)
APIConnectionError / APITimeoutError network failure or timeout
from mista import ValidationError

try:
    mista.sms.send(to="123", sender_id="YourBrand", message="Hi")
except ValidationError as error:
    for problem in error.errors:
        print(problem.field, problem.message)

All API errors carry status, body (the parsed response) and headers.

Retries, timeouts and HTTP client

mista = Mista(max_retries=2, timeout=30.0)
  • 429 Too Many Requests is retried for every request, waiting for Retry-After.
  • Network errors and 5xx responses are retried for GET only, so a send is never duplicated.
  • max_retries=0 turns retries off.
  • Pass http_client=httpx.Client(...) (or httpx.AsyncClient) for proxies or custom transports.
  • Use the client as a context manager, or call close() / await aclose(), to release connections.

Not covered

The retired Push API.

Development

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest && .venv/bin/mypy
MISTA_API_TOKEN=... .venv/bin/python scripts/smoke.py   # read-only: balance + account

License

MIT

Metadata

Release files for mista 0.2.0

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

Source distribution (sdist)

Source distribution for mista 0.2.0
File Size Uploaded
mista-0.2.0.tar.gz 21.6 kB Details

Built distribution (wheel)

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

Total release size: 41.7 kB

Release files / mista-0.2.0.tar.gz

Download URL mista-0.2.0.tar.gz
Size 21.6 kB
Tags Source
SHA-256 checksum
How to use checksums
48a5e748bf2900d1261d3e0d608afa48103f7ed6c9d43ee1023651b53ea862f1
BLAKE2b-256 checksum
How to use checksums
781f2bb3e98cebf27d0abdb61a365d04f910a7aed8ac15970aa0f0805d7fd0f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release files / mista-0.2.0-py3-none-any.whl

Download URL mista-0.2.0-py3-none-any.whl
Size 20.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b9fdc36743a3faa3036d144b82c93bba4651bc0ed4571801d9deda737296b85d
BLAKE2b-256 checksum
How to use checksums
429d265f5dbfa3d092b34571520d249adc9ec848227e2a453b86099f5a05b50c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.13

Release history Release notifications | RSS feed

0.2.1

2 release files

This release

0.2.0 This release

2 release files

0.1.0

2 release files

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