Claw Messenger
May 31, 2026 · 8 min read

How to Add iMessage to a LangGraph Agent (No Mac Required)

A working Python tutorial for wiring iMessage send and receive into a LangGraph agent. Runs on any Linux box, your laptop, or a serverless runtime. No Mac, no AppleScript, no chat.db.

What you will build: a LangGraph agent that receives iMessage from a phone number you control, runs the message through a single LLM node, and replies as iMessage. The whole thing is roughly 80 lines of Python and runs anywhere Python runs. Claw Messenger handles the Apple side as a managed relay, so the agent process can live on Linux, Windows, a VPS, or a container.

Why no Mac

Most iMessage integrations need a Mac somewhere in the loop. The common patterns are running BlueBubbles on a Mac mini, scripting Messages.app through AppleScript, or reading chat.db on macOS directly. They work but they tie your agent to a piece of Apple hardware that has to stay online, signed in, and patched.

A managed relay moves that hardware off your plate. Claw Messenger owns the upstream Apple connection and gives you a WebSocket plus REST API to send and receive. Your LangGraph process talks to the relay over HTTPS and WSS. No Mac. The same code works on a $5 droplet or a Lambda warm path.

Prerequisites

  • Python 3.11 or newer
  • A Claw Messenger account and API key, from the signup page
  • One phone you control that can text iMessage
  • An OpenAI API key (or swap in any LangChain chat model)

Install the Python dependencies:

pip install langgraph langchain-openai httpx websockets python-dotenv

Step 1: Set up Claw Messenger

Sign up at clawmessenger.com, then open the dashboard. You will see your dedicated agent phone number at the top and an API key section below it. Copy the API key (it starts with cm_live_).

Now register every phone number that should be allowed to text the agent. This is the allowlist. Anything from an unregistered number is dropped at the relay before it reaches your code, which keeps random inbound from showing up in your graph.

curl -X POST https://claw-messenger.onrender.com/api/routes \
  -H "Authorization: Bearer cm_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"phone_number": "+15551234567"}'

Then put both values in a .env file next to the script you are about to write:

CLAW_API_KEY=cm_live_xxxxxxxxxxxxxxxx
OPENAI_API_KEY=sk-...

Step 2: Build the minimal LangGraph agent

The agent is a single-node StateGraph. State holds the inbound text and the LLM reply. One node calls the model. That is enough to demonstrate the wiring, and the graph is the place you would later add tool nodes, memory, or routing.

# agent.py
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI

class State(TypedDict):
    inbound_text: str
    sender: str
    reply: str

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)

def respond(state: State) -> State:
    prompt = (
        "You are a helpful assistant reachable by iMessage. "
        "Keep replies under 300 characters. "
        f"User said: {state['inbound_text']}"
    )
    msg = llm.invoke(prompt)
    return {"reply": msg.content}

builder = StateGraph(State)
builder.add_node("respond", respond)
builder.add_edge(START, "respond")
builder.add_edge("respond", END)
graph = builder.compile()

Step 3: Wire iMessage send and receive

Inbound is a WebSocket. You open one connection to wss://claw-messenger.onrender.com/ws?key=... and the relay pushes every incoming message as a JSON object with type message. Each message has from, text, chatId, and service fields. Pass text into the graph, then post the reply back over REST.

The relay sends a ping every 30 seconds. Respond with pong to keep the connection alive. The connection drops after 90 seconds of silence.

# bridge.py
import asyncio, json, os
import httpx, websockets
from dotenv import load_dotenv
from agent import graph

load_dotenv()

API_KEY = os.environ["CLAW_API_KEY"]
BASE = "https://claw-messenger.onrender.com"
WS_URL = f"wss://claw-messenger.onrender.com/ws?key={API_KEY}"

async def send_imessage(to: str, text: str) -> None:
    async with httpx.AsyncClient(timeout=20) as client:
        r = await client.post(
            f"{BASE}/api/agent/send-message",
            headers={"Authorization": f"Bearer {API_KEY}"},
            json={"phone_number": to, "text": text, "claim_route": True},
        )
        r.raise_for_status()

async def handle_inbound(msg: dict) -> None:
    sender = msg.get("from") or msg.get("from_")
    text = (msg.get("text") or "").strip()
    if not sender or not text:
        return
    result = graph.invoke({"inbound_text": text, "sender": sender, "reply": ""})
    reply = result.get("reply", "").strip()
    if reply:
        await send_imessage(sender, reply)
        print(f"-> {sender}: {reply[:80]}")

async def main() -> None:
    async for ws in websockets.connect(WS_URL, ping_interval=None):
        try:
            print("Connected to Claw Messenger. Waiting for inbound...")
            async for raw in ws:
                msg = json.loads(raw)
                if msg.get("type") == "ping":
                    await ws.send(json.dumps({"type": "pong"}))
                elif msg.get("type") == "message":
                    asyncio.create_task(handle_inbound(msg))
        except websockets.ConnectionClosed:
            print("Reconnecting in 2s...")
            await asyncio.sleep(2)
            continue

if __name__ == "__main__":
    asyncio.run(main())

Two small details worth knowing. The relay returns the sender field as from on the wire and Python reserves that word, so some clients alias it to from_. The handler accepts either. Each inbound is dispatched on its own task so a slow LLM call does not block the next message.

Step 4: Run it end to end

From the directory with both files and your .env:

python bridge.py

You should see:

Connected to Claw Messenger. Waiting for inbound...

Text your Claw Messenger number from the phone you registered. Within a couple of seconds you will get a reply as iMessage and the terminal will log the outbound. If nothing happens, check that the number you texted from matches the one you registered exactly, in E.164 format.

Step 5: Production notes

The allowlist is your spam control. Every phone number that should be allowed to text the agent must be registered first via POST /api/routes. Anything else is dropped before it reaches your code. If you onboard new users dynamically, register their number the moment they sign up.

The WebSocket reconnects automatically in the loop above, but the relay also keeps any messages that arrive while you are disconnected. On reconnect you can replay them by sending {"type": "sync", "since": "<ISO8601 timestamp>"}. That returns every inbound since the timestamp followed by a sync.done with a count. Track the timestamp of the last message you processed and you will not miss anything across restarts.

Outbound errors come back as HTTP. A 429 means you hit the daily new recipient throttle, which protects the line from looking like spam. A 502 means the upstream send failed and is worth retrying once with backoff. A 403 means the recipient phone is not registered to your tenant or the account is paused.

Pricing is usage-based. The starter plan covers 250 messages a month, which is enough to develop and test against. If you build a multi-tenant product on top, the agency tier is $3 per active sub-tenant per month with 1,000 messages each, which keeps unit economics predictable when you grow.

Where to go from here

The graph in this tutorial has one node. The real value of LangGraph starts when you add tool nodes (calendar, search, your own API), conditional edges, and persistent memory keyed on the sender phone. The bridge stays the same. Only the graph grows.

If you are still picking a backend and want to see how Claw Messenger compares to BlueBubbles, Sendblue, and Linq side by side, the iMessage API comparison page walks through pricing, setup time, and trade-offs.

Connect your LangGraph agent to iMessage in 15 minutes. Real phone number, real iMessage, no Mac.

Get your API key

Try Claw Messenger free for 7 daysiMessage API for AI agents. No Mac required.