API Documentation

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.

Implementation guides

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.

Quickstart: prove one text thread first

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.

First-message path
  1. Start a trial from the dashboard.
  2. Copy your API key and keep it out of public logs or screenshots.
  3. Register one phone number you control and can answer now.
  4. Send one plain message from your agent.
  5. Reply from the phone and confirm the reply reaches your agent, webhook, or backend.
Start with one test thread

1. Start a trial

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.

2. Copy your API key

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");

3. Choose one test recipient

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"}'

4. Send one plain message

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."
      }
    ]
  }));
};
Send status is not the finish line.

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.

5. Reply from your phone

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.

6. Pick the next step

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.

Using n8n, LangChain, or CrewAI?

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.

n8n

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.

LangChain

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.

CrewAI

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.

How Phone Numbers Work

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.

Register a number programmatically

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"}'

# Response:
# {"ok": true, "phone_number": "+1234567890", "already_claimed": false}

WebSocket Connection

Endpoint

wss://claw-messenger.onrender.com/ws?key=YOUR_API_KEY

The API key is passed as a key query parameter. Generate keys in the dashboard or via the REST API.

Connection lifecycle

  1. Connect to the WebSocket endpoint with your API key
  2. Server validates the key and accepts the connection
  3. Exchange ping/pong every 30 seconds to stay alive
  4. Send and receive JSON messages
  5. On disconnect, reconnect with exponential backoff (max 30s)

Limits

Ping / Pong

Send {"type": "ping"} to keep the connection alive. The server responds with {"type": "pong"}. The server also sends pings — respond with pong to avoid disconnection.

Sending Messages

// 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.

FieldTypeRequiredDescription
typestringYes"send"
idstringYesCorrelation ID — returned in send.result
tostring | string[]Yes*E.164 phone number(s). Use chatId instead for existing groups.
chatIdstringYes*Send to an existing group. Use instead of to.
partsarrayYesMessage parts. Each: { "type": "text", "value": "..." }
servicestringNo"iMessage" (default), "SMS", or "RCS"

* Provide either to or chatId, not both.

Receiving Messages

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).

Typing & Read Receipts

Send typing indicator

// Start typing
{ "type": "typing.start", "to": "+1234567890" }

// Stop typing
{ "type": "typing.stop", "to": "+1234567890" }

Mark as read

{ "type": "read", "to": "+1234567890" }

Receive typing indicator

// Server -> Client
{
  "type": "typing",
  "from": "+1234567890",
  "started": true
}

Reactions

Send a reaction

{
  "type": "reaction",
  "messageId": "abc123",
  "reactionType": "love",
  "remove": false
}

Reaction types: love, like, dislike, laugh, emphasize, question. Set remove: true to remove a reaction.

Receive a reaction

// Server -> Client
{
  "type": "reaction",
  "messageId": "abc123",
  "from": "+1234567890",
  "reactionType": "love",
  "added": true
}

Group Messages

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.

Message Sync

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

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.

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.

Send result examples

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.

REST Endpoints

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.

API Keys

MethodPathDescription
POST/api/keysCreate a new API key (raw key returned once)
GET/api/keysList API keys (prefix + metadata only)
DELETE/api/keys/:idRevoke an API key

Phone Numbers

MethodPathDescription
POST/api/routesRegister a phone number (max 20)
GET/api/routesList registered phone numbers
PUT/api/routes/primarySet primary phone number
DELETE/api/routes/:phoneRelease a phone number

Billing

MethodPathDescription
GET/api/billing/usageCurrent message count, limit, and plan
POST/api/billing/subscribeCreate Stripe checkout session
POST/api/billing/upgradeChange plan (prorated)
GET/api/billing/portalStripe billing portal URL

Health

MethodPathDescription
GET/Service info + WebSocket URL
GET/healthHealth check with git commit
GET/healthzHealth check with connection count

Plans & Limits

PlanPriceMessages/moTrial
Base$52507 days
Growth$152,0007 days
Plus$256,0007 days
Pro$5015,0007 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.

Errors

WebSocket errors arrive as:

{
  "type": "error",
  "code": "unknown_type",
  "message": "Unknown message type: invalid"
}
CodeMeaning
unknown_typeInvalid message type sent
invalid_syncMissing or invalid "since" field in sync request
sync_errorDatabase 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"
}

Troubleshooting

"Server appears to be down"

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.

Connection closes immediately

Messages not sending

Not receiving inbound messages

Delivered or read, but no reply reached your backend

For 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: "Not connected to claw-messenger"

Debugging message delivery

For 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.

Diagnostic reports

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.