SSO and domains
For organization administrators. You need the rights named for each task. Only a
platform admin can change settings under /v1/platform. Start with the
administrator manual.
SSO (single sign-on) lets people sign in with the account provider of their own company. An organization can add its own OpenID Connect provider for the email domains that it owns. OpenID Connect (OIDC) is a standard way to let another service check who a person is.
This page shows how to verify a domain, how to add an SSO connection, and how
sign-in picks a provider. All routes are under /v1/organization. Reading needs
read on Sso. Changes need write on Sso. See Permissions.
Verified domains
Section titled “Verified domains”An email domain is the part of an email address after @, such as
example.com. Before a domain does anything, the organization must prove that
it owns it. It does this with a DNS TXT record.
Add the domain
Section titled “Add the domain”curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{"domain":"example.com"}' \ https://api.example.com/v1/organization/domains{ "domain": "example.com", "recordName": "_openbot-verification.example.com", "recordValue": "openbot-verification=4f7d...", "verifiedAt": null, "createdAt": "2026-10-21T09:00:00.000Z"}Add the TXT record
Section titled “Add the TXT record”At your DNS provider, add a TXT record. Use recordName as the name and
recordValue as the value.
Verify the domain
Section titled “Verify the domain”curl -X POST -H "Authorization: Bearer $OPENBOT_TOKEN" \ https://api.example.com/v1/organization/domains/example.com/verifyOpenBot looks up the TXT record. When it is right, the answer has a verifiedAt
time. If OpenBot does not find the record, it returns 409 with
DomainNotVerified. DNS changes can take some time, so try again later.
Only one organization can verify a domain. If another organization verified it
first, OpenBot returns 409 with DomainTaken.
GET /v1/organization/domains lists the domains.
DELETE /v1/organization/domains/:domain removes one. While an SSO connection
uses the domain, removing it returns 409 with DomainInUse.
Join by domain
Section titled “Join by domain”A verified domain also decides who joins the organization. When joinByDomain
is true in the organization settings, a person with
a verified email in the domain joins when they sign in. They get the
defaultRoles of the organization. joinByDomain is true by default.
Domain path parameters are checked and changed to lower case before the handler runs.
SSO connections
Section titled “SSO connections”An SSO connection is an OpenID Connect provider of the organization. It signs in people with an email in its domains.
Register a client
Section titled “Register a client”Register OpenBot as a client at your provider first. OpenBot asks for the scopes
openid profile email. After you create the connection, add its redirectUri
to the allowed redirect URIs of the client.
Create a connection
Section titled “Create a connection”curl -H "Authorization: Bearer $OPENBOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Example SSO", "issuer": "https://login.example.com", "clientId": "openbot", "clientSecret": "your-client-secret", "domains": ["example.com"], "enforce": true }' \ https://api.example.com/v1/organization/sso_connections| Field | Rules |
|---|---|
name |
Required. 1 to 120 characters. |
issuer |
Required. The HTTPS address of the provider, with no trailing slash. |
clientId |
Required. The client ID from the provider. |
clientSecret |
Optional. Leave it out for a public client. OpenBot then uses PKCE, a check that protects the sign-in code. |
domains |
Required. 1 to 50 verified domains of the organization. Another connection must not use them. |
enforce |
Required. See Enforce SSO. |
{ "id": "sso_2b9c...", "name": "Example SSO", "issuer": "https://login.example.com", "clientId": "openbot", "clientSecret": { "set": true, "hint": "…cret" }, "domains": ["example.com"], "enforce": true, "redirectUri": "https://api.example.com/auth/callback/sso_2b9c...", "version": 0, "createdAt": "2026-10-21T09:00:00.000Z", "updatedAt": "2026-10-21T09:00:00.000Z"}The redirectUri is <publicOrigin>/auth/callback/<connectionId>. Register it
at the provider. The client secret never comes back. The answer shows
{ set, hint } instead. See Platform.
Change or remove a connection
Section titled “Change or remove a connection”| Route | What it does |
|---|---|
GET /v1/organization/sso_connections |
Lists the connections. |
GET /v1/organization/sso_connections/:connectionId |
Reads one connection. |
PATCH /v1/organization/sso_connections/:connectionId |
Changes a connection. |
DELETE /v1/organization/sso_connections/:connectionId |
Removes a connection. |
The body of PATCH needs expectedVersion, the version that you read. If
someone changed the connection after you read it, OpenBot returns 409 with
SsoConnectionConflict. For clientSecret, leave it out to keep the current
secret. Send null to make it a public client.
Every instance uses a new or changed connection at once. If an instance misses the change notice, it still picks it up within 30 seconds.
Enforce SSO
Section titled “Enforce SSO”With enforce: true, people with an email in the domains of the connection must
sign in with it. They cannot use the platform provider. With enforce: false,
they can also sign in with the platform provider.
- A connection signs in only people with an email in its own domains.
- A person who signs in through a connection joins its organization. This works
even when
joinByDomainisfalse. - OpenBot never links accounts. Each account has exactly one sign-in provider. So a person who signs in with the platform provider and then with an SSO connection gets two different accounts.
Sign in with the right provider
Section titled “Sign in with the right provider”POST /auth/sign-in starts a browser sign-in. It needs no credential. It checks
the Origin header, like every cookie write. See
Authentication.
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":"/"}'{ "url": "https://login.example.com/authorize?..." }Both fields are optional. OpenBot picks the provider like this:
- When the domain of
emailhas an SSO connection, theurlgoes to that connection. - Otherwise, the
urlgoes to the platform provider. - When there is neither, OpenBot returns
503withSignInUnavailable.
Send the browser to url. The answer also sets the login state cookie, which
Better Auth needs to finish the sign-in. After the sign-in, the browser goes to
returnTo. See Return paths.
Errors
Section titled “Errors”| Status | Error | When |
|---|---|---|
400 |
SsoConnectionInvalid |
A value is not valid, such as a domain that is not verified or already used. |
403 |
Forbidden |
The caller may not do read or write on Sso. |
404 |
DomainNotFound |
The organization has no such domain. |
404 |
SsoConnectionNotFound |
The organization has no connection with this ID. |
409 |
DomainNotVerified |
OpenBot did not find the TXT record. |
409 |
DomainTaken |
Another organization verified the domain first. |
409 |
DomainInUse |
An SSO connection still uses the domain. |
409 |
SsoConnectionConflict |
Someone changed the connection after you read it. |
503 |
SsoUnavailable |
OpenBot cannot reach its database. |
See the full shapes in the SSO reference.