Connect your AI agent to iMessage, RCS & SMS via WebSocket.
Claw Messenger is a relay that connects your AI agent to iMessage, RCS, and SMS. Your agent connects via WebSocket and sends/receives messages in real time. Phone numbers are provisioned through our partner network — you register them in the dashboard, and we handle the carrier integration.
For the fastest path into production, pair these docs with the OpenClaw iMessage setup guide, the iMessage API comparison, and the iMessage on Android guide if you are evaluating consumer bridges like BlueBubbles or AirMessage.
Before you wire a full workflow, prove one controlled thread. Setup only counts when one controlled reply reaches your agent, webhook, or backend.
If you do not own a Mac or daily-drive Android, read the no-Mac iMessage guide to understand the supported agent API path before you start.
Create your Claw Messenger account and start the trial from the dashboard. Keep this first run small. The goal is to prove that your agent can participate in a real text thread before you build reminders, support follow-up, sales replies, or customer workflows.
Copy the API key from the dashboard and put it into your agent, script, or backend. Treat it like a password. Do not paste it into public logs, screenshots, analytics tools, or support messages.
const ws = new WebSocket("wss://claw-messenger.onrender.com/ws?key=YOUR_API_KEY");Use a phone number you control and can answer right now. Claw Messenger's reliable setup path is number-based today, so an iMessage-linked email address by itself is not a supported production setup.
Keep customer numbers out of the first run. This test is for proving routing and replies, not for a real user workflow.
curl -X POST https://claw-messenger.onrender.com/api/routes \
-H "Authorization: Bearer cm_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+1234567890"}'Send a short test message from your agent. Check the send.result event: if ok is false, fix that error before continuing. If ok is true, continue to the reply step. The send step alone is not the full proof.
Recommended test text:
This is my Claw Messenger test from an AI agent. Reply yes if you got it.Example WebSocket send:
const ws = new WebSocket("wss://claw-messenger.onrender.com/ws?key=YOUR_API_KEY");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "send",
id: "first-test-1",
to: "+1234567890",
parts: [
{
type: "text",
value: "This is my Claw Messenger test from an AI agent. Reply yes if you got it."
}
]
}));
};ok: true means Claw Messenger accepted the send request or reached a non-error send state. It does not, by itself, prove the message reached the phone.
For the first test, keep going until you see the reply come back into your agent, webhook, or backend. That reply is the proof that the text thread works both ways.
Reply from the test phone with yes, then watch your agent, webhook, or backend for an inbound message event.
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
console.log("Reply reached the agent:", data.text);
}
};The quickstart is complete when that controlled reply reaches your agent, webhook, or backend.
If the reply reaches your agent, choose the workflow you want to keep active: reminders, coaching check-ins, support follow-up, sales or onboarding replies, or product notifications that need a human response.
If the reply does not appear, keep the test narrow: confirm the WebSocket is still connected, confirm the phone number is registered to the account, confirm the reply was sent in the same thread, and log inbound WebSocket events for type: "message". For the full event reference, see Receiving Messages.
Keep the same controlled first-thread proof. These framework packages are source-ready in the repository, but they are not published to npm or PyPI yet.
Build and pack the source-ready community node, then use Manual Trigger → Send Message. The node sends the test but does not trigger on inbound replies, so keep a named WebSocket consumer, webhook, or backend connected. Setup is proven only when the matching reply reaches that consumer.
Build the source-ready Python wheel, then invoke the Claw Messenger tool with one controlled phone and message. Keep the tool call waiting. Setup is proven only when it returns proof_status: reply_received for the matching inbound reply.
Build the source-ready Python wheel, then prove the tool directly before adding it to a crew. Keep the tool call waiting. Setup is proven only when it returns proof_status: reply_received for the matching inbound reply.
Using OpenClaw? Install the plugin instead: openclaw plugins install @emotion-machine/claw-messenger. See the setup guide.
You register the phone numbers that should be able to communicate with your agent. When a registered number texts the agent, the relay routes the message to your account via WebSocket. Unregistered numbers are ignored.
POST /api/routescurl -X POST https://claw-messenger.onrender.com/api/routes \
-H "Authorization: Bearer cm_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone_number": "+1234567890"}'
# Response:
# {"ok": true, "phone_number": "+1234567890", "already_claimed": false}wss://claw-messenger.onrender.com/ws?key=YOUR_API_KEYThe API key is passed as a key query parameter. Generate keys in the dashboard or via the REST API.
ping/pong every 30 seconds to stay aliveSend {"type": "ping"} to keep the connection alive. The server responds with {"type": "pong"}. The server also sends pings — respond with pong to avoid disconnection.
// Client -> Server
{
"type": "send",
"id": "msg-1",
"to": "+1234567890",
"parts": [{ "type": "text", "value": "Hello!" }],
"service": "iMessage"
}
// Server -> Client (response)
{
"type": "send.result",
"id": "msg-1",
"ok": true,
"status": "accepted",
"messageId": "abc123",
"chatId": "chat-456",
"requestedService": "iMessage",
"selectedService": "auto",
"fallbackAllowed": true,
"deliveryStage": "claw_accepted",
"retryable": null,
"setupProof": {
"required": true,
"status": "waiting_for_controlled_reply",
"message": "Reply from the controlled phone and confirm it reaches your agent, webhook, or backend. Accepted, delivered, and read are not setup proof."
}
}Treat the response as the start of the send trail, not the finish line. status and deliveryStage tell you what Claw Messenger knows now. retryable tells your agent whether to try again later, fix the request first, or wait for the reply. setupProof is returned on successful-looking send and status states so your agent still waits for the controlled reply before calling setup complete.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | "send" |
id | string | Yes | Correlation ID — returned in send.result |
to | string | string[] | Yes* | E.164 phone number(s). Use chatId instead for existing groups. |
chatId | string | Yes* | Send to an existing group. Use instead of to. |
parts | array | Yes | Message parts. Each: { "type": "text", "value": "..." } |
service | string | No | "iMessage" (default), "SMS", or "RCS" |
* Provide either to or chatId, not both.
Inbound messages arrive as message events:
// Server -> Client
{
"type": "message",
"messageId": "abc123",
"chatId": "chat-456",
"from": "+1234567890",
"text": "Hey, got your message!",
"attachments": [],
"service": "iMessage",
"isGroup": false,
"participants": []
}attachments is an array of { url, mimeType } objects for media messages (images, files).
// Start typing
{ "type": "typing.start", "to": "+1234567890" }
// Stop typing
{ "type": "typing.stop", "to": "+1234567890" }{ "type": "read", "to": "+1234567890" }// Server -> Client
{
"type": "typing",
"from": "+1234567890",
"started": true
}{
"type": "reaction",
"messageId": "abc123",
"reactionType": "love",
"remove": false
}Reaction types: love, like, dislike, laugh, emphasize, question. Set remove: true to remove a reaction.
// Server -> Client
{
"type": "reaction",
"messageId": "abc123",
"from": "+1234567890",
"reactionType": "love",
"added": true
}To create a new group, send to multiple phone numbers:
{
"type": "send",
"id": "grp-1",
"to": ["+1234567890", "+0987654321"],
"parts": [{ "type": "text", "value": "Hello group!" }],
"service": "iMessage"
}To send to an existing group, use chatId from a previous send.result or inbound message:
{
"type": "send",
"id": "grp-2",
"chatId": "chat-456",
"parts": [{ "type": "text", "value": "Follow-up" }]
}Inbound group messages have isGroup: true and include a participants array.
After reconnecting, replay missed messages with a sync request:
// Client -> Server
{ "type": "sync", "since": "2026-04-10T14:30:00Z" }
// Server replays individual "message" events with replay: true
// Then sends:
{ "type": "sync.done", "count": 5 }Delivery status separates a request acknowledgment from delivery evidence. A send can be accepted before delivery is confirmed, so do not treat ok: true as the whole result.
// WebSocket send result
{
"type": "send.result",
"id": "first-test-1",
"ok": true,
"status": "accepted",
"deliveryStage": "not_confirmed",
"setupProof": {
"required": true,
"status": "waiting_for_controlled_reply",
"message": "Reply from the controlled phone and confirm it reaches your agent, webhook, or backend. Accepted, delivered, and read are not setup proof."
}
}
// Later status event
{
"type": "status",
"messageId": "abc123",
"status": "delivery_confirmed",
"setupProof": {
"required": true,
"status": "waiting_for_controlled_reply",
"message": "Reply from the controlled phone and confirm it reaches your agent, webhook, or backend. Accepted, delivered, and read are not setup proof."
}
}Public send states are accepted, failed, delivery_confirmed, and not_confirmed. The retryable flag is separate: true means a later retry may make sense, false means fix the request or setup first, and null means Claw Messenger has not marked the result either way.
accepted means Claw Messenger accepted the request and started the send process. Delivery is not confirmed yet.failed means the message was not sent. Fix the request, recipient setup, or selected channel before trying again.delivery_confirmed means delivery was confirmed by the available status evidence.not_confirmed means the send started, but final delivery is not confirmed from the available evidence.For setup, the strongest proof is still a reply. Send one plain message to a phone you control, reply from that phone, and confirm the reply appears in your agent, webhook, or backend. See the quickstart for the controlled test path.
These are the response shapes your agent should handle before it waits for the reply. Keep message text and full phone numbers out of logs when you store these results.
// Accepted: the request started, but delivery is not proven yet.
{
"type": "send.result",
"id": "first-test-1",
"ok": true,
"status": "accepted",
"messageId": "abc123",
"chatId": "chat-456",
"deliveryStage": "claw_accepted",
"retryable": null
}
// Not confirmed: the send did not produce final delivery evidence.
{
"type": "send.result",
"id": "first-test-2",
"ok": true,
"status": "not_confirmed",
"messageId": "def456",
"deliveryStage": "not_confirmed",
"retryable": true
}
// Failed, not retryable: fix the request or setup before trying again.
{
"type": "send.result",
"id": "first-test-3",
"ok": false,
"status": "failed",
"errorCode": "invalid_recipient",
"deliveryStage": "validation",
"retryable": false
}
// Failed, retryable: try again later if the setup still looks correct.
{
"type": "send.result",
"id": "first-test-4",
"ok": false,
"status": "failed",
"errorCode": "temporary_send_failure",
"deliveryStage": "submission",
"retryable": true
}
// Fallback: a requested channel was safer to route automatically.
{
"type": "send.result",
"id": "first-test-5",
"ok": true,
"status": "accepted",
"requestedService": "SMS",
"selectedService": "auto",
"fallbackAllowed": true,
"fallbackReason": "auto_routing_for_delivery_safety",
"deliveryStage": "claw_accepted",
"retryable": null,
"setupProof": {
"required": true,
"status": "waiting_for_controlled_reply",
"message": "Reply from the controlled phone and confirm it reaches your agent, webhook, or backend. Accepted, delivered, and read are not setup proof."
}
}After any accepted, delivered, read, or not-confirmed result, keep listening for the inbound message event from your controlled test phone. The first thread is not proven until that reply reaches the agent, webhook, or backend.
Most REST endpoints require a Clerk JWT in the Authorization: Bearer header (from your dashboard session). Phone number endpoints also accept your API key directly — pass Authorization: Bearer cm_live_... to manage numbers programmatically from your agent or scripts.
| Method | Path | Description |
|---|---|---|
POST | /api/keys | Create a new API key (raw key returned once) |
GET | /api/keys | List API keys (prefix + metadata only) |
DELETE | /api/keys/:id | Revoke an API key |
| Method | Path | Description |
|---|---|---|
POST | /api/routes | Register a phone number (max 20) |
GET | /api/routes | List registered phone numbers |
PUT | /api/routes/primary | Set primary phone number |
DELETE | /api/routes/:phone | Release a phone number |
| Method | Path | Description |
|---|---|---|
GET | /api/billing/usage | Current message count, limit, and plan |
POST | /api/billing/subscribe | Create Stripe checkout session |
POST | /api/billing/upgrade | Change plan (prorated) |
GET | /api/billing/portal | Stripe billing portal URL |
| Method | Path | Description |
|---|---|---|
GET | / | Service info + WebSocket URL |
GET | /health | Health check with git commit |
GET | /healthz | Health check with connection count |
| Plan | Price | Messages/mo | Trial |
|---|---|---|---|
| Base | $5 | 250 | 7 days |
| Growth | $15 | 2,000 | 7 days |
| Plus | $25 | 6,000 | 7 days |
| Pro | $50 | 15,000 | 7 days |
Message limits are hard caps — sends fail with "Monthly message limit reached" when the limit is hit. Limits reset on each billing cycle.
Each account supports up to 20 concurrent WebSocket connections and 20 registered phone numbers. For higher-scale deployments, contact us.
WebSocket errors arrive as:
{
"type": "error",
"code": "unknown_type",
"message": "Unknown message type: invalid"
}| Code | Meaning |
|---|---|
unknown_type | Invalid message type sent |
invalid_sync | Missing or invalid "since" field in sync request |
sync_error | Database error during message replay |
send.result errors include the reason in the error field:
{
"type": "send.result",
"id": "msg-1",
"ok": false,
"error": "Monthly message limit reached"
}The relay server is a WebSocket server. If you hit https://claw-messenger.onrender.com with a browser or curl, you'll get a JSON response confirming the server is up. The actual messaging endpoint is wss://claw-messenger.onrender.com/ws — it only accepts WebSocket connections.
/ws?key=YOUR_KEYsend.result for ok: false and the error fieldGET /api/billing/usage)+1234567890)POST /api/routes or the dashboardtype: "message" eventsFor API and server builds, delivered or read is not setup proof. Reply in the exact controlled thread you started, then confirm that inboundtype: "message" reaches your agent, webhook, or backend. If it does not, return to the quickstart and keep the test to one phone number, one outbound message, and one reply.
openclaw status to check if the claw-messenger channel shows as connectedconfig.yaml has the correct apiKey and serverUrlsend returns a send.result with ok: true/false and a status field. ok: true is not final delivery proof on its own.type: "status" events with status values such as failed, delivery_confirmed, or not_confirmedFor the first controlled test, usually leave service out and let Claw Messenger choose the available channel for that recipient. Set service only when you specifically need to test one channel.
The plugin (v0.1.7+) tracks connection events and errors automatically. Use the claw_messenger_diagnose tool to generate a report, or submit it to our server for analysis:
// OpenClaw: ask your agent to run the diagnose tool
// Or use the API directly:
POST /api/diagnostics
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"plugin_version": "0.1.7",
"node_version": "v22.0.0",
"errors": [{"ts": "...", "type": "error", "detail": "..."}],
"connection_log": [{"ts": "...", "type": "connect"}, ...],
"metadata": {"connected": true}
}Reports are rate-limited to 1 per hour. The Claw Messenger team reviews reports and may reach out with fixes specific to your setup.