# SDK API Reference

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `chengflow-sdk/src/sdk/index.ts`, `chengflow-sdk/src/sdk/channel-client.ts`, `chengflow-sdk/src/sdk/ws-client.ts`, `chengflow-sdk/src/sdk/management-client.ts`, `chengflow-sdk/src/sdk/session-manager.ts`

The SDK is plain TypeScript with no framework dependency. React hooks and components are built
on top of it, but the core classes work anywhere.

```ts
import {
  ChannelClient, ChannelClientError,
  ManagementClient, AuthClient, BrowserAuthSession,
  SessionManager, ExecutionMappingStore, WsClient,
} from "@chengflow/chat/sdk";
```

`@chengflow/chat/sdk` is the framework-free entry point of the `@chengflow/chat` package (ESM
only). The package root, `@chengflow/chat`, re-exports the same classes together with the React
layer. There is no separate `@chengflow/sdk` package.

## ChannelConfig

Every client takes the same config shape:

```ts
{
  apiBaseUrl: "https://api.example.com/api/v1",
  wsBaseUrl:  "wss://api.example.com/ws/executions",
  workspaceId: "…",
  channelId:  "weather-app",
  boundWorkflowId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
}
```

`workspaceId` is required — omitting it throws a `VALIDATION_ERROR` rather than falling back to
a default workspace.

## ChannelClient

The REST client. Authorization is handled for you: a token provider supplies the JWT, refresh is
automatic, and an unrecoverable auth failure notifies through the auth-expired channel rather
than throwing in a random call site.

```ts
const client = new ChannelClient(config);
```

### `execute()` — the method you will use most

```ts
const { conversation_id, workflow_id, execution_id } =
  await client.execute(channelId, workflowId, message, sessionId?, externalUserId?, attachments?);
```

It is a **two-step JWT flow**, not one endpoint:

```text
POST /workspaces/:workspace_id/conversations/resolve   → conversation_id
POST /conversations/:id/messages                       → execution_id
```

`external_user_id` defaults to `web-user`; `external_chat_id` comes from `sessionId`, defaulting
to a timestamped one. `mode` is `workflow_chat` and `extra_context` carries the `channel_id`.

**`execution_id` may be `null`** when the message did not trigger a workflow. Handle that
instead of assuming a run started.

### The rest

| Method | Does |
|---|---|
| `resolveConversation(workspaceId, req)` | Resolve or create a conversation for `(app_id, external_user_id, external_chat_id)` |
| `createMessage(conversationId, req)` | Post a message; `role`, `content`, optional `references`, `model`, `external_message_id`, `attachments` |
| `getConversationMessages(conversationId)` | History |
| `workflowSupportsAttachments(workflowId)` | True if the workflow contains an `io/file_upload` node |
| `submitApproval(...)` | Submit an approval decision and scope |
| `resumeExecution(executionId)` | Resume a paused execution |
| `bulkUpdateMemoryControls(...)` | Update memory controls in bulk |
| `getExecution(executionId)` | Execution detail |
| `getExecutionResult(executionId)` | The result payload |

`workflowSupportsAttachments` is how the UI decides whether to show the file button — ask the
workflow rather than guessing from configuration.

Failures throw `ChannelClientError` carrying a code, message, and HTTP status, so you can branch
on the code instead of parsing text.

## WsClient

```ts
const ws = new WsClient({
  url: "wss://api.example.com/ws/executions",
  tokenProvider: () => localStorage.getItem("access_token"),
});
```

| Option | Default |
|---|---|
| `reconnect` | `true` |
| `reconnectInterval` | 1000 ms |
| `reconnectMaxInterval` | 30000 ms |
| `reconnectBackoffRate` | 2 |
| `reconnectMaxAttempts` | `Infinity` |
| `reconnectJitter` | `true` |
| `heartbeatInterval` | 30000 ms |
| `heartbeatTimeout` | 10000 ms |
| `debug` | `false` |

`tokenProvider` is called **before every connection**, and the token is appended as `?token=`.
That matters for long-lived pages: a client that captured the token once would reconnect with an
expired one.

Subscriptions are per scope:

```ts
ws.send({ type: "SUBSCRIBE", scope: { type: "conversation", conversationId } });
ws.send({ type: "SUBSCRIBE", scope: { type: "execution", executionId } });
ws.send({ type: "UNSUBSCRIBE", scope: { type: "execution", executionId } });
```

Scopes are `workspace`, `conversation`, and `execution`. Subscriptions are **restored
automatically after a reconnect** — you do not re-subscribe by hand.

Heartbeats are `PING` / `PONG` with an optional `data` field and timestamp.

### Field casing — the detail that saves an afternoon

The backend wraps every message in a `WsEnvelope` with a `SCREAMING_SNAKE_CASE` `type`. Event
fields on the wire are a **mix of snake_case and camelCase** across message types. `WsClient`
normalises everything to **camelCase before dispatch**, so application code consumes camelCase
only.

Do not write code against the raw wire shape.

### Events

Conversation scope: `MESSAGE_CREATED`, `MESSAGE_COMPLETED`, `WORKFLOW_TRIGGERED`,
`CONVERSATION_CREATED`.

Execution scope: `EXECUTION_START`, `EXECUTION_PROGRESS`, `EXECUTION_COMPLETE`,
`EXECUTION_FAILED`, `EXECUTION_CANCELLED`, `EXECUTION_STATE_CHANGED`, the `NODE_STREAM_*`
family, plus approval and agent-review events.

### Protocol handshake

Every connection starts with a handshake frame, sent before any subscription:

```ts
{ type: "HANDSHAKE", protocol_version: WS_PROTOCOL_VERSION, client_version: SDK_VERSION }
```

| Export | Value | Meaning |
|---|---|---|
| `WS_PROTOCOL_VERSION` | `"1.0"` | Wire protocol the client speaks; the server compares the major version |
| `SDK_VERSION` | the package version | Informational; the server logs it |
| `PROTOCOL_MISMATCH_CODE` | `"PROTOCOL_MISMATCH"` | Error code a server sends before closing when it cannot speak that major version |

On `PROTOCOL_MISMATCH` the client stops reconnecting, reports status `protocol_mismatch`, and
`getProtocolRejectionMessage()` returns the server's explanation for your UI. A server that
predates the handshake ignores the frame, so the client stays compatible with it.

## SessionManager

Owns the browser half of the session mapping, keyed per `channelId` and persisted in
`localStorage`.

| Method | Does |
|---|---|
| `getOrCreateSessionId()` | Stable UUID v4 per channel, surviving reloads |
| `resetSession()` | New session id; clears the conversation id — the browser's `~new` |
| `setConversationId(id)` / `getConversationId()` / `clearConversationId()` | Conversation binding |
| `listSessions()` / `createSession(label?)` / `renameSession(id, label)` | Multiple sessions per channel |
| `togglePinSession(id)` / `deleteSession(id)` | Session list management |
| `getActiveSessionId()` / `setActiveSessionId(id)` | Which one is current |
| `clear()` | Wipe everything for this channel |

The session id maps to the backend's `external_chat_id`; combined with `(workspace_id, app_id)`
it determines a conversation. See [Channel Routing](/docs/chat-channel-routing).

A custom `Storage` can be injected — the SDK falls back to an in-memory store outside the
browser, so it is testable and SSR-safe.

## ManagementClient

For building a console rather than a chat window.

| Area | Methods |
|---|---|
| Channels | `listChannels`, `createChannel`, `updateChannel`, `deleteChannel` |
| Workspaces | `listWorkspaces`, `createWorkspace` |
| Workflows | `listPublishedWorkflows({ workspaceId, limit })`, `getWorkflowName` |
| Connection | `getChannelStatus`, `getChannelCapabilities`, `getChannelAuthPattern`, `connectChannel`, `completeConnect`, `disconnectChannel` |
| Keys | `listChannelKeys`, `deleteChannelKey` |

`getChannelAuthPattern` returns the fields a given platform needs, so a pairing form can be
rendered generically instead of hard-coding one form per platform. `connectChannel` /
`completeConnect` mirror the two-step connection lifecycle described in
[Messaging Channels](/docs/chat-messaging-channels).

## AuthClient and BrowserAuthSession

`AuthClient` handles login and password reset. `BrowserAuthSession` implements
`AuthTokenProvider` over `localStorage`, using the exported `ACCESS_TOKEN_KEY` and
`REFRESH_TOKEN_KEY`, and is the default provider when you do not pass one.

Implement `AuthTokenProvider` yourself if your host application already owns the tokens.

## ExecutionMappingStore

Tracks the mapping from a message to its execution and the execution's client-side status —
which is what lets a UI show a spinner on the right bubble when several turns are in flight.
`isTerminalExecutionStatus()` and the `execution-status` exports classify backend statuses into
client ones.

## React layer

```tsx
<ChatProvider config={config}>
  <ChatWindow />
</ChatProvider>
```

`useChannel(config)` is the core hook and returns:

```ts
{ messages, sendMessage, isLoading, connectionStatus, streamingContent,
  resetConversation, submitApproval, continueAgentReview,
  submitContextMemoryReview, error, supportsAttachments }
```

`ChatProvider` simply injects `useChannel`'s return value into context. Other hooks:
`use-websocket`, `use-conversation`, `use-channels`, `use-channel-connection`, `use-auth`.

## Next steps

- [SDK Quickstart](/docs/sdk-quickstart) — the shortest path to a working chat.
- [Integration Examples](/docs/sdk-integration-examples) — putting these together.
- [WebSocket API](/docs/api-websocket) — the protocol underneath `WsClient`.
