Building a Gateway Client: Authentication, Reconnect, and More
Learn to build operator dashboards, WebChat clients, and third-party apps using the Gateway WebSocket protocol. Covers client lifecycle: authentication, capabilities, reconnect, history, subscriptions, and upgrades.
Read this 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
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.
Install the packages
npm install @openclaw/gateway-client @openclaw/gateway-protocol
Note
These packages ship with OpenClaw release trains. During the initial rollout, npm may return
E404until the first package-bearing OpenClaw release is published; install them only after the registry pages below resolve.
@openclaw/gateway-protocolprovides schemas, runtime validators, TypeScript types, client identity and capability registries, structured error readers, and protocol version constants. Its npm tarball also includes the generatedprotocol.schema.jsonmachine-readable contract.@openclaw/gateway-clientis the reference connection implementation. Import the package root for the Node client and@openclaw/gateway-client/browserfor 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
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:
- Persist an Ed25519 device identity in the client.
- Wait for
connect.challenge, use itstsas the device proof'ssignedAt, sign the challenge-bound device payload, and sendconnectwith the requested operator role, scopes, and the shared Gateway token or password for bootstrap authentication. A received WebSocket challenge without a non-negative integertsis invalid. Clients that explicitly support Gateways from beforeconnect.challengeexisted may use local time only on their no-challenge path. - If the Gateway returns structured
PAIRING_REQUIREDdetails, show the request ID and pause or retry according toerror.details.recommendedNextStep. - On the Gateway host, review the request with
openclaw devices list, then approve that exact current request withopenclaw devices approve <requestId>. - Reconnect and persist
hello-ok.auth.deviceTokenwith 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 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:
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.
Warning
tool-eventsgates 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:
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. Even so, 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:
- Re-establish
sessions.subscribeand the selected session'ssessions.messages.subscribesubscription. - Call
chat.historyfor the selectedsessionKeyand replace local persisted rows with the returnedmessagesprojection. - If
inFlightRunis present, adopt itsrunId, bufferedtext, and optionalplan. Adopt the run even whentextis empty. - Treat
sessionInfo.hasActiveRunas aggregate direct-session activity.activeRunIds, when present, is the complete exact active set; an empty array therefore proves the session is idle. WhenhasActiveRunis true andactiveRunIdsis omitted, another runtime owner is active but its exact run identities are unavailable. In incremental merge events, omission means no change,nullis the event-only tombstone that clears cached exact IDs to unavailable, and an array replaces the cache (including[]for proven idle). Correlate only a run ID the client owns locally or received from a request, history response, or event, and never select the first list entry as an owner. - Show an observer headline or run-inspector link only when the observer digest's exact
runIdis present inactiveRunIds. Aggregate activity alone does not make a retained digest current. - Reconcile subsequent
agentevents bypayload.runIdandpayload.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.
Active-run cache matrix
Classify the source before applying activeRunIds; the same omission has different meaning in a full snapshot and an incremental delta.
| Client cache | Read path | Class | Required behavior |
|---|---|---|---|
| Web session roster | sessions.list, reconnect hydration | Snapshot | Replace the row; omission clears cached exact IDs to unavailable. |
| Web selected session | chat.history.sessionInfo | Snapshot | Replace the row projection; omission clears cached exact IDs. |
| Web session events | sessions.changed, session.message, lifecycle snapshots | Delta | Omission is inert; null clears; an array replaces. |
| Android session roster | sessions.list, reconnect hydration | Snapshot | Replace the list rows; omission clears cached exact IDs. |
| Android selected session | chat.history.sessionInfo, reconnect recovery | Snapshot | Replace activeRunIds even while other partial history fields merge. |
| Android session events | sessions.changed, session.message, lifecycle snapshots | Delta | Field presence controls replacement; null clears and omission is inert. |
| Apple session roster | sessions.list, reconnect hydration | Snapshot | Replace live rows; the offline cache strips transient active-run facts. |
| Apple selected session | chat.history.sessionInfo, reconnect recovery | Snapshot | Replace both the current row and its run-ID projection; omission clears both. |
| Apple session events | sessions.changed, session.message, lifecycle snapshots | Delta | Preserve field presence through decoding; omission is inert, null clears, and an array replaces. |
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:
- Call
artifacts.downloadwith the currentsessionKey, optionalagentId, and the block'sartifactId. - Use the returned short-lived
urlbeforeexpiresAt. The URL is scoped to that exact transcript-backed artifact and does not contain a reusable Gateway or device credential. - 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.
- If the URL expires, repeat
artifacts.downloadonce. 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 coming back from chat.history may include an __openclaw metadata wrapper:
-
idserves as the identity of a transcript entry. Rely on it for anchored history lookups, but avoid treating it as a unique key for display rows. -
seqrepresents the positive sequence of transcript records. A single stored record can map to multiple display rows, so group rows sharing the sameidand sequence together. -
kindmarks synthetic rows. A compaction boundary relies onkind: "compaction"and can incorporatetokensBeforeandtokensAfterwhen an associated checkpoint recorded those metrics.A session reset boundary relies on
kind: "reset". No checkpoint token metrics accompany it.
Use the response's hasMore and nextOffset values to page backward. Numeric offsets reflect the current transcript projection, so avoid storing them as durable bookmarks across reset or compaction events. Persist __openclaw.id instead. To navigate back to a specific row, invoke chat.history with messageId and the sessionId that produced it. The Gateway can locate that anchor from reset archive history; anchored responses deliberately exclude numeric paging metadata.
Subscribe instead of polling usage
Fetch the initial catalog via sessions.list, then invoke sessions.subscribe once for each connection. Combine sessions.changed events using sessionKey. Session change payloads may include live inputTokens, outputTokens, totalTokens, totalTokensFresh, contextTokens, estimatedCostUsd, response-usage settings, and active-run state.
Certain change notifications act only as invalidation signals. When an event lacks the row fields your view depends on, reload sessions.list. Avoid polling usage.cost or sessions.usage to maintain an up-to-date live session list; save those calls for on-demand aggregate or detailed reports.
Backfill exec approvals
A client with operator.approvals should register its event listener right after hello-ok finishes, then call exec.approval.list to fill in requests that happened before the connection. Match the list against live exec.approval.requested / exec.approval.resolved events by approval ID so a transition racing the list request is neither dropped nor revived.
Track protocol versions
The current wire version is 4. General operator and WebChat clients must agree on the exact current version using minProtocol: 4 and maxProtocol: 4. Only authenticated node clients and lightweight probes get the N-1 acceptance window, which currently spans protocol 3 through 4.
Protocol changes start as additive. protocol.schema.json includes since release-vintage metadata and required scope metadata for core methods, yet a wire version bump remains an explicit breaking change for third-party clients. Lock down the package versions you test, update the client and Gateway together whenever the wire version shifts, and check the OpenClaw changelog before each upgrade.