Connect Clients to OpenClaw: Gateway WebSocket Protocol
Ever tried to build a system where every tool, from a simple CLI to a mobile app, needs to talk to a central brain without losing track of who is who? Managing these connections can get messy fast. OpenClaw handles this through its Gateway WS protocol, which acts as the single control plane and transport layer for everything in the system.
Whether you are building a headless node or a custom web UI, you will connect over WebSocket and define your role and scope right at the start. Here is how the protocol works and how you can work with it.
Transport
Section titled “Transport”The protocol uses WebSocket with text frames containing JSON payloads. When you connect, your first frame must be a connect request.
Handshake (connect)
Section titled “Handshake (connect)”The handshake follows a specific challenge-response pattern. First, the Gateway sends a challenge to you:
{ "type": "event", "event": "connect.challenge", "payload": { "nonce": "…", "ts": 1737264000000 }}Then, you respond to the Gateway with your details, including your role and authentication:
{ "type": "req", "id": "…", "method": "connect", "params": { "minProtocol": 3, "maxProtocol": 3, "client": { "id": "cli", "version": "1.2.3", "platform": "macos", "mode": "operator" }, "role": "operator", "scopes": ["operator.read", "operator.write"], "caps": [], "commands": [], "permissions": {}, "auth": { "token": "…" }, "locale": "en-US", "userAgent": "openclaw-cli/1.2.3", "device": { "id": "device_fingerprint", "publicKey": "…", "signature": "…", "signedAt": 1737264000000, "nonce": "…" } }}If everything looks good, the Gateway sends back a confirmation:
{ "type": "res", "id": "…", "ok": true, "payload": { "type": "hello-ok", "protocol": 3, "policy": { "tickIntervalMs": 15000 } }}When a device token is issued, the hello-ok response also includes these auth details:
{ "auth": { "deviceToken": "…", "role": "operator", "scopes": ["operator.read", "operator.write"] }}Node example
Section titled “Node example”If you are connecting as a node rather than an operator, your request will look a bit different, focusing on capabilities and commands:
{ "type": "req", "id": "…", "method": "connect", "params": { "minProtocol": 3, "maxProtocol": 3, "client": { "id": "ios-node", "version": "1.2.3", "platform": "ios", "mode": "node" }, "role": "node", "scopes": [], "caps": ["camera", "canvas", "screen", "location", "voice"], "commands": ["camera.snap", "canvas.navigate", "screen.record", "location.get"], "permissions": { "camera.capture": true, "screen.record": false }, "auth": { "token": "…" }, "locale": "en-US", "userAgent": "openclaw-ios/1.2.3", "device": { "id": "device_fingerprint", "publicKey": "…", "signature": "…", "signedAt": 1737264000000, "nonce": "…" } }}Framing
Section titled “Framing”The protocol uses three main frame types to keep communication organized:
- Request:
{type:"req", id, method, params} - Response:
{type:"res", id, ok, payload|error} - Event:
{type:"event", event, payload, seq?, stateVersion?}
If you are using methods that cause side effects, you need to use idempotency keys as defined in the schema.
Roles + scopes
Section titled “Roles + scopes”There are two primary roles you can take:
operator: This is for control plane clients like the CLI, UI, or automation scripts.node: This is for capability hosts that provide access to things like the camera, screen, or system execution.
Scopes (operator)
Section titled “Scopes (operator)”Common scopes for operators include:
operator.readoperator.writeoperator.adminoperator.approvalsoperator.pairing
The method scope is just the first check. Some slash commands sent via chat.send have stricter requirements. For instance, using /config set or /config unset requires the operator.admin scope.
Caps/commands/permissions (node)
Section titled “Caps/commands/permissions (node)”When a node connects, it declares what it can do:
caps: High-level categories of what the node can do.commands: A specific list of commands the node allows.permissions: Granular toggles for specific actions.
The Gateway treats these as claims and checks them against server-side allowlists.
Presence
Section titled “Presence”The Gateway tracks who is online using system-presence. These entries are keyed by device identity. Because entries include deviceId, roles, and scopes, a UI can show one single row for a device even if it is connected as both an operator and a node.
Node helper methods
Section titled “Node helper methods”Nodes can call skills.bins to get the current list of skill executables. This helps with automatic allow-checks.
Operator helper methods
Section titled “Operator helper methods”Operators have a few useful tools:
tools.catalog: Fetches the runtime tool catalog for an agent. It shows if a tool comes from thecoreor aplugin.tools.effective: Fetches the specific tools available for a current session. The Gateway determines this context server-side to keep things secure.
Exec approvals
Section titled “Exec approvals”If an execution request needs a human to say “yes,” the Gateway broadcasts an exec.approval.requested event. An operator with the operator.approvals scope can then resolve this by calling exec.approval.resolve. For node-hosted executions, the request must include a systemRunPlan or it will be rejected.
Agent delivery fallback
Section titled “Agent delivery fallback”When you make an agent request, you can set deliver=true. If you set bestEffortDeliver=false, the system stays strict; if it can’t find a clear route, it returns an error. If you set bestEffortDeliver=true, the system can fall back to session-only execution if external routes aren’t available.
Versioning
Section titled “Versioning”The PROTOCOL_VERSION is defined in src/gateway/protocol/schema.ts. You must send your minProtocol and maxProtocol versions when connecting. If they don’t match what the server expects, the connection is rejected. You can generate schemas and models using these commands:
pnpm protocol:genpnpm protocol:gen:swiftpnpm protocol:check
Security is handled in a few layers. If OPENCLAW_GATEWAY_TOKEN is set on the server, your initial connect.params.auth.token must match it.
After pairing, the Gateway gives you a device token. You should save this and use it for future connections. You can also rotate or revoke these tokens if you have the operator.pairing scope. If auth fails, the Gateway provides specific error codes and hints, like canRetryWithDeviceToken, to help you recover.
Device identity + pairing
Section titled “Device identity + pairing”Every client must include a device identity when connecting. Nodes should use a stable device.id based on a keypair fingerprint. New devices usually need pairing approval unless auto-approval is turned on for local connections.
You must sign the connect.challenge nonce provided by the server. While legacy signatures are sometimes accepted, you should aim to use the latest signing format for the best security.
Device auth migration diagnostics
Section titled “Device auth migration diagnostics”If you are migrating an older client, you might see these error codes:
| Message | details.code | details.reason | Meaning |
|---|---|---|---|
device nonce required | DEVICE_AUTH_NONCE_REQUIRED | device-nonce-missing | Client omitted device.nonce (or sent blank). |
device nonce mismatch | DEVICE_AUTH_NONCE_MISMATCH | device-nonce-mismatch | Client signed with a stale/wrong nonce. |
device signature invalid | DEVICE_AUTH_SIGNATURE_INVALID | device-signature | Signature payload does not match v2 payload. |
device signature expired | DEVICE_AUTH_SIGNATURE_EXPIRED | device-signature-stale | Signed timestamp is outside allowed skew. |
device identity mismatch | DEVICE_AUTH_DEVICE_ID_MISMATCH | device-id-mismatch | device.id does not match public key fingerprint. |
device public key invalid | DEVICE_AUTH_PUBLIC_KEY_INVALID | device-public-key | Public key format/canonicalization failed. |
To migrate successfully, always wait for the challenge, sign the payload with the server nonce, and include that nonce in your connection parameters.
TLS + pinning
Section titled “TLS + pinning”The protocol supports TLS for all WebSocket connections. If you want extra security, you can pin the gateway certificate fingerprint in your configuration or via the CLI.
This protocol gives you access to the entire Gateway API. This includes status updates, chat, agent management, and node approvals. You can find the full list of what is possible in the TypeBox schemas located at src/gateway/protocol/schema.ts.
Next Steps
Section titled “Next Steps”- Check out the TypeBox schema definitions
- Learn more about Operator Scopes
- Explore Node Configuration
- Read about Session Management
OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.