Client
@openbot-build/client is the TypeScript client for OpenBot. It is built from the same API contract as the server.
Every request, answer, and error has a type. Your editor shows mistakes before you run the code.
The client uses Effect, a TypeScript library for typed programs. It runs in Node.js, Deno, Bun, workers, and browsers.
Install
The OpenBot packages are not on npm yet. Use them from the OpenBot workspace:
{ "dependencies": { "@openbot-build/client": "workspace:*", "effect": "4.0.5" }}Make a client
layer makes a Layer that gives you the Client service. A Layer is an Effect recipe that builds a service.
import { Client, layer } from "@openbot-build/client"import { Effect } from "effect"
const program = Effect.gen(function* () { const client = yield* Client const view = yield* client.api.sessions.createSession({ payload: { title: "Release notes" }, }) return view.session.id})
const id = await Effect.runPromise( program.pipe( Effect.provide( layer({ baseUrl: "https://api.example.com", token: process.env.OPENBOT_TOKEN, }), ), ),)| Option | Required | What it does |
|---|---|---|
baseUrl |
Yes | The origin of OpenBot, such as https://api.example.com. |
token |
No | A user token, as a string or an Effect Redacted value. It is sent as Authorization: Bearer <token>. |
organization |
No | An organization ID. With the session cookie, it is sent as the OpenBot-Organization header. A token ignores it. |
onUnauthorized |
No | A function that runs when a protected request returns 401. |
To make a user token, see Authentication.
The client never saves the token and never puts it in a URL. onUnauthorized does not sign in or try the request again. Use it to tell the user, for example after a token expires or someone revokes it.
Call an operation
Every operation is at client.api.<group>.<operation>. The names are the same as in the API reference.
Each call takes one object with these optional fields:
| Field | What it holds |
|---|---|
params |
Values in the path, such as sessionId. |
query |
Values in the query string. |
payload |
The JSON body. |
const sessions = yield * client.api.sessions.listSessions({ query: {} })
const view = yield * client.api.sessions.getSession({ params: { sessionId: id }, })
const archived = yield * client.api.sessions.listSessions({ query: { archived: "true" }, })Some groups and their pages:
| Group | Use it to | Reference |
|---|---|---|
sessions |
Make Sessions, start Turns, and read results. | Sessions |
agents |
Make and manage Agents. | Agents |
requests |
Answer the requests of your Agents. | Requests |
apps |
Find an App at an address. | App discovery |
connections |
Add and manage App connections. | Connections |
appIndex |
Search the public App Index. | App Index |
userTokens |
List, make, revoke and check user tokens. | User tokens |
organizations |
List and create your organizations. | Organizations |
organization |
Manage the organization, its members, roles and settings. | Organization |
modelProviders |
Manage model providers and list models. | Model providers |
sso |
Verify email domains and manage SSO connections. | SSO |
platform |
Manage the platform. Only for platform admins. | Platform |
health |
Read the service document and health. | Health |
Every operation needs a permission. A call without it fails with PermissionDenied. See Permissions.
Handle errors
Each failed call gives a typed error. The type ClientError lists all of them:
- API errors from the contract. Each one has a
_tag, such asSessionNotFoundorSessionConflict. - Schema errors (
SchemaError) when an answer does not match the contract. - HTTP client errors when the network fails.
Use Effect.catchTag to handle one kind of error:
const view = yield * client.api.sessions .getSession({ params: { sessionId: id } }) .pipe(Effect.catchTag("SessionNotFound", () => Effect.succeed(undefined)))Common sign-in errors:
| Tag | Status | Meaning |
|---|---|---|
AuthenticationRequired |
401 |
No key or cookie, or the key is revoked or expired. |
OrganizationRequired |
400 |
A cookie request without organization, and the person belongs to more than one organization. |
MembershipRequired |
403 |
The account is not an active member of the organization. |
ForbiddenOrigin |
403 |
A cookie request that changes data has the wrong Origin. |
AuthenticationUnavailable |
503 |
OpenBot cannot check credentials right now. |
All error schemas are exported from @openbot-build/api.
Use the client in a browser
A web app on the same origin as OpenBot can use the sign-in cookie instead of a token. Leave out token:
import { layer } from "@openbot-build/client"
const ClientLive = layer({ baseUrl: window.location.origin, // Needed when the person belongs to more than one organization. organization: "org_8f3a", onUnauthorized: async () => { // OpenBot picks the provider and answers with its address. const response = await fetch("/auth/sign-in", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ returnTo: "/" }), }) const { url } = await response.json() window.location.assign(url) },})The client sends requests with credentials: "same-origin", so the browser adds the cookie.
The browser also adds the Origin header. For requests that change data, OpenBot checks that it is exactly publicOrigin in the platform settings.
Read the organizations of the person with GET /auth/session. See Authentication.
To serve your web app on the same origin, see Deploy.
Custom setup
make({ baseUrl }) builds the Client service without the default parts. You then give it:
- an
HttpClientservice, - client middleware for
AgentAuth, used by token routes, - client middleware for
AccountAuth, used by routes that do not act in one organization, such as/v1/organizationsand/v1/platform, - client middleware for
SessionAuth, used by cookie-only routes.
Use this when you need your own HTTP client, logging, or headers. See packages/client/src/Client.ts for how layer does it.
Show App views
Some App tools come with a small user interface, called an MCP Apps view. MCP is the Model Context Protocol, a standard way for Agents to talk to tools. OpenBot keeps the view on the server. Your client only shows it.
First open the view for an App operation. Then make a view client with AppViews.make:
import { AppViews, Client } from "@openbot-build/client"import { Effect } from "effect"
const program = Effect.gen(function* () { const client = yield* Client const opened = yield* client.api.appViews.open({ params: { operationId }, payload: { sessionId, itemId }, }) return yield* AppViews.make(opened, accountId, () => refresh())})| Argument | What it is |
|---|---|
opened |
The view that appViews.open returned. |
owner |
A stable ID of the signed-in account. It keeps views of different accounts apart. |
onChange |
Optional. Runs when an action in the view changes status. Use it to refresh requests and the Session. |
The result works with mount from @openbot-build/protocol/McpApps, which shows the view in an iframe. See Protocol.
The view client holds no credentials. Every request goes through OpenBot, so the normal capability checks apply:
- It checks that each answer is a valid MCP result.
- It sends each action once, with a new request ID.
- It then checks the saved action every 250 milliseconds until it has a final result. It never sends the action again.
When you close the view, the client stops checking. An action that OpenBot already accepted still runs to the end.