Authentication
For developers who connect a client to OpenBot or extend its server. The examples need the access stated on this page. Start with the developer manual.
This page shows how people sign in to OpenBot and how programs call the API. A person signs in once in a browser. Then the person creates user tokens for scripts and services.
There are two ways to call the business routes under /v1:
| Way | Who uses it | What you send |
|---|---|---|
| User token | Scripts, backend services, bots | Authorization: Bearer <token> |
| Session cookie | A browser client on the OpenBot origin | The __Host-openbot-session cookie |
Both ways need a permission for each call. See Permissions.
Sign in
Section titled “Sign in”OpenBot does not keep passwords. It uses OpenID Connect providers. OpenID Connect (OIDC) is a standard way to let another service check who a person is. There are two kinds of providers:
- The platform provider signs in people for the whole platform. A platform admin sets it. See Set up the platform provider.
- An SSO connection is the provider of one organization. It signs in people with an email in the verified domains of that organization. See SSO and domains.
To start a sign-in, your client sends this request from the OpenBot origin:
curl -X POST https://api.example.com/auth/sign-in \ -H "Origin: https://api.example.com" \ -H "Content-Type: application/json" \ -d '{"email":"ada@example.com","returnTo":"/settings/keys"}'{ "url": "https://id.example.com/authorize?..." }Both fields are optional. OpenBot uses the domain of email to pick the
provider. When the domain has an SSO connection, the url goes to it. Otherwise
it goes to the platform provider. When there is neither, OpenBot returns 503
with SignInUnavailable.
Send the browser to that url. The answer also sets the login state cookie.
After the person signs in, the provider sends the browser back to OpenBot.
OpenBot sets the session cookie and sends the browser to returnTo. See
Return paths. If the sign-in fails, the browser goes to
/auth/error?error=<code> and gets no cookie.
Better Auth routes
Section titled “Better Auth routes”OpenBot uses Better Auth for sign-in. Better Auth
serves its routes under /auth/*, as its
Effect guide shows. These
routes are not in the API reference. The
Better Auth docs describe them.
You can still use them directly. The platform provider has the provider ID
oidc and the callback /auth/callback/oidc. An SSO connection uses its own ID
as the provider ID, and its callback is /auth/callback/<connectionId>.
curl -X POST https://api.example.com/auth/sign-in/social \ -H "Origin: https://api.example.com" \ -H "Content-Type: application/json" \ -d '{ "provider": "oidc", "callbackURL": "https://api.example.com/settings/keys" }'The callbackURL must be on the OpenBot origin. If it is not, Better Auth
returns 403.
Set up the platform provider
Section titled “Set up the platform provider”The platform operator sets up the provider. See Set up sign-in.
Development account
Section titled “Development account”The development server (devenv shell -- pnpm dev) uses one local test account
and no provider. The account is developer@localhost.test, and it is a platform
admin. Only the development entry point offers this. The production program
never does. See Configuration.
This command signs in and saves the cookie in a file:
curl -c cookies.txt -X POST http://localhost/auth/development/sign-in \ -H "Origin: http://localhost" \ -H "Content-Type: application/json" \ -d '{}'Read and end the sign-in
Section titled “Read and end the sign-in”GET /auth/session returns the signed-in account and its organizations. It
needs the session cookie.
{ "userId": "u_123", "profile": { "name": "Ada", "email": "ada@example.com", "emailVerified": true }, "memberships": [ { "organizationId": "org_8f3a", "name": "Example", "status": "active", "roles": ["member"] } ], "platformAdmin": false, "expiresAt": "2026-11-19T10:00:00.000Z"}| Field | What it is |
|---|---|
memberships |
The organizations of the account, with its status and roles in each one. |
platformAdmin |
true when the account is a platform admin. Then it can use /v1/platform. See Platform. |
POST /auth/sign-out ends the sign-in. It is a Better Auth route. Send a JSON
body ({}) and the Origin header (see below). It returns 200.
Membership
Section titled “Membership”One platform has many organizations. A user and an organization are separate. One account can create organizations and be a member of many of them. When an account signs in, OpenBot checks where it belongs:
- It joins the organization that verified the domain of its verified email,
when that organization allows
joinByDomain. - It joins the organization of its SSO connection, if it signed in through one.
- OpenBot never makes an organization for it. Without one, organization routes
answer
403withMembershipRequired. The account can still create an organization or accept an invitation.
OpenBot also does these checks on each request with the session cookie. See Organization.
A suspended member gets 403 with MembershipRequired when they use the
cookie. Their tokens stop working at once and get 401 with
AuthenticationRequired.
Choose an organization
Section titled “Choose an organization”A request with the session cookie must say which organization it acts in. Send
the ID in the OpenBot-Organization header:
curl -b cookies.txt \ -H "OpenBot-Organization: org_8f3a" \ https://api.example.com/v1/chats- Without the header, OpenBot uses your only active organization.
- If you have more than one active organization and send no header, OpenBot
returns
400withOrganizationRequired.
A user token belongs to one organization. It ignores the header.
The session cookie
Section titled “The session cookie”After sign-in, the browser holds the cookie __Host-openbot-session. It is
Secure, HttpOnly and SameSite=Lax.
- A sign-in lasts 30 days.
- OpenBot renews it after one day of use.
The Origin rule
Section titled “The Origin rule”A cookie goes with every browser request, even from other sites. So OpenBot
checks where a write comes from. When you use the cookie, every POST, PUT,
PATCH and DELETE request must send an Origin header. The header must be
exactly equal to publicOrigin in the
platform settings. If it is not, OpenBot returns
403 with ForbiddenOrigin.
curl -b cookies.txt \ -H "Origin: http://localhost" \ -H "Content-Type: application/json" \ -d '{"input":"Hello"}' \ http://localhost/v1/bots/$BOT/messagesRequests with a user token do not need the Origin header.
User tokens
Section titled “User tokens”A user token lets a program act for you. A token can never do more than you can do. It can also have its own policy, so it does less. See Permissions.
You can manage tokens with the session cookie, or with a token that may do
write on Token. A token made without a policy cannot manage tokens.
A token belongs to one organization: the organization of the request that made
it. With the cookie, send the OpenBot-Organization header to choose it. See
Choose an organization.
See the full shapes in the User tokens reference.
Create a token
Section titled “Create a token”curl -b cookies.txt \ -H "Origin: https://api.example.com" \ -H "Content-Type: application/json" \ -d '{"name":"CI bot","expiresInDays":90}' \ https://api.example.com/v1/user/tokens| Field | Rules |
|---|---|
name |
Required. 1 to 80 characters after trimming spaces. |
expiresInDays |
Optional. A whole number from 1 to 365. Leave it out for a token that works until you revoke it. |
policy |
Optional. A policy that limits the token. Leave it out to allow everything that you can do, except acts on Token and Platform. |
OpenBot returns 201:
{ "token": { "id": "5f0c2a8e-7d1b-4c3e-9a55-2b6f1e8d4c10", "name": "CI bot", "prefix": "obut_Xr4pQ2aL", "policy": "{\"_tag\":\"Rules\",\"algorithm\":\"denyOverrides\",\"rules\":[{\"_tag\":\"Allow\",\"actions\":[\"*\"],\"resources\":[\"Chat\",\"File\",\"Request\",\"...\",\"Budget\"]}]}", "createdAt": "2026-10-20T09:00:00.000Z", "expiresAt": "2027-01-18T09:00:00.000Z", "lastUsedAt": null }, "secret": "obut_Xr4pQ2aL..."}Every secret starts with obut_, so a leaked token is easy to spot. The
prefix is the first 13 characters. Use it to tell your tokens apart.
List tokens
Section titled “List tokens”curl -b cookies.txt https://api.example.com/v1/user/tokensThe answer is { "items": [ ... ] }, oldest first. The list never contains
secrets. lastUsedAt changes at most once a minute. Expired tokens stay in the
list until you revoke them.
Revoke a token
Section titled “Revoke a token”Use the token’s UUID id from the list. Old v4 and new v7 IDs both work. The
API rejects an ID that is not UUID text before it reaches storage.
curl -X DELETE -b cookies.txt \ -H "Origin: https://api.example.com" \ https://api.example.com/v1/user/tokens/5f0c2a8e-7d1b-4c3e-9a55-2b6f1e8d4c10OpenBot returns 204 and deletes the token. The token stops working at once.
Check a token
Section titled “Check a token”GET /v1/tokens/verify shows who the caller is. For a token, it also shows the
token and its policy. Every caller can use it.
{ "caller": { "kind": "user_token", "userId": "u_123", "organizationId": "org_8f3a", "tokenId": "5f0c2a8e-7d1b-4c3e-9a55-2b6f1e8d4c10" }, "token": { "id": "5f0c2a8e-7d1b-4c3e-9a55-2b6f1e8d4c10", "name": "CI bot" }}With the session cookie, caller.kind is member and token is null.
Limits and errors
Section titled “Limits and errors”| Status | Error | When |
|---|---|---|
401 |
AuthenticationRequired |
No valid cookie or token. |
403 |
Forbidden |
The caller may not do read or write on Token. |
403 |
ForbiddenOrigin |
A cookie write without the exact Origin header. |
404 |
TokenNotFound |
You have no token with this ID. |
409 |
TokenLimitReached |
You already have 25 tokens. The body has "limit": 25. |
503 |
TokenStorageUnavailable |
OpenBot cannot reach its database. |
A member can hold 25 tokens at a time. Expired tokens count too, so revoke old tokens to make room.
Use a token
Section titled “Use a token”Send the token in the Authorization header on any /v1 route:
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ https://api.example.com/v1/chatsA revoked, expired or unknown token gets 401 with AuthenticationRequired. If
OpenBot cannot check the token, it returns 503 with
AuthenticationUnavailable.
Your own space
Section titled “Your own space”Inside the organization, each member has their own Chats, Apps and connections.
Your tokens and your cookie see only your own. If you ask for a Chat of another
member, OpenBot answers 404, as if it does not exist.
Return paths
Section titled “Return paths”Some flows send the browser back to your client after a round trip. You choose
the place with returnTo.
A return path must be a path on the same origin. It must start with one /. It
cannot point to /auth/*, /v1/* or /development/*, because OpenBot owns
those paths. The default is /.
OpenBot has no pages, so your client must serve these paths. Two flows add a query value:
- App authorization. When an App sign-in comes back and the person is signed
in, OpenBot finishes the flow. The browser goes to the return path with
?authorization=<flowId>. Your client can read the result withGET /v1/app-authorizations/<flowId>. If the person is not signed in, the browser goes to/?authorization=<flowId>. Your client signs the person in withPOST /auth/sign-in(see Sign in), with this address asreturnTo. Then it callsPOST /v1/app-authorizations/<flowId>/completeto finish the flow. - Developer tests. A developer test authorization returns to
/?developerTest=<testId>.
The callback submits a durable command. It keeps the original caller and token
limits. Completion takes { "commandKey" } and returns HTTP 202. Read the
flow’s executionId, then use the execution API to observe or cancel the work.
GET /v1/app-authorization-actions/:executionId gives its safe result. An
unknown token exchange waits for a decision. A browser disconnect does not
cancel it.