--- summary: "Build a third-party operator or WebChat client for the Gateway WebSocket protocol" read_when: - Building an operator, dashboard, or WebChat client outside the OpenClaw repository - Implementing Gateway reconnect, history, approvals, or device pairing - Updating a third-party client for a new Gateway wire version title: "Building a Gateway client" --- Use the published Gateway packages to build operator dashboards, WebChat clients, and other third-party applications. This guide covers the client lifecycle around the wire contract: authentication, capabilities, reconnect recovery, history, subscriptions, and version upgrades. For frame shapes, the handshake, errors, and the complete method surface, read the [Gateway protocol specification](https://docs.openclaw.ai/gateway/protocol). ## Install the packages ```bash npm install @openclaw/gateway-client @openclaw/gateway-protocol ``` These packages ship with OpenClaw release trains. During the initial rollout, npm may return `E404` until the first package-bearing OpenClaw release is published; install them only after the registry pages below resolve. - [`@openclaw/gateway-protocol`](https://www.npmjs.com/package/@openclaw/gateway-protocol) provides schemas, runtime validators, TypeScript types, client identity and capability registries, structured error readers, and protocol version constants. Its npm tarball also includes the generated [`protocol.schema.json`](https://unpkg.com/@openclaw/gateway-protocol@beta/protocol.schema.json) machine-readable contract. - [`@openclaw/gateway-client`](https://www.npmjs.com/package/@openclaw/gateway-client) is the reference connection implementation. Import the package root for the Node client and `@openclaw/gateway-client/browser` for the browser-safe protocol, device-auth, and reconnect helpers. The Node entry owns its WebSocket transport. A browser host supplies a WebSocket adapter plus persistent storage and signing callbacks for the device identity and device token. ## Choose scopes and pair the device A full interactive chat client that also renders approval prompts should request `role: "operator"` with these scopes: | Scope | Use it for | | -------------------- | ----------------------------------------------------------------------------------------- | | `operator.read` | `chat.history`, `sessions.list`, `sessions.subscribe`, model status, and read-only events | | `operator.write` | `chat.send` and ordinary session mutations | | `operator.approvals` | Listing, displaying, and resolving exec or plugin approvals | Add `operator.questions` only if the client handles interactive questions, `operator.pairing` only if it manages paired devices or nodes, and `operator.admin` only for administrative operations such as `config.patch`. The [operator scopes reference](https://docs.openclaw.ai/gateway/operator-scopes) defines the complete method and approval-time rules. Do not create a per-client bearer token by hand-editing `openclaw.json`. Configure the Gateway's shared bootstrap authentication with `openclaw configure --section gateway` or the `openclaw onboard --gateway-auth ...` options, then let device pairing mint the client token: 1. Persist an Ed25519 device identity in the client. 2. Wait for `connect.challenge`, use its `ts` as the device proof's `signedAt`, sign the challenge-bound device payload, and send `connect` with the requested operator role, scopes, and the shared Gateway token or password for bootstrap authentication. A received WebSocket challenge without a non-negative integer `ts` is invalid. Clients that explicitly support Gateways from before `connect.challenge` existed may use local time only on their no-challenge path. 3. If the Gateway returns structured `PAIRING_REQUIRED` details, show the request ID and pause or retry according to `error.details.recommendedNextStep`. 4. On the Gateway host, review the request with `openclaw devices list`, then approve that exact current request with `openclaw devices approve `. 5. Reconnect and persist `hello-ok.auth.deviceToken` with the negotiated role and scopes. Use that device token for later connections. Scope or role upgrades create a new pending pairing request. Token rotation cannot expand the approved pairing contract. See the [Devices CLI](https://docs.openclaw.ai/cli/devices) for approval, rotation, and revocation commands. ## Advertise client capabilities `connect.params.caps` describes optional behavior the client can consume. It does not grant authorization. Import names from `GATEWAY_CLIENT_CAPS` instead of duplicating string literals: ```ts import { GATEWAY_CLIENT_CAPS } from "@openclaw/gateway-protocol/client-info"; const caps = [GATEWAY_CLIENT_CAPS.TOOL_EVENTS]; ``` The current registry contains `approvals`, `exec-approvals`, `inline-widgets`, `run-tool-bindings`, `session-scoped-events`, `plugin-approvals`, `task-suggestions`, `terminal-offset-seq`, `tool-events`, and `ui-commands`. Advertise only capabilities the client actually implements. `tool-events` gates live tool-execution streaming. The Gateway registers only connections that advertise this capability as recipients for a run's structured tool events. Without it, the connection receives no live tool events and the handshake does not report an error. Capability-gated agent tools are a separate use of the same declaration. If an agent tool requires a client capability, the Gateway omits that tool unless the originating client advertised every required capability. ## Validate attachments before sending Attachment limits are operator-tunable, so do not hardcode them. Read `hello-ok.policy.attachments` and validate locally before uploading: ```ts const attachments = hello.policy.attachments; if (attachments) { const ceiling = isImage ? attachments.maxImageBytes : attachments.maxBytes; if (file.byteLength > ceiling) rejectLocally(); } ``` Both values are decoded per-attachment ceilings. Still check the serialized request against `policy.maxPayload`: attachments travel as base64, so a file near `maxBytes` can exceed the frame limit on its own. Older gateways omit `policy.attachments`; when it is absent, send and handle the server outcome. Accepted MIME types and per-message handling are not advertised because they depend on the entrypoint and the resolved model. The gateway can return a typed rejection, while text-only model runs can omit additional images after their offload cap and still complete the request. The values are a connection-time snapshot, so re-read them on every reconnect. ## Recover state after reconnect Treat every successful reconnect as a new projection over durable history and current in-memory run state: 1. Re-establish `sessions.subscribe` and the selected session's `sessions.messages.subscribe` subscription. 2. Call `chat.history` for the selected `sessionKey` and replace local persisted rows with the returned `messages` projection. 3. If `inFlightRun` is present, adopt its `runId`, buffered `text`, and optional `plan`. Adopt the run even when `text` is empty. 4. Read `sessionInfo.hasActiveRun` and `sessionInfo.activeRunIds`. Prefer exact membership in `activeRunIds` when deciding whether a retained run still owns the streaming UI. A true `hasActiveRun` with no listed ID can represent another active runtime projection. 5. Reconcile subsequent `agent` events by `payload.runId` and `payload.seq`. Maintain the highest accepted sequence independently for each run, ignore an already-seen or lower sequence, and treat a forward gap as a reason to reload authoritative history. The outer event frame also has an optional `seq`, which orders events on the current WebSocket connection. It resets with a new connection. The `seq` inside an `agent` event payload is assigned per run and orders that run's lifecycle, assistant, plan, tool, and other stream events. ## Render generated image artifacts Assistant-generated images arrive as canonical `type: "image"` content blocks. Managed blocks include a stable `artifactId`, a Gateway-relative `url`, MIME type, dimensions, size, and accessible alt text. Keep that reference in the transcript cache; do not persist downloaded bytes or temporary download URLs. Resolve the image through the authenticated WebSocket connection: 1. Call `artifacts.download` with the current `sessionKey`, optional `agentId`, and the block's `artifactId`. 2. Use the returned short-lived `url` before `expiresAt`. The URL is scoped to that exact transcript-backed artifact and does not contain a reusable Gateway or device credential. 3. Fetch it from the Gateway origin using the same TLS pin and reverse-proxy headers as the active connection. Validate the response as an image and enforce a 12 MiB source limit plus a bounded decoded thumbnail. 4. If the URL expires, repeat `artifacts.download` once. Reconnect or route changes cancel the old load rather than retargeting it to another Gateway. Older image blocks without `artifactId` remain displayable by existing Control UI clients, but native clients should show a readable attachment fallback rather than forward a shared owner credential. ## Use history metadata and stable anchors Rows returned by `chat.history` can carry an `__openclaw` metadata envelope: - `id` is the transcript entry identity. Use it for anchored history requests, but not as a unique display-row key. - `seq` is the positive transcript-record sequence. One stored record can project into more than one display row, so keep siblings with the same `id` and sequence together. - `kind` identifies synthetic rows. A compaction boundary uses `kind: "compaction"` and may include `tokensBefore` and `tokensAfter` when a matching checkpoint recorded those metrics. Page backward with the response's `hasMore` and `nextOffset` values. Numeric offsets describe the current transcript projection, so do not persist them as long-lived bookmarks across reset or compaction. Persist `__openclaw.id` instead. To restore around a known row, call `chat.history` with `messageId` and the `sessionId` that returned it. The Gateway can resolve that anchor from reset archive history; anchored responses intentionally omit numeric paging metadata. ## Subscribe instead of polling usage Load the initial catalog with `sessions.list`, then call `sessions.subscribe` once per connection. Merge `sessions.changed` events by `sessionKey`. Session change payloads can carry live `inputTokens`, `outputTokens`, `totalTokens`, `totalTokensFresh`, `contextTokens`, `estimatedCostUsd`, response-usage settings, and active-run state. Some change notifications are only invalidation signals. If an event omits the row fields your view needs, refresh `sessions.list`. Do not poll `usage.cost` or `sessions.usage` to keep a live session list current; reserve those methods for on-demand aggregate or detailed reports. ## Backfill exec approvals A client with `operator.approvals` should install its event listener as soon as `hello-ok` completes, then call `exec.approval.list` to backfill requests that predate the connection. Reconcile the list and live `exec.approval.requested` / `exec.approval.resolved` events by approval ID so a transition racing the list request is neither lost nor resurrected. ## Track protocol versions The current wire version is `4`. General operator and WebChat clients must negotiate the exact current version with `minProtocol: 4` and `maxProtocol: 4`. Only authenticated node clients and lightweight probes have the N-1 acceptance window, currently protocol `3` through `4`. Protocol changes are additive first. `protocol.schema.json` includes `since` release-vintage metadata and required scope metadata for core methods, but a wire version bump is still an explicit breaking event for third-party clients. Pin the package versions you test, upgrade the client and Gateway together when the wire version changes, and review the [OpenClaw changelog](https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md) before each upgrade. ## Related - [Gateway protocol](https://docs.openclaw.ai/gateway/protocol) - [Embedding OpenClaw](https://docs.openclaw.ai/gateway/embedding) - [Gateway RPC reference](https://docs.openclaw.ai/reference/rpc) - [Gateway integrations for external apps](https://docs.openclaw.ai/gateway/external-apps)