iMessage API Python: send and receive without a Mac
Give your Python AI agent a messaging line. This example sends one message and waits for your matching reply.
Choose the right Python approach
Claw Messenger is a managed API for AI agents. Your Python process can run on Linux, Windows, or macOS.
You don't need to run a Mac server. It does not read your personal iMessage history or use your personal Apple account.
For scripts running through your own Mac, py-iMessage documents a different, Mac-only approach.
Prepare one controlled test
Use Python 3.11 or later and an active Claw Messenger account. The seven-day trial requires a card.
In your dashboard, register a phone you control and create an API key. Use its full international number, including the plus sign.
Set CLAW_API_KEY and CLAW_TEST_PHONE in your server environment. Keep the key out of source control and browser code.
python -m pip install "websockets>=13,<17"Send and receive in one Python script
Save this as first_reply.py, then run python first_reply.py. Reply to the received message with its exact test code.
import asyncio
import json
import os
import secrets
from websockets.asyncio.client import connect
async def main():
key = os.environ["CLAW_API_KEY"]
phone = os.environ["CLAW_TEST_PHONE"]
token = secrets.token_hex(4)
async with connect(
"wss://claw-messenger.onrender.com/ws",
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=15,
) as ws:
await ws.send(json.dumps({
"type": "send", "id": token, "to": phone,
"parts": [{"type": "text", "value": f"Reply with {token}"}],
}))
async with asyncio.timeout(180):
while True:
event = json.loads(await ws.recv())
kind = event.get("type")
if kind == "ping":
await ws.send(json.dumps({"type": "pong"}))
elif kind == "error":
raise RuntimeError(event.get("code", "API error"))
elif kind == "send.result" and event.get("id") == token:
if not event.get("ok"):
raise RuntimeError(event.get("error") or "Send failed")
print("Send accepted. Reply from your test phone.")
elif (
kind == "message"
and event.get("from") == phone
and (event.get("text") or "").strip() == token
):
print("Matching reply reached Python.")
return
try:
asyncio.run(main())
except TimeoutError:
print("Connection or reply wait timed out. No reply was proved.")The Authorization header authenticates the socket without placing your key in the URL. The script answers server pings while waiting.
A fresh code prevents an old message from passing this test. Success means your reply reached Python, not just the phone.
What each result means
- Send accepted
- The API accepted your request; a reply is still needed.
- Matching reply
- Your test phone's code reached this process.
- Timeout
- No matching reply arrived during this test.
If connection fails, check the key and account status. If sending fails, check your message allowance and recipient limits.
If the phone receives nothing, check delivery status before retrying. Accepted, delivered, and read are separate states.
Routing can use iMessage, RCS, or SMS. A successful send alone does not prove iMessage delivery.
Move from a test to a client agent
This script exits after one reply. Production needs a persistent listener, reconnect backoff, and duplicate-message handling.
REST sends are also available, but self-serve replies still return over WebSocket. A short-lived HTTP function cannot keep that listener alive.
For separate client workspaces, use Agency subtenants. Each has a dedicated visible number; one line can serve many users.
Keep API keys and message routing separate for each client. Agency reviews your use case before setup.
Plan your Agency setup