Skip to content

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.


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.
Terminal window
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.

Terminal window
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.

  • 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/settings instead.
  • With --force, setup writes the new values. It keeps the current sign-in secret, so nobody is signed out because of it.

GET /v1/platform/settings reads the settings. PATCH /v1/platform/settings changes them.

Terminal window
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.

A secret goes in, but it never comes out. The answer shows { set, hint } instead:

  • set is true when a secret is stored.
  • hint is … and the last 4 characters, so you can tell secrets apart. It is null for a secret shorter than 12 characters.

The body of PATCH needs expectedVersion, the version that you read. Add only the fields that you want to change.

Terminal window
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/settings

If 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.


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.


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.


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.

Terminal window
curl -X PATCH -H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"suspended"}' \
https://api.example.com/v1/platform/organizations/org_8f3a

Nobody 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.


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:

Terminal window
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_providers
availability 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.


/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 (for GET) or write (for all other methods) on Platform.

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:

Terminal window
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/tokens

The 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.


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.