Skip to content

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.

The protocol uses WebSocket with text frames containing JSON payloads. When you connect, your first frame must be a connect request.

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

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": "…"
}
}
}

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.

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.

Common scopes for operators include:

  • operator.read
  • operator.write
  • operator.admin
  • operator.approvals
  • operator.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.

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.

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.

Nodes can call skills.bins to get the current list of skill executables. This helps with automatic allow-checks.

Operators have a few useful tools:

  • tools.catalog: Fetches the runtime tool catalog for an agent. It shows if a tool comes from the core or a plugin.
  • tools.effective: Fetches the specific tools available for a current session. The Gateway determines this context server-side to keep things secure.

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.

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.

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:gen
  • pnpm protocol:gen:swift
  • pnpm 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.

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.

If you are migrating an older client, you might see these error codes:

Messagedetails.codedetails.reasonMeaning
device nonce requiredDEVICE_AUTH_NONCE_REQUIREDdevice-nonce-missingClient omitted device.nonce (or sent blank).
device nonce mismatchDEVICE_AUTH_NONCE_MISMATCHdevice-nonce-mismatchClient signed with a stale/wrong nonce.
device signature invalidDEVICE_AUTH_SIGNATURE_INVALIDdevice-signatureSignature payload does not match v2 payload.
device signature expiredDEVICE_AUTH_SIGNATURE_EXPIREDdevice-signature-staleSigned timestamp is outside allowed skew.
device identity mismatchDEVICE_AUTH_DEVICE_ID_MISMATCHdevice-id-mismatchdevice.id does not match public key fingerprint.
device public key invalidDEVICE_AUTH_PUBLIC_KEY_INVALIDdevice-public-keyPublic 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.

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.

AI Setup Assistant

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.