Comprehensive reference for the OpenClaw Gateway WebSocket protocol, as used by Clawmander.
- Transport: WebSocket (ws:// or wss://)
- Default Port: 18789
- Protocol Version: 3
- Message Format: JSON (newline-delimited frames)
- Direction: Bidirectional — clients send RPC requests, gateway sends responses and events
The connection handshake is a 3-step process:
On WebSocket connect, the gateway may immediately send a challenge:
{
"type": "event",
"event": "connect.challenge",
"payload": {
"nonce": "abc123",
"ts": 1234567890
}
}On localhost (127.0.0.1), the challenge is optional. If no challenge is received within ~2 seconds, the client should proceed directly to Step 2.
{
"type": "req",
"id": "1",
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "cli",
"version": "1.0.0",
"platform": "linux",
"mode": "cli"
},
"role": "operator",
"scopes": ["operator.read", "operator.write", "operator.admin"],
"auth": {
"token": "<OPENCLAW_TOKEN>"
}
}
}Critical requirements:
client.idmust be"cli"(not a custom value)client.modemust be"cli"or"operator"auth.tokenis required for non-localhost connections; on localhost, device pairing is auto-approved
{
"type": "res",
"id": "1",
"ok": true,
"payload": {
"type": "hello-ok",
"protocol": 3,
"server": {
"version": "1.x.x",
"connId": "conn-uuid"
},
"features": {
"methods": ["chat.send", "chat.history", "sessions.list", "..."],
"events": ["chat", "presence.update", "start", "end", "..."]
},
"snapshot": {
"presence": [],
"sessionDefaults": {},
"uptimeMs": 12345
},
"auth": {
"role": "operator",
"scopes": ["operator.read", "operator.write", "operator.admin"]
},
"policy": {
"maxPayload": 4194304,
"tickIntervalMs": 10000
}
}
}All messages are JSON objects with a type field.
{
"type": "req",
"id": "<unique-string>",
"method": "<rpc-method>",
"params": { ... }
}{
"type": "res",
"id": "<matching-request-id>",
"ok": true,
"payload": { ... }
}On error:
{
"type": "res",
"id": "<matching-request-id>",
"ok": false,
"error": {
"code": "ERR_CODE",
"message": "Human-readable description",
"retryable": false,
"retryAfterMs": 0
}
}{
"type": "event",
"event": "<event-name>",
"payload": { ... },
"seq": 42
}The gateway uses a token configured via gateway.auth.token or OPENCLAW_GATEWAY_TOKEN environment variable. Pass it in the connect params:
"auth": { "token": "<token>" }| Scope | Permissions |
|---|---|
operator.read |
Read-only: status, presence, session list, history |
operator.write |
Read + write: send messages, patch sessions, reset sessions |
operator.admin |
Read + write + admin: resolve approvals, delete sessions |
The OpenClawCollector (passive monitor) uses operator.read only.
The ChatGatewayClient (interactive) uses operator.read, operator.write, operator.admin.
- Localhost (127.0.0.1): Challenge is optional; device pairing auto-approved; token required if set
- LAN/Remote: Token required; challenge always sent
Handshake. See Section 2.
Send a message to an agent session.
Params:
{
"sessionKey": "agent:my-agent:main",
"message": "Hello, what are you working on?",
"idempotencyKey": "<uuid>",
"attachments": [],
"thinking": "auto",
"timeoutMs": 120000
}Response: {} (empty on success; streaming response arrives as chat events)
Retrieve the transcript for a session.
Params:
{
"sessionKey": "agent:my-agent:main",
"limit": 50
}Response: Array of message objects.
Abort an active run.
Params:
{
"sessionKey": "agent:my-agent:main",
"runId": "<optional-run-id>"
}Inject a message into a session without triggering a response.
Params:
{
"sessionKey": "agent:my-agent:main",
"message": "System injection",
"label": "system"
}List available sessions.
Params:
{
"limit": 50,
"agentId": "my-agent",
"search": "keyword",
"includeGlobal": false,
"includeDerivedTitles": true,
"includeLastMessage": false
}Response: Array of session objects:
[
{
"key": "agent:my-agent:main",
"agentId": "my-agent",
"displayName": "My Agent",
"model": "anthropic/claude-opus-4",
"modelProvider": "anthropic"
}
]Find a session by key, ID, or label.
Params:
{ "key": "agent:my-agent:main" }Update session settings.
Params:
{
"key": "agent:my-agent:main",
"model": "anthropic/claude-sonnet-4-5",
"thinkingLevel": "auto",
"verboseLevel": "off",
"elevatedLevel": "ask",
"responseUsage": "tokens",
"label": "My Custom Label",
"sendPolicy": "allow"
}Reset a session (clear context / start fresh).
Params:
{
"key": "agent:my-agent:main",
"reason": "new"
}reason values: "new" (new conversation), "reset" (full reset)
Delete one or more sessions.
Params:
{ "keys": ["agent:my-agent:main"] }List available models.
Params: {} (empty)
Response: Array of model objects:
[
{
"id": "anthropic/claude-opus-4",
"name": "Claude Opus 4",
"provider": "anthropic"
}
]List registered agents.
Params: {}
Request approval for a shell command (agent → gateway).
Params:
{
"id": "<approval-uuid>",
"command": "rm -rf /tmp/build",
"sessionKey": "agent:my-agent:main",
"agentId": "my-agent"
}Resolve a pending approval request (operator → gateway).
Params:
{
"id": "<approval-uuid>",
"decision": "approve"
}decision values: "approve", "deny"
Get gateway status snapshot.
Params: {}
Response: Status object including sessions count, heartbeat info, channel summary.
Get health snapshot.
Params: {}
Emitted for each message delta, final, error, or abort.
{
"type": "event",
"event": "chat",
"payload": {
"state": "delta",
"runId": "<run-uuid>",
"sessionKey": "agent:my-agent:main",
"seq": 5,
"message": {
"content": [
{ "type": "text", "text": "Hello! I'm currently " }
]
}
}
}States:
delta— partial text chunk;message.content[]contains text blocksfinal— run complete; full response inmessage;usagefield availableerror— run failed;errorMessagecontains descriptionaborted— run was aborted by operator
{
"type": "event",
"event": "start",
"payload": {
"runId": "<uuid>",
"sessionKey": "agent:my-agent:main",
"agentId": "my-agent"
}
}{
"type": "event",
"event": "presence",
"payload": {
"agentId": "my-agent",
"name": "My Agent",
"status": "running",
"lastInputSeconds": 5
}
}Status values: running → active, connected → active, idle → idle, disconnected → offline, error → error
{
"type": "event",
"event": "connect.challenge",
"payload": { "nonce": "abc123", "ts": 1234567890 }
}Sent on a configurable interval (default 10s). Contains agent health info.
Format: agent:<agentId>:<kind> or agent:<agentId>:subagent:<subagentId>
Examples:
agent:general-agent:main— main session for general-agentagent:research:main— main session for research agentagent:general-agent:subagent:researcher— subagent session
direct— Direct 1:1 operator chat session (shown in chat UI)group— Multi-agent group channel (excluded from chat sidebar)cron— Scheduled/automated session (excluded from chat sidebar)
Filter to direct sessions only for the chat interface.
Track working vs idle agents via the presence event:
lastInputSeconds— seconds since last agent input; low value = actively workingstatus/state— raw status string- Mapped to:
active(running/connected),idle,offline(disconnected),error
When an agent spawns a subagent, the chat.subagent event is emitted:
{
"sessionKey": "agent:main:main",
"childSessionKey": "agent:main:subagent:researcher",
"state": "working",
"label": "research"
}state values: working, done, error
{
"code": "ERR_CODE",
"message": "Human-readable",
"retryable": false,
"retryAfterMs": 0
}| Code | Meaning |
|---|---|
ERR_AUTH |
Authentication failed (bad token) |
ERR_SCOPE |
Insufficient scopes for operation |
ERR_NOT_FOUND |
Session or resource not found |
ERR_CONFLICT |
Idempotency key collision |
ERR_RATE_LIMIT |
Rate limited; use retryAfterMs |
ERR_TIMEOUT |
RPC timed out |
Use exponential backoff:
delay = min(delay * 2, 30000)
starting delay = 1000ms
max delay = 30000ms (30s)
Do NOT retry on auth errors (ERR_AUTH) — fix the token first.
On reconnect, re-send the full handshake (challenge → connect → hello-ok).
- TLS/WSS: Use
wss://in production;ws://acceptable for localhost-only - Device Identity: Optional device pairing for enhanced trust on localhost
- Tailscale: Recommended for LAN access without exposing port to internet
- Token: Store in environment variable (
OPENCLAW_TOKEN), never in code - Scopes: Use minimum required scopes per client (read-only for monitoring)