---
title: Gateway Client API
description: The low-level typed RPC + WebSocket SDK for a Smithers Gateway.
icon: "satellite-dish"
---

This is the transport layer beneath the UI: a typed client that speaks the
Gateway's HTTP RPC and WebSocket protocol, plus the TanStack DB collection
factory that turns those calls into live, reconnecting data. It is
framework-free on purpose.

<Note>
Most UIs should not touch this. Reach for the React bindings in
[`/reference/gateway-react`](/reference/gateway-react), which hold one client
for you and expose live hooks. Drop down to the client only for non-React hosts,
custom transports, or scripts.
</Note>

Everything on this page lives at the subpath
`smithers-orchestrator/gateway-client`, **not** on the bare
`smithers-orchestrator` facade. Import from the subpath or the build fails.

```ts
import {
  SmithersGatewayClient,
  SmithersGatewayConnection,
  GatewayRpcError,
  gatewayBackoffDelay,
  createSmithersCollections,
  createSmithersDataClient,
  smithersCollectionKeys,
  type WorkspaceMode,
  type SmithersCollections,
} from "smithers-orchestrator/gateway-client";
```

## SmithersGatewayClient

The typed client. Construct it with a base URL (and optional auth token); call a
named RPC method, or open a WebSocket and subscribe to a run's event stream.

```ts
const client = new SmithersGatewayClient({
  baseUrl: "http://127.0.0.1:7331",
  token: process.env.SMITHERS_TOKEN,
});
```

<ParamField path="options" type="SmithersGatewayClientOptions">
  <Expandable title="SmithersGatewayClientOptions">
    <ParamField path="baseUrl" type="string">
      Gateway origin. Defaults to `globalThis.location.origin` in a browser, else
      `http://127.0.0.1:7331`. A trailing slash is stripped; `ws+unix:` URLs are
      supported for local sockets.
    </ParamField>
    <ParamField path="token" type="string">
      Bearer token sent as the `authorization` header on RPC and in the WebSocket
      `connect` handshake.
    </ParamField>
    <ParamField path="headers" type="HeadersInit">
      Extra headers merged into every HTTP RPC request.
    </ParamField>
    <ParamField path="fetch" type="typeof fetch">
      `fetch` override. Defaults to `globalThis.fetch`; calls reject if neither is
      available.
    </ParamField>
    <ParamField path="WebSocket" type="typeof WebSocket">
      `WebSocket` override. Defaults to `globalThis.WebSocket`; `connect` throws if
      neither is available.
    </ParamField>
    <ParamField path="client" type="{ id?, version?, platform? }">
      Client identity sent in the `connect` handshake.
    </ParamField>
  </Expandable>
</ParamField>

### RPC methods

Each method is a thin typed wrapper over `rpc(method, params)`, which POSTs to
`/v1/rpc/<method>` and unwraps the response frame. Params and return shapes come
from the method catalog; see [`/rpc/launch-run`](/rpc/launch-run) for one method
in full.

```ts
const { runId } = await client.launchRun({ workflowId: "review", input: {} });
await client.submitApproval({ runId, nodeId: NODE_ID, decision: "approve" });
const runs = await client.listRuns();
await client.cancelRun({ runId });
```

<ResponseField name="run lifecycle" type="methods">
  `launchRun`, `resumeRun`, `pauseRun`, `cancelRun`, `hijackRun`, `rewindRun`.
</ResponseField>
<ResponseField name="human-in-the-loop" type="methods">
  `submitApproval`, `submitSignal`, `listApprovals`.
</ResponseField>
<ResponseField name="reads" type="methods">
  `getRun`, `listRuns`, `listWorkflows`, `getNodeOutput`, `getNodeDiff`,
  `whatHappened`.
</ResponseField>
<ResponseField name="crons" type="methods">
  `cronList`, `cronCreate`, `cronDelete`, `cronRun`.
</ResponseField>
<ResponseField name="escape hatches" type="methods">
  `rpc(method, params, { signal? })` for any typed method, `rpcRaw(method,
  params?, { signal? })` for an untyped one, and `extensionRpc(namespace, key,
  params?)` / `streamExtension(namespace, key, params?)` for gateway extensions.
</ResponseField>

### Subscribing to events

`connect()` opens a `SmithersGatewayConnection`. The higher-level generators
filter and yield frames for you, so most callers iterate one of those instead of
driving the connection directly.

```ts
const ac = new AbortController();
for await (const frame of client.streamRunEvents(
  { runId: RUN_ID },
  { signal: ac.signal },
)) {
  console.log(frame.event, frame.payload);
}
```

<ResponseField name="streamRunEvents(params, { signal? })" type="AsyncGenerator<GatewayEventFrame>">
  Subscribes to one run and yields its `run.*` frames (`run.event`,
  `run.gap_resync`, `run.heartbeat`, `run.error`). Pass `afterSeq` to resume.
  Closes the socket when iteration ends.
</ResponseField>
<ResponseField name="streamRunEventsResilient(params, options?)" type="AsyncGenerator<GatewayEventFrame>">
  Same frames, but reconnects with backoff + jitter on a silent drop and resumes
  from the last observed `seq`. Stops on terminal completion or abort. Options:
  `signal`, `backoff` ([`GatewayBackoffOptions`](#gatewaybackoffdelay)),
  `healthyAfterMs`, and `onReconnect(event)`.
</ResponseField>
<ResponseField name="streamDevTools(params, { signal? })" type="AsyncGenerator<GatewayEventFrame>">
  Subscribes to a run's DevTools snapshot stream.
</ResponseField>
<ResponseField name="connect({ subscribe?, signal? })" type="Promise<SmithersGatewayConnection>">
  Opens the WebSocket, performs the `connect` handshake (protocol + auth), and
  returns the live connection. Use this only when you need request/response over
  the socket or raw event frames.
</ResponseField>

### SmithersGatewayConnection

A single open WebSocket. RPC over the socket via `request` / `requestRaw`, and a
single in-order async stream of server event frames via `events(signal?)`. One
consumer per connection; the generators above own a connection each.

```ts
const conn = await client.connect({ subscribe: [RUN_ID] });
const sub = await conn.request("streamRunEvents", { runId: RUN_ID });
for await (const frame of conn.events()) {
  if (frame.event === "run.event") console.log(frame.payload);
}
conn.close();
```

<ResponseField name="request(method, params)" type="Promise<payload>">
  Typed request/response over the socket.
</ResponseField>
<ResponseField name="requestRaw(method, params?)" type="Promise<unknown>">
  Untyped request/response over the socket.
</ResponseField>
<ResponseField name="events(signal?)" type="AsyncGenerator<GatewayEventFrame>">
  In-order server event frames. Ends on close; throws on an error or invalid
  frame.
</ResponseField>
<ResponseField name="close()" type="void">
  Closes the socket, rejects pending requests, and ends `events()`.
</ResponseField>

**Source** [`SmithersGatewayClient.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/SmithersGatewayClient.ts) · [`SmithersGatewayConnection.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/SmithersGatewayConnection.ts) · **Tests** [`SmithersGatewayClient.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/SmithersGatewayClient.test.ts) · **See also** [`/rpc/launch-run`](/rpc/launch-run), [Gateway integration](/integrations/gateway)

## GatewayRpcError

The error every RPC rejects with on a failed frame or HTTP error. Inspect `code`
to branch (for example, `requiredScope` is set on an authorization failure).

```ts
try {
  await client.launchRun({ workflowId: "review", input: {} });
} catch (error) {
  if (error instanceof GatewayRpcError) {
    console.error(error.method, error.code, error.requiredScope);
  }
}
```

<ResponseField name="method" type="string">
  The RPC method that failed (or `"websocket"` for a malformed socket frame).
</ResponseField>
<ResponseField name="code" type="string">
  Machine-readable error code, e.g. `HTTP_ERROR`, `INVALID_GATEWAY_RESPONSE`, or
  the gateway's own code from the response frame.
</ResponseField>
<ResponseField name="status" type="number">
  HTTP status when the failure came over HTTP RPC.
</ResponseField>
<ResponseField name="requiredScope" type="string">
  The scope the call needed, on an authorization failure.
</ResponseField>
<ResponseField name="refresh" type="string">
  Set when the gateway asks the client to refresh credentials.
</ResponseField>
<ResponseField name="details" type="unknown">
  Extra context attached by the gateway.
</ResponseField>

**Source** [`GatewayRpcError.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/GatewayRpcError.ts) · **Tests** [`gateway-client.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/gateway-client.test.ts)

## gatewayBackoffDelay

Exponential backoff with full jitter for one 0-based attempt. The resilient
stream uses it internally; call it directly when you write your own reconnect
loop.

```ts
const delayMs = gatewayBackoffDelay(attempt, { baseMs: 250, maxMs: 10_000 });
```

<ParamField path="attempt" type="number" required>
  0-based attempt index. The base delay grows by `factor ** attempt`, capped at
  `maxMs`.
</ParamField>
<ParamField path="options" type="GatewayBackoffOptions">
  <Expandable title="GatewayBackoffOptions">
    <ParamField path="baseMs" type="number" default="250">
      Base delay before growth and jitter.
    </ParamField>
    <ParamField path="maxMs" type="number" default="10000">
      Ceiling on the pre-jitter delay.
    </ParamField>
    <ParamField path="factor" type="number" default="2">
      Per-attempt growth multiplier.
    </ParamField>
    <ParamField path="jitter" type="number" default="0.5">
      Symmetric jitter fraction applied to the delay.
    </ParamField>
    <ParamField path="random" type="() => number" default="Math.random">
      RNG override for deterministic tests.
    </ParamField>
  </Expandable>
</ParamField>

<ResponseField name="number" type="number">
  Milliseconds to wait, never negative.
</ResponseField>

**Source** [`gatewayBackoffDelay.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/gatewayBackoffDelay.ts) · **Tests** [`gatewayBackoffDelay.test.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/tests/gatewayBackoffDelay.test.ts)

## createSmithersCollections

Builds the TanStack DB collection registry. Local mode uses the Gateway REST
domain API plus `/v1/api/stream` SSE invalidation. Multiplayer mode uses
Electric shapes from `electricBaseUrl` for reads and keeps the same domain API
write path.

```ts
import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient();
const mode: WorkspaceMode = {
  kind: "local",
  apiBaseUrl: "http://127.0.0.1:7331",
};
const collections = createSmithersCollections(mode, queryClient);
```

<ParamField path="mode" type="WorkspaceMode" required>
  `{ kind: "local", apiBaseUrl, token? }` or `{ kind: "multiplayer",
  apiBaseUrl, electricBaseUrl, workspaceId, token? }`.
</ParamField>
<ParamField path="queryClient" type="QueryClient" required>
  The TanStack Query client used by QueryCollection and invalidation.
</ParamField>

<ResponseField name="SmithersCollections" type="object">
  Registry methods include `runs`, `run`, `runTree`, `runEvents`, `nodes`,
  `approvals`, `workflows`, `docs`, `prompts`, `scores`, `tickets`,
  `memoryFacts`, and `crons`, plus `connect`, `invalidate`, and `close`.
</ResponseField>

**Source** [`createSmithersCollections.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/data/createSmithersCollections.ts) · **Tests** [`smithers-collections`](https://github.com/smithersai/smithers/tree/main/packages/gateway-client/tests)

## createSmithersDataClient

Creates the domain API client that collection mutation handlers call. Reads and
writes target `/v1/api/*`; `stream.subscribe` opens `/v1/api/stream` and emits
`change`, `reset`, and `heartbeat` invalidation events.

```ts
const dataClient = createSmithersDataClient({
  mode: { kind: "local", apiBaseUrl: "http://127.0.0.1:7331" },
});

const runs = await dataClient.api.listRuns({ filter: { limit: 20 } });
const ack = await dataClient.api.cancelRun({ runId: runs[0]?.runId ?? "" });
```

<ParamField path="options.mode" type="WorkspaceMode" required>
  Workspace mode and auth token.
</ParamField>
<ParamField path="options.fetch" type="typeof fetch">
  Fetch override for tests or custom runtimes.
</ParamField>
<ParamField path="options.EventSource" type="typeof EventSource">
  SSE implementation override. When omitted, the client falls back to streaming
  `fetch`.
</ParamField>

<ResponseField name="SmithersDataClient" type="object">
  `{ mode, api, stream, close }`. The `api` object covers run lifecycle,
  approvals, signals, crons, docs, prompts, memory facts, scores, tickets, node
  output, node diffs, and schema signatures.
</ResponseField>

## Collection keys and row types

`smithersCollectionKeys` contains the TanStack Query keys used by the registry.
The row types are exported from the same package, including `GatewayRunRow`,
`GatewayRunSummaryRow`, `GatewayRunEventRow`, `GatewayRunNode`,
`GatewayApprovalRow`, `GatewayWorkflowRow`, `GatewayCronRow`,
`GatewayMemoryFactRow`, `GatewayScoreRow`, `GatewayTicketRow`, and
`GatewayPromptRow`.

Run-tree helpers remain public for custom inspectors:
`flattenGatewayRunNode`, `snapshotToGatewayRunNode`, and
`reconcileSnapshotNodes`.

---

**Source** [`index.ts`](https://github.com/smithersai/smithers/blob/main/packages/gateway-client/src/index.ts) · **Tests** [`packages/gateway-client/tests`](https://github.com/smithersai/smithers/tree/main/packages/gateway-client/tests) · **See also** [Gateway React API](/reference/gateway-react), [Gateway integration](/integrations/gateway), [`/rpc/launch-run`](/rpc/launch-run)
