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.
const ws = new WebSocket("wss://claw-messenger.onrender.com/ws?key=YOUR_API_KEY");
ws.onopen = () => {
console.log("Connected");
// Send a message
ws.send(JSON.stringify({
type: "send",
id: "msg-1",
to: "+1234567890",
parts: [{ type: "text", value: "Hello from my agent!" }],
service: "iMessage"
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Received:", data);
};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,
"messageId": "abc123",
"chatId": "chat-456"
}| 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 }// Server -> Client
{
"type": "status",
"messageId": "abc123",
"status": "delivered"
}Status values: delivered, read, failed.
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" eventsopenclaw 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 an error field on failure — always check thistype: "status" events with status: "failed"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.