Skip to content

Endpoints: SSO & SCIM

Per-organization OIDC connection management (org-admin), plus the public login flow. See Quick Start: SSO & SCIM — Single sign-on for a setup walkthrough.

Requires admin role. Connects an OIDC provider to this org. 409 if a connection already exists — use PATCH to modify one, or DELETE it first to fully replace it.

Request body

{
"display_name": "Acme Corp",
"issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"client_id": "app-client-id",
"client_secret": "app-client-secret",
"default_role": "member",
"allowed_domains": ["acme.com"],
"enabled": true
}
FieldTypeDefaultDescription
display_namestring—Shown on the login page’s SSO button
issuerstring—OIDC issuer URL
client_idstring——
client_secretstring—Encrypted at rest, never returned by any response
default_rolestringmemberRole granted to a user auto-provisioned on first login. Cannot be owner
allowed_domainsstring[] | nullnullOptional email-domain allowlist. If set, no other org’s connection may claim the same domain
enabledbooltrue—

Response 201 — secret never included, only whether one is set

{
"provider": "oidc",
"display_name": "Acme Corp",
"issuer": "https://login.microsoftonline.com/{tenant-id}/v2.0",
"client_id": "app-client-id",
"client_secret_set": true,
"default_role": "member",
"allowed_domains": ["acme.com"],
"enabled": true,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}
StatusDescription
201Connection created
403Caller does not have admin role
409A connection already exists for this org, or allowed_domains includes a domain already claimed by a different org’s connection
422default_role was owner

Requires admin role. Returns the org’s connection, or 404 if none is configured.

Requires admin role. All fields optional — client_secret only re-encrypts and replaces the stored secret when provided, so enabled/default_role/allowed_domains can be changed without re-entering it.

StatusDescription
200Connection updated
404No connection configured
409Updated allowed_domains includes a domain already claimed by a different org’s connection

Requires admin role. Disconnects the org’s SSO connection.

Public — no authentication (the caller isn’t logged in yet). Convenience for the login page’s “Continue with SSO” box: matches the email’s domain against every enabled connection’s allowed_domains.

Request body

{"email": "[email protected]"}

Response 200

{"org_id": "3f4a5b6c-...", "display_name": "Acme Corp"}
StatusDescription
200A connection’s allowed_domains matched
404No connection matches — fall back to a password login

Public. Redirects (307) the browser to the org’s IdP authorization endpoint, with a PKCE challenge/nonce/CSRF state stored in a short-lived sso_state cookie.

StatusDescription
307Redirect to the IdP
404No enabled connection for this org
502The identity provider was unreachable

Public. The IdP redirects here with ?code=&state= after the user authenticates. Validates the state/PKCE/nonce/ID-token, resolves or JIT-provisions the user and membership (linking an existing local-password account by email rather than duplicating it; an existing membership’s role is never downgraded), sets the session cookie, and redirects into the dashboard.

StatusDescription
307Login completed — redirected into the dashboard, session cookie set
400Missing/expired/tampered state, or the code exchange / ID-token verification failed
403The connection has allowed_domains set and the asserted email’s domain isn’t in it
502The identity provider was unreachable

The session this sets is marked as established through an identity provider, so it satisfies the org’s require_2fa policy without TOTP.


Platform-wide Google and Microsoft sign-in, configured by a superadmin under social_login_* in PATCH /admin/settings. See Quick Start: Sign in with Google or Microsoft for setup and for the order in which a callback resolves the account. {provider} is google or microsoft; anything else is a 422.

Public. Which provider buttons the login and register pages render: a provider is listed only while social_login_enabled, its own social_login_{provider}_enabled, and both its client ID and secret are set.

{"providers": [{"id": "google", "label": "Google"}], "allow_signup": true}

Public. Redirects (307) to the provider, with a PKCE challenge, nonce and CSRF state in a short-lived oauth_state cookie (10 minutes). Optional ?invite=<token> carries an invitation link through the round trip.

StatusDescription
307Redirect to the provider
404The provider isn’t enabled
502The provider’s discovery document was unreachable

Public. The provider redirects here. Always answers 307 into the dashboard, never with an error body. The only exception is a redirect to GET /auth/sso/{org_id}/start when the email’s domain is claimed by an org’s SSO connection.

RedirectWhen
/overviewSigned in to an account with an organization; session cookie set
/onboardingSigned in to an account with no organization yet, including a new one
/login#mfa_token=…The account has its own TOTP. The dashboard completes it with POST /auth/session/2fa, the same as a password login
/account?linked={provider}A link started from /auth/me/identities/{provider}/link succeeded
/login?oauth_error=<code>Sign-in failed. For a link attempt, /account?oauth_error=<code>. See error codes

Session. The providers linked to the current account.

[{"provider": "google", "email": "[email protected]", "linked_at": "2026-09-10T08:00:00Z"}]

Session. Redirects to the provider. The callback attaches the provider account the user picks to this account (its email need not match), then returns to /account?linked={provider}.

Session. Unlinks the provider.

StatusDescription
200Unlinked
404That provider isn’t linked
409It is the account’s only way to sign in — no password, no org SSO connection, no other linked provider

Superadmin. Tests the provider’s saved configuration with a real round trip: it redirects to the provider like start does, whether or not the provider or the master switch is on. The callback runs every check a sign-in runs, then redirects to /admin/settings/sign-in?tab={provider}&test=<result> instead of signing anyone in. No account is created or linked, and the caller’s session is untouched. Recorded in the audit log as social_login.test.

test=Also carriesMeaning
okemail, live (1/0), warning=sso_domain when applicableThe configuration works
not_configured—No saved client ID and secret
discovery_faileddetailThe provider’s discovery document couldn’t be loaded
token_exchange_faileddetail — the provider’s own error, e.g. invalid_client: …The code exchange was refused
id_token_invaliddetailSignature, audience or issuer (Microsoft: tenant) didn’t verify
email_unverified, email_missing—The same refusals a sign-in would hit
provider_errordetailThe provider redirected back with an error
cancelled—The user backed out at the provider

Per-organization SCIM 2.0 provisioning (RFC 7643/7644) — Entra ID (or any SCIM-compliant IdP) pushes Users/Groups directly into this org, independent of any interactive login. Complements SSO above: SSO handles authentication, SCIM handles provisioning. See Quick Start: SSO & SCIM — Automatic provisioning via SCIM for a setup walkthrough.

Requires admin role. Enables SCIM provisioning for this org. 409 if a config already exists — use PATCH to modify one. Returns the bearer token once — it is never shown again.

Request body

{
"enabled": true,
"default_role": "member",
"group_role_mappings": [
{"pattern": "^admin", "role": "admin"}
]
}
FieldTypeDefaultDescription
enabledbooltrue—
default_rolestringmemberRole granted to a SCIM-provisioned user whose current groups match no group_role_mappings entry. Cannot be owner
group_role_mappingsarray[]Ordered {pattern, role} rules — pattern is a regular expression matched against each of a user’s current SCIM group (Team) names; the first entry that matches any of them wins. role cannot be owner

Response 201

{"token": "gRHDHWLB4utU3APb9ahLZXsTpyhDl8NghxL-H4lIPT4"}
StatusDescription
201Config created — copy token now
403Caller does not have admin role
409A config already exists for this org
422default_role/a mapping’s role was owner, or a pattern did not compile as a regular expression

Requires admin role. Returns the org’s config, or 404 if none is configured.

{
"enabled": true,
"default_role": "member",
"group_role_mappings": [{"pattern": "^admin", "role": "admin"}],
"token_set": true,
"base_url": "https://your-api/scim/v2/3f4a5b6c-...",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}

token_set is whether a bearer token exists — the raw value is never returned here, only at creation/rotation time. base_url is the exact tenant URL to paste into Entra ID.

Requires admin role. All fields optional. Does not touch the bearer token — use POST /orgs/{org_id}/scim/rotate-token to rotate it.

Requires admin role. Mints a new bearer token and invalidates the previous one immediately. Returns {"token": "..."} once, same as creation.

Requires admin role. Disconnects SCIM provisioning and invalidates the bearer token. Does not delete any Team or membership SCIM already created — same reasoning as disconnecting SSO not deleting Users.

SCIM protocol surface — /scim/v2/{org_id}/*

Section titled “SCIM protocol surface — /scim/v2/{org_id}/*”

Authenticated with the bearer token above (Authorization: Bearer <token>), never a session cookie or a regular API key — this credential only unlocks this surface, nothing else in the REST API. 401 if the token is missing/invalid/disabled, 403 if it belongs to a different org.

Method & pathDescription
GET /ServiceProviderConfig, GET /ResourceTypes, GET /SchemasStatic discovery documents, for the IdP’s “Test Connection” step
GET /UsersList/search — supports filter=userName eq "..." and startIndex/count pagination
POST /UsersProvision a user — links an existing local account by email (same rule as SSO’s JIT provisioning) or creates one, at the config’s default_role. Idempotent by email
GET /Users/{id}Get one
PATCH /Users/{id}, PUT /Users/{id}Update — {"active": false} deactivates the org membership (Entra sends this on unassignment), true reactivates. 409 if the target is the org’s owner
DELETE /Users/{id}Removes the org membership (not the underlying account, which may belong to other orgs). 409 if the target is the org’s owner
GET /GroupsList/search — supports filter=displayName eq "..."
POST /GroupsCreates a Team with this displayName
GET /Groups/{id}Get one
PATCH /Groups/{id}, PUT /Groups/{id}Update displayName/members — adding/removing members syncs Team membership and recomputes each affected member’s role via group_role_mappings
DELETE /Groups/{id}Deletes the Team (cascades its memberships) and recomputes every former member’s role