Platform
For platform operators. These tasks affect the whole deployment. Have server access or platform admin rights as the task requires. Start with the administrator manual.
The platform is the whole OpenBot deployment. One platform has many
organizations. Platform admins run the platform. This page shows how to set it
up and how to manage it with /v1/platform.
Platform admins
Section titled “Platform admins”A platform admin manages the platform. A platform admin is not a member of
every organization. They cannot read the Chats, Bots or members of an
organization unless they are also a member of it. They manage the platform only
through /v1/platform.
A platform admin is an email address. The entry links to an account the first time someone signs in with that email, and the provider verified it.
| Route | What it does |
|---|---|
GET /v1/platform/admins |
Lists the platform admins. |
POST /v1/platform/admins |
Adds a platform admin. The body is { email }. |
DELETE /v1/platform/admins/:email |
Removes a platform admin. |
curl -b cookies.txt \ -H "Origin: https://api.example.com" \ -H "Content-Type: application/json" \ -d '{"email":"bob@example.com"}' \ https://api.example.com/v1/platform/admins{ "email": "bob@example.com", "userId": null, "addedAt": "2026-10-21T09:00:00.000Z"}userId is null until Bob signs in for the first time. The platform must keep
at least one admin. Removing the last one returns 409 with
LastPlatformAdmin.
A new database has no platform settings. Until you run the setup program, every
route except /health answers 503 with SetupRequired.
The setup program connects to the database with OPENBOT_DATABASE_URL and
OPENBOT_SECRET_KEYS. It writes the first platform settings and the first
platform admins. In the image, it is /app/dist/app/Setup.mjs. From the code,
run pnpm --filter @openbot-build/openbot setup. The package also has the
command openbot-setup.
docker run --rm --env-file openbot.env openbot:$OPENBOT_RELEASE \ /app/dist/app/Setup.mjs \ --public-origin=https://api.example.com \ --oidc-issuer=https://id.example.com \ --oidc-client-id=openbot \ --admin-email=alice@example.com| Flag | Required | What it does |
|---|---|---|
--public-origin |
Yes | The public HTTPS origin of OpenBot, such as https://api.example.com. No path and no trailing slash. |
--oidc-issuer |
With the ID | The HTTPS address of the platform OpenID Connect provider, with no trailing slash. |
--oidc-client-id |
With issuer | The client ID at that provider. |
--oidc-client-secret |
No | The client secret. Leave it out for a public client that uses PKCE. |
--admin-email |
At least one | A person with this verified email becomes a platform admin when they sign in. Repeat the flag for more people. |
--s3-endpoint |
With the bucket | The address of the object storage for files, such as https://s3.us-east-1.amazonaws.com. |
--s3-public-endpoint |
No | The address where clients put files with a direct upload, when it is not --s3-endpoint. |
--s3-region |
No | The region of the storage. The default is us-east-1. |
--s3-bucket |
With the endpoint | The bucket for files. |
--s3-access-key-id |
With the endpoint | The access key ID for the storage. |
--s3-secret-access-key |
With the endpoint | The secret key for the storage. |
--s3-path-style |
No | Put the bucket in the path. MinIO and RustFS need it. |
--force |
No | Write the settings even when they exist. Use it only for recovery, when no platform admin can sign in. |
Set --oidc-issuer and --oidc-client-id together, or leave both out. Leave
both out when people sign in only through the SSO connections of
organizations.
A command line can show up in process lists. So you can give the client secret
in the environment variable OPENBOT_SETUP_OIDC_CLIENT_SECRET instead of
--oidc-client-secret. This is the better way.
Set the four storage flags --s3-endpoint, --s3-bucket, --s3-access-key-id
and --s3-secret-access-key together, or leave all of them out. You can give
the secret key in OPENBOT_SETUP_S3_SECRET_ACCESS_KEY instead. Without them,
the platform has no storage until an admin sets files in the platform
settings.
Setup also makes a random sign-in secret. Better Auth uses it to sign the sign-in cookies. OpenBot stores it encrypted, and you never see it.
Run setup again
Section titled “Run setup again”- With the same origin and provider, setup changes nothing. It only adds the admin emails.
- With other values, setup fails. It tells you to use
PATCH /v1/platform/settingsinstead. - With
--force, setup writes the new values. It keeps the current sign-in secret, so nobody is signed out because of it.
Platform settings
Section titled “Platform settings”GET /v1/platform/settings reads the settings. PATCH /v1/platform/settings
changes them.
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ https://api.example.com/v1/platform/settings{ "version": 3, "publicOrigin": "https://api.example.com", "identityProvider": { "issuer": "https://id.example.com", "clientId": "openbot", "clientSecret": { "set": true, "hint": "…k9Qz" } }, "organizationCreation": "anyone", "ceiling": "{\"_tag\":\"Rules\",\"algorithm\":\"denyOverrides\",\"rules\":[]}", "appRestrictions": [], "codeMode": { "enabled": false, "timeoutMs": 30000, "memoryMb": 64, "outputBytes": 65536 }, "rateLimit": { "requestsPerMinute": null }, "tokenRate": { "organization": null, "member": null, "bot": null, "token": null }, "files": { "storage": null, "uploads": { "server": true, "direct": true }, "maxBytes": 20971520, "quotaBytes": null, "mediaTypes": [ "image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf", "text/plain" ], "unusedMs": 86400000 }, "updatedAt": "2026-10-21T09:00:00.000Z"}| Field | What it is |
|---|---|
version |
Goes up by one with each change. |
publicOrigin |
The public HTTPS origin of OpenBot. |
identityProvider |
The platform OpenID Connect provider, or null when people sign in only through SSO connections. |
organizationCreation |
Who can create an organization: anyone or platform_admins. See Organization creation. |
ceiling |
A policy with deny rules for every organization. See Policy ceiling. |
appRestrictions |
Apps that the App Index hides. See App Index. |
codeMode |
Whether Bots can use Code Mode, and its limits. See Code Mode. |
rateLimit |
How many API requests each caller may send in one minute. See Rate limit. |
tokenRate |
The highest token rates of each scope. Organizations can only set lower ones. See Token rate. |
files |
The object storage and the limits for files. |
updatedAt |
When the settings last changed. |
Secrets never come back
Section titled “Secrets never come back”A secret goes in, but it never comes out. The answer shows { set, hint }
instead:
setistruewhen a secret is stored.hintis…and the last 4 characters, so you can tell secrets apart. It isnullfor a secret shorter than 12 characters.
Change the settings
Section titled “Change the settings”The body of PATCH needs expectedVersion, the version that you read. Add
only the fields that you want to change.
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"expectedVersion":3,"organizationCreation":"platform_admins"}' \ https://api.example.com/v1/platform/settingsIf someone changed the settings after you read them, OpenBot returns 409 with
SettingsConflict and the current version. Read the settings again, then try
again.
identityProvider in PATCH works like this:
| Value | What happens |
|---|---|
null |
Removes the platform provider. |
{ issuer, clientId } |
Sets the provider, and keeps the current secret. |
{ issuer, clientId, clientSecret: "…" } |
Sets the provider and a new secret. |
{ issuer, clientId, clientSecret: null } |
Sets the provider as a public PKCE client. |
Every instance uses a change at once. See Configuration.
Policy ceiling
Section titled “Policy ceiling”ceiling is a policy that applies in every organization. It may only have deny
rules. A deny from the ceiling always wins, so no role and no token can do what
it denies. A condition can use the organizationId of the caller. See
Permissions. The ceiling does not apply to the platform
API, so it never locks out the platform admins.
This ceiling stops every organization from adding its own model providers:
{ "expectedVersion": 4, "ceiling": "{\"_tag\":\"Rules\",\"algorithm\":\"denyOverrides\",\"rules\":[{\"_tag\":\"Deny\",\"actions\":[\"write\"],\"resources\":[\"ModelProvider\"],\"reason\":\"The platform gives the model providers.\"}]}"}A ceiling with an allow rule returns 400. A ceiling in a request may have at
most 100 rules.
Organization creation
Section titled “Organization creation”organizationCreation decides who can call POST /v1/organizations. OpenBot
never makes an organization when an account signs in.
| Value | What happens |
|---|---|
anyone |
The default. Anyone who signs in can create organizations. |
platform_admins |
Only platform admins can create organizations. Other people join by invitation, by a verified domain, or by SSO. |
See Organization for all the ways an account joins.
Organizations
Section titled “Organizations”GET /v1/platform/organizations lists every organization of the platform in
pages. Use limit (1 to 100) and after with the next value of the last
page.
PATCH /v1/platform/organizations/:organizationId changes the status of an
organization: active or suspended.
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status":"suspended"}' \ https://api.example.com/v1/platform/organizations/org_8f3aNobody can use a suspended organization. Its tokens stop working at once. Set
status to active to undo it.
GET /v1/platform/organizations/:organizationId/credits reads the credits of an
organization. PATCH on the same route turns them on or off, and
POST .../credits/grants gives credits. See Credits.
Shared model providers
Section titled “Shared model providers”A platform can pay for model providers and share them with organizations. It can also share models. Organizations can also bring their own keys. See Models.
| Route | What it does |
|---|---|
GET /v1/platform/models |
Lists the shared models. |
POST /v1/platform/models |
Adds a shared model. |
GET /v1/platform/models/:modelId |
Reads one shared model. |
PATCH /v1/platform/models/:modelId |
Changes a shared model. |
DELETE /v1/platform/models/:modelId |
Removes a shared model. |
GET /v1/platform/model_providers |
Lists the shared providers. |
POST /v1/platform/model_providers |
Adds a shared provider. |
GET /v1/platform/model_providers/:providerId |
Reads one shared provider. |
PATCH /v1/platform/model_providers/:providerId |
Changes a shared provider. |
DELETE /v1/platform/model_providers/:providerId |
Removes a shared provider. |
GET /v1/platform/model_providers/:providerId/prices |
Lists the prices of a shared provider. See Prices. |
PUT /v1/platform/model_providers/:providerId/prices |
Sets a new price for one model of a shared provider. |
The bodies are the same as for the models and providers of an organization, plus
availability. First add the model, then a provider that links it:
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "id": "gpt-4.1", "name": "GPT-4.1", "reasoningEfforts": [], "serviceTiers": [], "availability": { "_tag": "All" } }' \ https://api.example.com/v1/platform/models
curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "kind": "openai", "name": "OpenAI", "apiKey": "sk-...", "models": [{ "model": "gpt-4.1", "modelOwner": "platform" }], "availability": { "_tag": "All" } }' \ https://api.example.com/v1/platform/model_providersavailability |
Who can use the model or provider |
|---|---|
{ "_tag": "All" } |
Every organization. |
{ "_tag": "None" } |
No organization. |
{ "_tag": "Selected", "organizations": ["org_…"] } |
Only the listed organizations. |
An organization sees a shared model of a shared provider only when the
availability of both lets it in. An organization can also link a shared model
that it may use to its own provider. Then it uses the model with its own key.
A shared provider links only shared models. You cannot remove a shared model
while a shared provider links it: OpenBot returns 409 with ModelInUse. When
you remove a shared model, OpenBot also removes the links that the providers of
organizations have to it.
Call the platform API
Section titled “Call the platform API”/v1/platform accepts two kinds of callers:
- The session cookie of a platform admin.
- A user token of a platform admin. The token’s own policy must allow
read(forGET) orwrite(for all other methods) onPlatform.
No role in an organization gives the actions on Platform. A token made without
policy never gets them. So a token for the platform must name them when you
create it:
curl -b cookies.txt \ -H "Origin: https://api.example.com" \ -H "Content-Type: application/json" \ -d '{"name":"Platform admin","expiresInDays":30,"policy":"{\"_tag\":\"Rules\",\"algorithm\":\"denyOverrides\",\"rules\":[{\"_tag\":\"Allow\",\"actions\":[\"read\",\"write\"],\"resources\":[\"Platform\"]}]}"}' \ https://api.example.com/v1/user/tokensThe token still works only while its account is a platform admin. Other callers
get 403 with Forbidden.
OpenBot writes each platform change to the audit log, with the organization ID
platform. The API cannot read these entries yet.
See the full shapes in the Platform reference.
Errors
Section titled “Errors”| Status | Error | When |
|---|---|---|
400 |
SettingsInvalid |
A value in the change is not valid. |
403 |
Forbidden |
The caller is not a platform admin, or the token does not allow it. |
404 |
OrganizationNotFound |
No organization has this ID. |
404 |
PlatformAdminNotFound |
No platform admin has this email. |
409 |
SettingsConflict |
Someone changed the settings after you read them. |
409 |
LastPlatformAdmin |
The change would leave the platform without an admin. |
503 |
SetupRequired |
Nobody has run the setup program yet. |
503 |
PlatformUnavailable |
OpenBot cannot reach its database. |