There is no Python-specific difficulty in this category. Every hosted provider is REST, so httpx is the integration. What follows is the surrounding code that makes it production-shaped rather than a snippet.
A client worth keeping
One class, one place that knows the vendor. When you switch providers — and this is a young category, so plan for it — only this file changes.
import osfrom dataclasses import dataclass import httpx class SendError(Exception): def __init__(self, message: str, status: int, retryable: bool): super().__init__(message) self.status = status self.retryable = retryable @dataclass(frozen=True)class SendResult: id: str channel: str class Messaging: def __init__(self, base_url: str | None = None, api_key: str | None = None): self._client = httpx.Client( base_url=base_url or os.environ["IMESSAGE_API_URL"], headers={"Authorization": f"Bearer {api_key or os.environ['IMESSAGE_API_KEY']}"}, # A hung provider must not hold your worker forever. timeout=httpx.Timeout(10.0, connect=5.0), ) def send(self, to: str, text: str) -> SendResult: response = self._client.post("/v1/messages", json={"to": to, "text": text}) if response.is_error: # 429 and 5xx are worth another go; 4xx means the call itself is wrong. retryable = response.status_code == 429 or response.status_code >= 500 raise SendError(response.text, response.status_code, retryable) data = response.json() return SendResult(id=data["id"], channel=data.get("channel", "imessage")) def close(self) -> None: self._client.close()Retries and rate limits
Providers throttle on purpose — hammering a line is what gets the underlying Apple account flagged. Treat a 429 as useful advice rather than an obstacle, and honour Retry-After when the vendor sends it.
import randomimport time from messaging import Messaging, SendError, SendResult def send_with_retry(client: Messaging, to: str, text: str, attempts: int = 4) -> SendResult: for attempt in range(attempts): try: return client.send(to, text) except SendError as err: if attempt == attempts - 1 or not err.retryable: raise # Jitter stops a batch of failures from retrying in lockstep and # rebuilding the exact spike that triggered the throttle. base = (2 ** attempt) * 0.5 time.sleep(base + random.random() * base) raise AssertionError("unreachable")Receiving replies with FastAPI
Inbound webhooks are delivered at least once. Duplicates will happen, so deduplicate on the provider's message id and acknowledge before you do any real work.
import hashlibimport hmacimport jsonimport os from fastapi import BackgroundTasks, FastAPI, Request, Response app = FastAPI()SECRET = os.environ["IMESSAGE_WEBHOOK_SECRET"].encode() @app.post("/webhooks/imessage")async def inbound(request: Request, background: BackgroundTasks) -> Response: # Verify against the exact bytes received, before any JSON round-trip. raw = await request.body() expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, request.headers.get("x-signature", "")): return Response(status_code=401) event = json.loads(raw) # Return immediately; do the work after. A slow handler becomes a provider # retry, which becomes a duplicate message to your customer. background.add_task(handle, event) return Response(status_code=200) async def handle(event: dict) -> None: message_id = event["message_id"] if await already_handled(message_id): return await mark_handled(message_id) await respond(event["from"], event["text"])Bulk sending from a DataFrame
The most common Python use case in this category is 'I have a spreadsheet of customers and I want to text them'. Two things make the difference between that working and getting your line flagged: pace it, and personalise it.
import time import pandas as pd from messaging import Messagingfrom retry import send_with_retry def run_campaign(df: pd.DataFrame, template: str, per_minute: int = 30) -> pd.DataFrame: """Send one personalised message per row, paced to look human.""" client = Messaging() gap = 60 / per_minute results = [] try: for i, row in enumerate(df.itertuples()): text = template.format(first_name=row.first_name) try: result = send_with_retry(client, row.phone, text) results.append({"phone": row.phone, "id": result.id, "error": None}) except Exception as err: # one bad number must not end the run results.append({"phone": row.phone, "id": None, "error": str(err)}) if i < len(df) - 1: time.sleep(gap) finally: client.close() return pd.DataFrame(results)Check consent before the loop, not after
A DataFrame of phone numbers is not a list of people who agreed to hear from you. Filter on your consent column before you send, and honour opt-outs on every run — see consent before you text.
Reading iMessage directly on macOS
If your Python runs on a Mac, you can read the local Messages database without any vendor at all. Useful for analysis and for self-hosted inbound; read-only, and open a copy rather than the live file.
import sqlite3from pathlib import Path DB = Path.home() / "Library" / "Messages" / "chat.db" def recent_messages(limit: int = 50) -> list[dict]: # Read-only URI: never write to the live database Messages.app is using. # Requires Full Disk Access for your terminal or Python binary. conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True) conn.row_factory = sqlite3.Row try: rows = conn.execute( """ SELECT handle.id AS contact, message.text, message.is_from_me, -- Apple epoch is 2001-01-01, stored in nanoseconds. datetime(message.date / 1000000000 + 978307200, 'unixepoch', 'localtime') AS sent_at FROM message JOIN handle ON message.handle_id = handle.ROWID WHERE message.text IS NOT NULL ORDER BY message.date DESC LIMIT ? """, (limit,), ).fetchall() finally: conn.close() return [dict(row) for row in rows]The Node equivalents are in iMessage API for Node.js. For the provider decision itself, start at who sells an iMessage API.
Common questions
- What is the best iMessage API for Python?
- All hosted providers work equally well from Python since they expose REST. Blooio publishes an official Python SDK alongside Node, Go and Java. Otherwise `httpx` or `requests` against the vendor's REST endpoint is the whole integration.
- Is there a Python library for sending iMessages?
- There are vendor SDKs on PyPI and open-source packages that read the local chat database on macOS. There is no official Apple library, because Apple publishes no API.