BluAuth
Docs
Sign in
User FAQ
  • Reset my password
  • I can't sign in
  • Didn't get reset email
  • Account linking
  • Session expiry
  • Two-factor auth
Admin Guides
Theme Studio
  • Overview
  • Layouts
  • Styling tokens
  • Concept copy
  • Assets & backgrounds
  • Advanced CSS
Admin Shell
  • Users
  • Providers
  • Clients
  • Invitations
Integrations
  • OIDC flow
  • Legacy OAuth flow
  • Provider token brokering
  • Email triggers
  • Webhook events
  • Session contract
Reference
  • API
  • Error codes
  • Event shapes
  • Design tokens
Runbooks
  • Deployment
  • Local operations

API Reference

BluAuth exposes four distinct surfaces, each with its own conventions:

  1. Protocol endpoints — OIDC (/api/oidc/**), legacy OAuth broker (/oauth2/**), and discovery (/.well-known/**). These return spec-compliant payloads, not BluAuth's envelope.
  2. Auth JSON API — /api/auth/**. Session-cookie authenticated. Returns the BluAuth envelope.
  3. Admin JSON API — /api/admin/**. Admin-role required. Returns the BluAuth envelope. CSRF-protected via session cookie.
  4. Provider token brokering — /api/provider-tokens/**. Bearer-token authenticated. Returns the BluAuth envelope.

All JSON APIs wrap success/error responses in the canonical envelope documented in Response envelopes. Protocol endpoints follow their respective RFCs.

Response envelopes

Success

{
    "success": true,
    "data": { "...": "resource or collection" },
    "meta": {
        "pagination": { "page": 1, "pageSize": 20, "total": 143, "totalPages": 8 },
        "count": 20,
        "hasMore": true
    }
}

meta is present on list endpoints only. debug is attached in non-production stages and includes timestamp, service, and version.

Error

{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Human-readable message",
        "status": 400,
        "requestId": "req_01H...",
        "fields": { "email": { "code": "invalid_format", "message": "Invalid email" } }
    }
}

fields is present only on VALIDATION_ERROR responses derived from Zod issues. debug is present in non-production stages with stack, cause, timestamp, service, and version. See the Error Codes reference for every code.

Authentication

SchemeWhere it appliesCredential
Session cookie/api/auth/**, /api/admin/**Better Auth session cookie issued by /api/auth/sign-in/* or OAuth callback
Admin role/api/admin/**Session belongs to a user with role = 'admin' (enforced via requireAdminRole)
OAuth Bearer/api/oidc/userinfo, /api/provider-tokens/:providerAuthorization: Bearer <access_token> issued by /api/oidc/token
Client authentication/api/oidc/token, /api/oidc/token/introspect, .../revokeclient_secret_basic, client_secret_post, or private_key_jwt (see token_endpoint_auth_methods_supported)
Cross-origin public/.well-known/**, /api/oidc/authorize, /api/oidc/jwks, /api/oidc/end-session, /api/health, /api/auth/theme, /api/auth/providers, /api/auth/client-policy, /api/auth/invitation-contextNone — publicly readable with CORS allow

OIDC endpoints

All OIDC endpoints return spec-compliant payloads. Errors follow OIDC Core / OAuth 2.0 conventions ({"error": "...", "error_description": "..."}), not the BluAuth envelope.

MethodPathPurpose
GET/.well-known/openid-configurationDiscovery document
GET/api/oidc/authorizeStart authorization-code flow
POST/api/oidc/tokenExchange code for tokens (or client_credentials)
POST/api/oidc/token/introspectIntrospect an access token (RFC 7662)
POST/api/oidc/token/revokeRevoke an access token (RFC 7009)
GET | POST/api/oidc/userinfoGet user claims (Bearer-authed)
GET/api/oidc/jwksPublic keys for ID-token signature verification
GET/api/oidc/end-sessionRP-Initiated logout (OpenID Connect RP-Initiated Logout 1.0)

GET /.well-known/openid-configuration

Discovery document. Returns:

{
    "issuer": "https://auth.blutools.io",
    "authorization_endpoint": "https://auth.blutools.io/api/oidc/authorize",
    "token_endpoint": "https://auth.blutools.io/api/oidc/token",
    "userinfo_endpoint": "https://auth.blutools.io/api/oidc/userinfo",
    "jwks_uri": "https://auth.blutools.io/api/oidc/jwks",
    "introspection_endpoint": "https://auth.blutools.io/api/oidc/token/introspect",
    "revocation_endpoint": "https://auth.blutools.io/api/oidc/token/revoke",
    "end_session_endpoint": "https://auth.blutools.io/api/oidc/end-session",
    "scopes_supported": ["openid", "profile", "email", "admin"],
    "response_types_supported": ["code"],
    "grant_types_supported": ["authorization_code", "client_credentials"],
    "subject_types_supported": ["pairwise"],
    "id_token_signing_alg_values_supported": ["ES256"],
    "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt"],
    "code_challenge_methods_supported": ["S256"]
}

GET /api/oidc/authorize

Start the OIDC authorization-code flow. On success, redirects the user agent to redirect_uri with code and state.

Query parameters:

ParamRequiredNotes
client_idyesUUID. Must match an active row in oauth_clients
redirect_uriyesMust match one of the client's registered redirectUris
response_typeyesMust be code
scopeyesSpace-delimited. All requested scopes must be in allowedScopes
stateyesOpaque relay value
nonceyesRequired — rejected if missing
code_challengeyesRequired (PKCE enforced)
code_challenge_methodyesMust be S256

Behavior:

  • If the session is unauthenticated, redirects to /login?unauthorized=true&client_slug=<slug> and stores returnTo as the OIDC continuation so the user is returned to authorize after login.
  • If the session requires a password reset, redirects to /reset-password with continuation state.

POST /api/oidc/token

Exchange an authorization code for tokens, or mint a client_credentials token.

Authentication: client authentication per token_endpoint_auth_methods_supported. Accepts client_secret_basic, client_secret_post, or private_key_jwt.

Body (authorization_code): grant_type=authorization_code&code=<code>&redirect_uri=<uri>&code_verifier=<verifier>

Body (client_credentials): grant_type=client_credentials (public clients cannot use this grant — 400).

Response:

{
    "access_token": "...",
    "id_token": "...",
    "token_type": "Bearer",
    "expires_in": 3600
}

client_credentials tokens carry sub = client_id with no user claims.

Responses set Cache-Control: no-store and Pragma: no-cache per OAuth 2.0 §5.1.

POST /api/oidc/token/introspect

RFC 7662. Authenticates the client, then returns the introspection response.

Body: token=<access_token>&token_type_hint=access_token

Response (active):

{
    "active": true,
    "sub": "<pairwise-subject-or-client-id>",
    "client_id": "<client-uuid>",
    "scope": "openid profile email",
    "token_type": "Bearer",
    "exp": 1713283200,
    "iat": 1713279600
}

Returns {"active": false} when the token is unknown, expired, or belongs to a different client.

POST /api/oidc/token/revoke

RFC 7009. Always returns HTTP 200 with {"ok": true}, regardless of whether the token existed. Only revokes tokens that belong to the authenticating client.

Body: token=<access_token>&token_type_hint=access_token

GET / POST /api/oidc/userinfo

Returns claims for the authenticated user. Requires Authorization: Bearer <access_token>.

Claims returned reflect scopes granted to the token:

  • openid — sub
  • profile — name, given_name, family_name, picture, preferred_username
  • email — email, email_verified

GET /api/oidc/jwks

Returns the signing-key JWKS for verifying BluAuth-issued ID tokens. Keys are ES256 on P-256. Rotated monthly by KeyRotationCron.

GET /api/oidc/end-session

RP-Initiated Logout (OpenID Connect RP-Initiated Logout 1.0). Clears the Better Auth session cookie, deletes the session row, then redirects.

Query parameters:

ParamRequiredNotes
id_token_hintnoPreviously issued ID token
post_logout_redirect_urinoMust match some active client's postLogoutRedirectUris — else 400
statenoOpaque value relayed to the RP

Legacy OAuth broker

Phase-1 compatibility surface. Downstream apps that predate OIDC integration still call these.

MethodPathPurpose
GET/oauth2/loginStart legacy broker flow
GET | POST/oauth2/callbackReturn from upstream, redirect with session + returnTo

GET /oauth2/login

Query parameters:

ParamNotes
providerrequired — provider slug
returnTorelative path or a URL matching an active client's redirectUris
modepopup opts into window.postMessage completion
originrequired when mode=popup — target origin for postMessage
intentlink to attach the upstream account to the current session
invite to claim an invitation after upstream authentication
invitation_tokenrequired when intent=invite
client_idrequired for client-bound consent & scope escalation
additional_scopesspace-delimited — merges with provider's base scopes
promptOIDC prompt value — downgraded against provider prompt_values_supported

On success, redirects the user agent to the upstream IdP's authorization URL.

When intent=invite, BluAuth validates the invitation token up front and rejects providers that are not linked to the invited client.

GET/POST /oauth2/callback

Exchanges the upstream code, resolves identity (create or link), issues a BluAuth session, publishes user.created, account.linked, and session.created events, and redirects to returnTo. On popup mode, returns HTML that calls window.opener.postMessage.

When the stored broker state has intent=invite, the callback validates the invitation again, re-checks provider eligibility for the invited client, accepts the invitation for the resolved user, and redirects to the invitation onboarding path instead of the generic returnTo.


Auth JSON API

Session-authenticated. All endpoints return the canonical envelope unless noted.

MethodPathAuthPurpose
GET/api/auth/providerspublicProviders visible for client_slug (or default)
GET/api/auth/client-policypublicHosted-login password/signup policy for a client
GET/api/auth/invitation-contextpublicResolve invitation status and themed client context
GET/api/auth/themepublicResolve theme by client_slug or theme_id
GET/api/auth/linkable-providerssessionAll active providers the user may link
GET/api/auth/security-statesessionSession metadata: user, requirePasswordReset, role
POST/api/auth/session-contextsessionStamp a clientSlug onto the active session
POST/api/auth/change-passwordsessionChange password for the authenticated user
POST/api/auth/accept-invitepublicAccept an invitation; creates or updates a user
ALL/api/auth/[...all]mixedBetter Auth catch-all (sign-in, sign-up, reset, etc.)

GET /api/auth/providers

Query: client_slug (optional — defaults to default).

Response: { success: true, data: ProviderInfo[] } where each item has { slug, name, type, buttonText, styleVariant, iconUrl }. Cached in Valkey for the PROVIDER_LIST TTL.

GET /api/auth/client-policy

Query: client_slug (optional — defaults to default).

Response: { success: true, data: { clientSlug, passwordAuthEnabled, selfServiceSignupEnabled, requireMfaForPasswordAuth } }.

Use this to drive hosted-login UX. Provider listings and password/signup policy are intentionally separate: /api/auth/providers tells you which upstream providers to render, while this endpoint tells you whether BluAuth should render or accept credential auth for that client_slug.

GET /api/auth/invitation-context

Query: token (required).

Response: { success: true, data: { status, tokenValid, email?, clientSlug?, clientName?, onboardingPath?, requiredProviderKeys, passwordAllowed } }.

This is the public preflight for /invite. It lets the hosted page resolve the correct client branding and invitation state before the user sets a password or begins upstream sign-in.

GET /api/auth/theme

Query: exactly one of client_slug or theme_id. theme_id takes priority.

Response: resolved theme payload — see Design Tokens for the full field list. Cached under CacheKey.theme(clientSlug).

GET /api/auth/linkable-providers

Requires session. Returns all active providers the authenticated user could link.

GET /api/auth/security-state

Returns:

{
    "success": true,
    "data": {
        "authenticated": true,
        "isAdmin": false,
        "requirePasswordReset": false,
        "user": { "id": "...", "email": "...", "name": "...", "role": "user" }
    }
}

POST /api/auth/session-context

Body: { "clientSlug": "string" }. Writes clientSlug onto the current sessions row so downstream telemetry and webhook emissions can carry the initiating client.

POST /api/auth/change-password

Body: { "currentPassword": "string", "newPassword": "string (min 8)" }. Verifies against the current credential account, enforces the password policy (incl. pwned-password checks when enabled in service_settings), then clears requirePasswordReset.

Errors: AUTHENTICATION_REQUIRED if current password doesn't match; VALIDATION_ERROR if password policy fails.

POST /api/auth/accept-invite

Body: { "token": "string", "password": "string (min 8)" }. Accepts an invitation on the BluAuth password path; creates the user or updates the existing user's password. Publishes invitation.accepted before issuing the session. Returns { userId, clientSlug, onboardingPath }.

If an invitation requires one or more providers, this endpoint rejects the password path with a validation/business-rule error and the invite must be claimed through the OAuth broker instead.

Invite links can also be claimed through the OAuth broker with intent=invite. In invite mode, BluAuth validates the invitation token, verifies that the chosen provider is linked to the invited client, verifies the provider is one of the invitation's required providers when applicable, resolves the authenticated identity, and then accepts the invitation. If the provider email differs from the invited email, BluAuth can attach the invited address as a verified alias when the invite token proves control of that address.

ALL /api/auth/[...all]

Better Auth catch-all. Handles sign-in/sign-up, forgot-password, reset-password, email-verification, and all standard credential flows. Responses follow Better Auth's shape, not the BluAuth envelope.


Admin JSON API

Admin role required on every route. Returns the canonical envelope. CSRF enforced via session cookie. Rate-limited.

Users — /api/admin/users

MethodPathBody / QueryNotes
GET/page, pageSize≤100, searchPaginated list; filters deletedAt IS NULL
POST/{ email, name?, firstName?, lastName?, password | generatePassword, requirePasswordReset? }201 on create; 409 with existing user info if email collides
GET/:id—Full user payload
PUT/:idpartial user fieldsIdempotent
DELETE/:id—Soft-deletes. Fires user.deleted before removal
POST/:id/password{ password | generatePassword: true, requirePasswordReset? }422 if SSO-only and allowPasswordForSsoUsers=false; returns temporaryPassword when generated
GET/:id/merge-previewtargetUserIdPreview entity moves
POST/:id/merge{ targetUserId, reason? }Merges source into target; records an audit log

Providers — /api/admin/providers

MethodPathBody / QueryNotes
GET/page, pageSize≤100, search, typePaginated list
POST/{ name, slug, type, clientId, clientSecret, issuerUri?, discoveryUrl?, scopes? }201; auto-discovery fetched for type=oidc with discoveryEnabled; secrets encrypted via KMS
GET/:id—Excludes encrypted secrets
PUT/:idpartial provider fieldsRefreshes discovery if issuerUri or discoveryUrl changes
DELETE/:id—Hard-delete. Cache invalidated
POST/:id/refresh-discovery—Re-fetches .well-known/openid-configuration and rewrites security policy

Clients — /api/admin/clients

MethodPathBody / QueryNotes
GET/page, pageSize≤100, searchPaginated list
POST/{ name, slug, clientType?, redirectUris, postLogoutRedirectUris?, allowedScopes?, themeId?, allowedProviderTokens? }201. Issues a private_key_jwt assertion keypair — private key returned only on this response
GET/:id—Excludes private key
PUT/:idpartial client fieldsIdempotent
DELETE/:id—Soft-deletes
GET/:id/providers—Providers linked to this client
POST/:id/providers{ providerId }Link a provider
DELETE/:id/providers/:providerId—Unlink a provider
POST/:id/rotate-secret—Regenerates the private_key_jwt keypair. Private PEM returned once with warning

Themes — /api/admin/themes

MethodPathBodyNotes
GET/—Unpaginated list (admin-only)
POST/theme fields — see design tokens201
GET/:id—
PUT/:idpartial theme fieldsInvalidates cached theme for every client referencing it
DELETE/:id—Fails with RESOURCE_CONFLICT if any client uses it
POST/:id/duplicate—Clones theme with name = "<name> (copy)"

Invitations — /api/admin/invitations

MethodPathBody / QueryNotes
GET/page, pageSize≤100, search, status, clientIdPaginated with client join
POST/{ email, clientId, context?, requiredProviderKeys?, ttlDays? (1-30) }201. Fires invitation.created. Email-send failure does not fail the request
DELETE/:id—Revokes a pending invitation. Fires invitation.revoked. 400 if invitation is already accepted/revoked

Signing Keys — /api/admin/signing-keys

MethodPathNotes
GET/Lists all keys with kid, algorithm, isActive, createdAt
POST/generateMints an ES256 keypair in KMS and a corresponding JWKS entry. 201

Webhooks — /api/admin/webhooks

MethodPathBody / QueryNotes
GET/page, pageSize≤100, clientId?Paginated
POST/{ clientId, url, secret, events: string[], isActive? }201. secret KMS-encrypted at rest
GET/:id—Excludes secret
PUT/:idpartial fieldsSupplying a new secret re-encrypts
DELETE/:id—Hard-delete
GET/:id/deliveriespage, pageSize≤100Paginated delivery log incl. statusCode, attempts, nextRetryAt, deliveredAt

Settings — /api/admin/settings

MethodPathBodyNotes
GET/—Returns resolved service_settings
PUT/{ blockPwnedPasswords?, allowPasswordForSsoUsers? }At least one field required

Assets — /api/admin/assets/upload-url

MethodPathBodyNotes
POST/{ kind: 'theme_logo' | 'provider_icon' | 'theme_background', fileName, contentType, sizeBytes }Returns { uploadUrl, cdnUrl, storageKey, expiresIn: 300 }. Upload directly to uploadUrl with PUT

Email Test — /api/admin/email-test

MethodPathBodyNotes
POST/{ themeId: uuid | null, recipientEmail, previewOnly?, emailConcept?: 'password-reset' | 'welcome' | 'invitation' }With previewOnly: true returns rendered HTML/text; otherwise sends via SES

Provider token brokering

POST /api/provider-tokens/:provider

Mints / refreshes an upstream provider access token for the authenticated user.

Auth: Authorization: Bearer <bluauth-access-token>. The bearer is a BluAuth-issued access token from /api/oidc/token.

Body: { "requiredScopes": ["https://www.googleapis.com/auth/calendar.readonly", "..."] } (optional).

Response:

{
    "success": true,
    "data": {
        "accessToken": "<upstream-provider-token>",
        "expiresIn": 3540,
        "provider": "google",
        "scopes": ["openid", "email", "https://www.googleapis.com/auth/calendar.readonly"]
    }
}

Authorization: the calling client's allowedProviderTokens must include :provider; a per-client grant must exist; granted scopes must cover requiredScopes.

Errors:

  • INVALID_TOKEN (401) — missing or unknown bearer
  • UNAUTHORIZED_CLIENT (403) — client not permitted for this provider
  • NO_LINKED_ACCOUNT (404) — user has no linked account for :provider
  • INSUFFICIENT_SCOPE (403) — grant exists but does not cover requiredScopes
  • REAUTH_REQUIRED (502) — upstream refresh token revoked; user must re-login
  • UPSTREAM_PROVIDER_ERROR (502) — provider returned a transient failure

Responses always set Cache-Control: no-store, private.

See the provider token brokering guide for end-to-end integration patterns.


Operations

GET /api/health

Liveness + dependency check. Returns HTTP 200 when healthy, 503 otherwise.

{
    "status": "healthy",
    "checks": {
        "database": { "status": "ok", "latency": 12 },
        "cache": { "status": "ok", "latency": 2 }
    },
    "timestamp": "2026-04-17T10:30:00.000Z"
}

Cache status can be ok, degraded, or warming. warming does not flip healthy false.


Conventions

  • Pagination — page (≥1) and pageSize (≤100). Response meta.pagination = { page, pageSize, total, totalPages }.
  • Timestamps — ISO-8601 UTC with Z suffix. Server uses nowUTC() throughout.
  • IDs — UUID v4 unless otherwise stated. URL params are parsed via getRouterParam.
  • Secrets in responses — returned exactly once (client clientAssertionPrivateKey, temporaryPassword, upload presigns). Never logged; never re-fetchable.
  • Cache headers — token and provider-token endpoints set Cache-Control: no-store. Other endpoints use defaults.
  • Request IDs — every error response carries requestId. Log it on the client to correlate with server-side Sentry issues.
  • CORS — authorize, userinfo, jwks, end-session, /.well-known/**, /api/auth/providers, /api/auth/client-policy, and /api/auth/invitation-context allow cross-origin reads. token, token/introspect, token/revoke are server-to-server.

On this page

  • Response envelopes
  • Success
  • Error
  • Authentication
  • OIDC endpoints
  • GET /.well-known/openid-configuration
  • GET /api/oidc/authorize
  • POST /api/oidc/token
  • POST /api/oidc/token/introspect
  • POST /api/oidc/token/revoke
  • GET / POST /api/oidc/userinfo
  • GET /api/oidc/jwks
  • GET /api/oidc/end-session
  • Legacy OAuth broker
  • GET /oauth2/login
  • GET/POST /oauth2/callback
  • Auth JSON API
  • GET /api/auth/providers
  • GET /api/auth/client-policy
  • GET /api/auth/invitation-context
  • GET /api/auth/theme
  • GET /api/auth/linkable-providers
  • GET /api/auth/security-state
  • POST /api/auth/session-context
  • POST /api/auth/change-password
  • POST /api/auth/accept-invite
  • ALL /api/auth/[...all]
  • Admin JSON API
  • Users — /api/admin/users
  • Providers — /api/admin/providers
  • Clients — /api/admin/clients
  • Themes — /api/admin/themes
  • Invitations — /api/admin/invitations
  • Signing Keys — /api/admin/signing-keys
  • Webhooks — /api/admin/webhooks
  • Settings — /api/admin/settings
  • Assets — /api/admin/assets/upload-url
  • Email Test — /api/admin/email-test
  • Provider token brokering
  • POST /api/provider-tokens/:provider
  • Operations
  • GET /api/health
  • Conventions
DocsPrivacyTerms
© 2026 Blu Digital Group