Skip to content

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 as SessionNotFound or SessionConflict.
  • 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 HttpClient service,
  • 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/organizations and /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.