API reference
Authentication
Unless a route is marked public, it requires an authenticated BetterAuth
session. Admin routes (/api/admin/*) require the admin or superadmin
global role. Pass the session cookie set on sign-in. An
Authorization: Bearer <token> header is honoured only where a route says so:
the wallet read routes (a user JWT with scope wallet:self:read), the machine
routes (/api/internal/*, /api/app/billing/*) and the consumption routes
(a client_credentials token). Unauthenticated requests to session routes
receive 401 AUTH_001; authenticated requests lacking the required role
receive 403 AUTH_011. Errors use the envelope described in
Error codes.
System
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /health | public | Health check. Returns { "status": "ok", "version": "<version>" } |
GET | /error | public | Templated authentication-error page (HTML; requires TEMPLATES_DIR, otherwise the SPA handles the path). Query: error, error_description, optional client_id (per-application template and branding) and login (same-origin back-link) |
GET | /select-org | session | Organization selection page shown during an authorization with the org scope when the user belongs to several organizations (templated with TEMPLATES_DIR, otherwise the SPA). Anonymous callers are redirected to /login |
GET | /complete-registration | public | Templated one-shot registration page reached from an application invitation email. Query: token (the invitation), optional client_id. A pending token renders the email/password form and social-provider buttons. A provider callback returns here with a session and renders the completion page. Anything else redirects to /error |
GET | /accept-invitation | public | Templated organization invitation page. Query: id (the BetterAuth invitation). Renders registration (email/password and social providers) or the acceptance/completion state. Unknown ids redirect to /error |
Discovery (OIDC / OAuth 2.1)
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /.well-known/openid-configuration | public | OIDC discovery document |
GET | /.well-known/oauth-authorization-server | public | RFC 8414 authorization-server metadata |
BetterAuth
All BetterAuth-managed routes are served under /api/auth/. See
OAuth 2.1 / OIDC for the full flow.
| Method | Path | Auth | Description |
|---|---|---|---|
ALL | /api/auth/* | varies | BetterAuth handler — sign-in, sign-up, sign-out, sessions, OAuth2, OIDC, MFA, passkeys, social login |
GET | /api/auth/jwks | public | JSON Web Key Set |
GET/POST | /api/auth/oauth2/authorize | public | Authorization endpoint |
POST | /api/auth/oauth2/token | client credentials / public (PKCE) | Token endpoint |
POST | /api/auth/oauth2/introspect | client credentials | Token introspection |
POST | /api/auth/oauth2/revoke | client credentials | Token revocation |
GET/POST | /api/auth/oauth2/end-session | id_token_hint / session | RP-initiated logout (needs enableEndSession on the application). See Sessions and sign-out |
GET | /api/auth/oauth2/userinfo | Bearer | UserInfo endpoint |
App configuration (public)
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/app-config | public | Registration policy and enabled social providers. Query param client_id=<app-slug> (an unknown slug answers 404 APP_002). Without it (the service's own sign-in) registration is closed and no social provider is offered. |
User self-service
All routes require the caller's own session.
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/user/subscription | session | Own app access list with plan details, each app's availablePlans, and stripeConfigured |
PATCH | /api/user/subscription/:applicationId | session | Self-service plan change ({ planId }). A free plan applies at once. 403 SUB_005 when Stripe is not configured. A paid plan answers { ok, changed: false, pending, checkoutUrl }: pending: true when a live paid subscription was changed in place (the plan follows Stripe's update), otherwise checkoutUrl to open (Stripe Checkout, which grants the plan on completion). 409 SUB_007 is not returned here: a checkout over a live paid subscription is never offered |
GET | /api/user/billing/invoices | session | The caller's invoices across applications and wallet top-ups, newest first. Query limit (1–100, default 20), cursor. Returns { items, nextCursor } (same item shape as the application read API) |
GET | /api/user/billing/usage | session | Metered usage of the current period for an application the caller uses: ?applicationId= → { periodStart, features }. 403 APP_005 without access |
POST | /api/user/billing/checkout | session | Open a Stripe Checkout in subscription mode for a paid plan: { applicationId, planId } → { url }. The plan must have a recurring price; 403 APP_005 without access to the application |
POST | /api/user/billing/portal | session | Open the Stripe Billing Portal (payment methods, cancellation) for the user's Stripe Customer → { url } |
GET | /api/user/consumption/:appId | session | Own consumption aggregates for an application |
GET | /api/user/sessions | session | Own active sessions |
DELETE | /api/user/sessions/:id | session | Revoke one of own sessions (404 AUTH_016 when it does not exist or is not yours) |
GET | /api/user/organizations | session | Organizations the user belongs to |
PATCH | /api/user/profile | session | Update own profile (name, phone, company, position, address, image) |
Wallet
Credit is held in integer micro-euros (1 EUR = 1 000 000), shared by every
application and never expiring. The balance goes negative only when
something that already happened is recorded: usage reported after the call, or
a refunded payment whose credit was spent. The debt blocks further spending
(canSpend: false) and is repaid by the next top-up. Amounts are sent and returned
as strings (JSON has no 64-bit integer). Every write takes an
idempotencyKey: replaying the same key with the same parameters returns the
original transaction (200, replayed: true), with other parameters 409 WAL_003. A first application answers 201.
Account: { "balance": "5000000", "isUnlimited": false, "canSpend": true, "currency": "eur" }. canSpend is true for an unlimited account or a strictly
positive balance: applications should block on it rather than re-derive it.
Transaction: { id, type, amount, delta, balanceAfter, applicationId, metadata, createdAt } where type is topup, grant, refund, usage, adjust or
reversal (a payment taken back by a refund or dispute),
amount is the signed value of the operation and delta its effect on the
balance (0 for usage on an unlimited account).
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/user/wallet | session or JWT scope wallet:self:read | The caller's wallet |
GET | /api/user/wallet/topup-options | session or JWT scope wallet:self:read | Whether online top-up is available and its bounds: { enabled, currency, minCents, maxCents } |
POST | /api/user/wallet/checkout | session | Open a Stripe Checkout to buy credit: { amountCents } → 201 { url }. 503 WAL_006 when Stripe is not configured |
GET | /api/user/wallet/stats | session or JWT scope wallet:self:read | The caller's spend over the last days (1–90, default 30): { days, total, notBilled, calls, series, byApplication, byKind, byModel, daily }. total is the real consumption, unlimited accounts included; notBilled is the part of it that was never deducted. series has one point per UTC day, empty days included. byModel groups by model (with prompt/completion/total tokens; mcp for tool calls), and daily gives the per-day split by application and by model |
GET | /api/user/wallet/usage | session or JWT scope wallet:self:read | What one chat session spent: query sessionId (required, ≤200) and days (1–90, default 30) → { sessionId, applicationSlug, days, total, notBilled, calls, byModel }. The sessionId is the one the application passed to the gateway (metadata.session_id); a bearer caller is bound to its own application (the token azp), so two applications cannot read each other's identical session ids |
GET | /api/user/wallet/transactions | session or JWT scope wallet:self:read | The caller's history, newest first. Query: limit (1–200, default 50), cursor (from nextCursor), type. Returns { items, nextCursor } |
GET | /api/internal/wallet/users/:userId | machine wallet:read | A user's wallet (created empty on first read) |
POST | /api/internal/wallet/users/:userId/usage | machine wallet:debit | Charge a consumption: { amount, idempotencyKey, applicationSlug?, metadata? }. Always recorded, even beyond the balance (the debt blocks the next calls). The gateway's metadata carries kind, model, the token counts and, when the client passed one, sessionId |
GET | /api/internal/gateway/users/:userId/plan | machine wallet:read | The plan a user holds for an application, and its gateway team: ?applicationSlug= → { plan, teamId } (teamId is null when the user holds no plan). Called by the gateway's custom auth to pick the team of each request |
GET | /billing/subscribe, /billing/invoices, /billing/usage | session (the signed-in user) | The delegated billing pages of an application (?client_id=<slug>), served only when its billing_mode is delegated and the user has access. Anything else: 302 to the application's sign-in when anonymous, 404 for a non-delegated application, 403 without access, 500 (static page, never the SPA) when the template fails to render |
GET | /api/app/billing/users/:userId/subscription | machine billing:read (for the calling application) | The user's subscription in the calling application: { subscription } with plan, isActive, expiresAt, stripeStatus, currentPeriodStart/End, cancelAtPeriodEnd, canceledAt. subscription is null when the user holds none. The application is the token's client, never a request parameter |
GET | /api/app/billing/users/:userId/usage | machine billing:read | Metered usage of the current UTC month for the calling application: { periodStart, features }, each feature with featureKey, unit, usageLimit (-1 = unlimited), used, remaining (null when unlimited) |
GET | /api/app/billing/users/:userId/invoices | machine billing:read | The user's Stripe invoices for the calling application, newest first. Query: limit (1–100, default 20), cursor (from nextCursor). Returns { items, nextCursor }; each item has id, status, billingReason, currency, amountDue, amountPaid, amountRemaining (minor units), periodStart, periodEnd, createdAt, hostedInvoiceUrl, invoicePdf. Wallet top-up invoices are not listed here (they belong to no application) |
GET | /api/admin/wallets | admin | Wallets with the owner's name and email, balance and 30-day spend. Query: page, limit, search (name or email), filter (all, debt, credit, unlimited). Returns { wallets, total, page, limit } |
GET | /api/admin/wallets/stats | admin | Platform totals (credit held, debt, accounts in debt or unlimited), spend (with its notBilled share, i.e. usage on unlimited accounts) and top-up series over days, five top consumers, breakdown by application, by type and by model |
GET | /api/admin/wallets/pricing | admin | The pricing in force and its last 20 changes: { current: { usdToEurRate, marginPercent, source, change } | null, history }. source is environment until a superadmin saves a value; current is null when no rate is set anywhere (usage answers 503 WAL_007) |
PUT | /api/admin/wallets/pricing | superadmin | Save the exchange rate and margin: { usdToEurRate, marginPercent, reason } (rate: positive decimal string, up to 8 decimals; margin: integer 0–1000). Applies to the next debit; recorded with its author |
GET | /api/admin/wallets/usage-ledger | admin | Usage debits reported by the gateway in [from, to) (ISO dates, limit up to 1000, cursor), oldest first: { items: [{ requestId, userId, amount, costUsd, createdAt }], nextCursor }. Read by pnpm llm:reconcile |
GET | /api/admin/wallets/users/:userId/stats | admin | A user's spend statistics (same shape as the user endpoint) |
GET | /api/admin/wallets/users/:userId | admin | A user's wallet |
GET | /api/admin/wallets/users/:userId/transactions | admin | A user's history (same query as above) |
POST | /api/admin/wallets/users/:userId/topups | superadmin | Record credit bought outside Stripe (manual invoice): { amount, idempotencyKey, reference } |
POST | /api/admin/wallets/users/:userId/grants | superadmin | Give credit: { amount, idempotencyKey, reason } |
POST | /api/admin/wallets/users/:userId/adjustments | superadmin | Signed correction: { amount, idempotencyKey, reason } (negative removes credit; 402 WAL_002 if it would take the balance below zero: only usage and reversals may) |
PUT | /api/admin/wallets/users/:userId/unlimited | superadmin | Toggle unlimited access: { isUnlimited, reason }. Audited |
Machine routes (/api/internal/*) accept only a client_credentials
token carrying the route's scope. The client's current configuration is
checked too, so disabling the client or removing a scope takes effect
immediately. The usage metadata is a flat map (max 32 entries) for raw
quantities such as model and token counts: never put secrets in it.
applicationSlug is the calling application (the end user's token azp); the
usage is still charged if that application has been deleted since.
Consumption
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/consumption | client_credentials token / admin session | Record a consumption entry ({ applicationId, userId, key, value, idempotencyKey? }). When key is a metered feature of the user's active plan, it is billed instead: the per-period aggregate is capped (403 BIL_003 past the limit), the report is idempotent on idempotencyKey, and the response is { success, metered: true, aggregate } |
GET | /api/consumption/:userId/:applicationId | client_credentials token / admin session | Aggregates for a user + application |
GET | /api/consumption/:userId/:applicationId/:key | client_credentials token / admin session | Single aggregate by key |
DELETE | /api/consumption/:userId/:applicationId/:key | admin | Reset a consumption counter |
A client_credentials token is bound to its own application (the token's azp) and gets 403 AUTH_011 on another one; a user-bound bearer token is refused with 403 CONS_004, and so is a caller with neither a valid token nor an admin session.
Admin — Applications
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/applications | admin | List all applications |
POST | /api/admin/applications | admin | Create an application (auto-creates default roles and a free plan) |
GET | /api/admin/applications/favicon?url= | admin | Detect the favicon declared by the page at url ({ iconUrl }, null when none). Public hosts only (SSRF-guarded, redirects re-checked), 30 requests per minute; the admin form uses it to pre-fill the icon |
GET | /api/admin/applications/:id | admin | Get a single application |
PATCH | /api/admin/applications/:id | admin | Update an application |
DELETE | /api/admin/applications/:id | admin | Delete an application (cascades) |
POST | /api/admin/applications/:id/rotate-secret | admin | Rotate the OAuth client secret |
PATCH | /api/admin/applications/:id/providers | admin | Update per-application enabled social providers |
GET | /api/admin/applications/:id/machine-scopes | admin | Scopes the application may request through client_credentials |
POST | /api/admin/applications/:appId/users/:userId/subscription/cancel | admin | Stop the user's Stripe subscription at the end of its period → { ok, cancelAtPeriodEnd: true }. 404 SUB_008 without a Stripe subscription |
POST | /api/admin/applications/:appId/users/:userId/subscription/resume | admin | Undo the above → { ok, cancelAtPeriodEnd: false } |
PUT | /api/admin/applications/:id/machine-scopes | superadmin | Replace them ({ scopes }, among m2m, wallet:read, wallet:debit, billing:read). Private applications only; removing a scope revokes the client's tokens |
Registration (public)
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/registration/complete | public | Redeem an application invitation with email/password ({ token, name, password }, password ≥ 8). Creates a verified account, grants the invited application's access and consumes the invitation. 400 AUTH_012 for an invalid, expired or already-used token; 409 AUTH_013 when the address already has an account. Rate-limited (20/min) |
POST | /api/registration/organization-invitation/complete | public | Redeem an organization invitation with email/password ({ invitationId, name, password }). Creates a verified account, adds it to the invited organization and consumes the invitation in one transaction. No application access is granted. 400 AUTH_012, 409 AUTH_013. Rate-limited (20/min) |
POST | /api/registration/organization-invitation/accept | session | Accept an organization invitation for the signed-in account ({ invitationId }). The session email must match the invited address (403 AUTH_014) and be verified (403 AUTH_015). Delegates to the organization plugin. Rate-limited (20/min) |
POST | /api/registration/organization-invitation/social | public | Start a social sign-up/sign-in for an organization invitation ({ invitationId, provider }). Returns { url } to redirect the browser. Carries a short-lived signed invitation in BetterAuth's server-trusted OAuth state; the account created by the provider must match the invited address. Rate-limited (20/min) |
POST | /api/registration/application-invitation/accept | session | Finish an application invitation for a session created after the invitation (social sign-up). Body { token }; the session email must match (403 AUTH_014) and be verified (403 AUTH_015). Grants application access and consumes the token. No organization is assigned. Rate-limited (20/min) |
POST | /api/registration/application-invitation/social | public | Start a social sign-up/sign-in for an application invitation ({ token, provider }). The provider must be enabled on the application. Returns { url } and carries the signed invitation in BetterAuth's server-trusted OAuth state. Rate-limited (20/min) |
Admin — Application users
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/applications/:id/users | admin | List users with access to the application. Paginated (?page=&limit=&search=); returns { users, total, page, limit } |
GET | /api/admin/applications/:id/subscriptions | admin | Compact subscription rows for the financial tab ({ userId, planId, isActive }) |
POST | /api/admin/applications/:id/users | admin | Grant a user access ({ userId, roleId?, subscriptionPlanId?, reason? }; defaults to the app's default plan when no plan is given) |
POST | /api/admin/applications/:id/users/bulk | admin | Grant many users ({ organizationId | userIds, roleId?, subscriptionPlanId? }) |
PATCH | /api/admin/applications/:id/users/:userId | admin | Update a user's access record (isActive, roleId, subscriptionPlanId, expiresAt, reason). reason is required when subscriptionPlanId or expiresAt is present; set subscriptionPlanId to null to revoke the subscription |
DELETE | /api/admin/applications/:id/users/:userId | admin | Revoke a user's access |
GET | /api/admin/applications/:id/users/:userId/history | admin | Login history for the user in this application. Paginated (?page=&limit=); returns { entries, total, page, limit } |
Admin — Registration invitations
Admin-triggered, one-shot registration for an address that has no account, on an application whose self-registration is closed. The token is emailed as a link to /complete-registration, stored only as a hash, single-use and time-limited (72 hours).
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/admin/registration-invitations | admin | Create an invitation ({ email, applicationId }). Emails the link and returns { invitation, inviteUrl, emailSent } (201). inviteUrl is returned once so the administrator can hand it over when mail is not configured. 409 AUTH_013 when the address already has an account |
GET | /api/admin/registration-invitations | admin | List invitations, newest first. Optional applicationId filter and page/limit or offset paging (default 20, max 100). Returns { invitations, total, page, limit } |
DELETE | /api/admin/registration-invitations/:id | admin | Revoke a pending invitation (400 AUTH_012 when it is missing or already used) |
POST | /api/admin/registration-invitations/:id/resend | admin | Issue a fresh invitation for the same address and application, voiding the previous pending link (201) |
Admin — Roles and permissions
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/applications/:appId/roles | admin | List roles with their permission assignments |
POST | /api/admin/applications/:appId/roles | admin | Create a role |
PATCH | /api/admin/applications/:appId/roles/:roleId | admin | Update a role's description (null clears it) and/or isDefault. The name is immutable: any other field is a 400 APP_001 |
DELETE | /api/admin/applications/:appId/roles/:roleId | admin | Delete a role (fails if users are assigned) |
GET | /api/admin/applications/:appId/permissions | admin | List permissions |
POST | /api/admin/applications/:appId/permissions | admin | Create a permission (resource + action) |
DELETE | /api/admin/applications/:appId/permissions/:permId | admin | Delete a permission |
POST | /api/admin/applications/:appId/roles/:roleId/permissions | admin | Assign a permission to a role ({ permissionId }, idempotent). Role and permission must belong to the application (PERM_002 / PERM_003) |
DELETE | /api/admin/applications/:appId/roles/:roleId/permissions/:permId | admin | Remove a permission from a role (same ownership checks) |
Admin — Accounting journal
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/accounting/journal | admin | Read-only, newest-first merge of every billing action: wallet top-ups, grants, refunds, adjustments and reversals, unlimited-access changes and subscription plan changes (metered usage is excluded). Query: page, limit (≤ 100, default 25), kind (topup, grant, refund, adjust, reversal, unlimited_enabled, unlimited_disabled, subscription_assigned, subscription_changed, subscription_removed), userId, applicationId (account events carry no application and drop out), from (inclusive) and to (exclusive) ISO dates; page * limit is capped at 5000 (400 APP_001 beyond). Returns { entries, total, page, limit }; an entry carries owner, the actor (null for Stripe and the default-plan rule), source, application, signed micro-euro amount and balanceAfter, fromPlan / toPlan, reason and reference |
Admin — Subscription plans
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/applications/:appId/plans | admin | List plans with prices |
POST | /api/admin/applications/:appId/plans | admin | Create a plan with plain features (string, number or boolean values). Creates its Stripe product when Stripe is configured. Refused with 400: a Stripe id, a metered feature declared as an object, or any unknown field |
PATCH | /api/admin/applications/:appId/plans/:planId | admin | Update a plan |
DELETE | /api/admin/applications/:appId/plans/:planId | admin | Delete a plan (fails if active subscriptions exist) |
POST | /api/admin/applications/:appId/users/:userId/subscription | admin | Assign a plan to a user without one ({ planId, expiresAt?, reason? }); 409 SUB_002 when one is active (change it with the PATCH above). Returns 201 |
DELETE | /api/admin/applications/:appId/users/:userId/subscription | admin | Remove the user's subscription (404 SUB_001 when there is none). Returns 204 |
POST | /api/admin/applications/:appId/plans/:planId/prices | admin | Add a price: billingType fixed (name, amount, currency, interval) or metered (adds featureKey, unit, usageLimit, -1 = unlimited; interval is month). Refused with 400 on a Stripe id or an unknown field |
PATCH | /api/admin/applications/:appId/plans/:planId/prices/:priceId | admin | Rename a price; on a metered price also change unit and usageLimit. Amount, currency, interval and feature key are fixed |
DELETE | /api/admin/applications/:appId/plans/:planId/prices/:priceId | admin | Delete a price tier |
Admin — Consumption
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/applications/:id/consumption | admin | Consumption aggregates for all users of an application |
GET | /api/admin/applications/:appId/consumption/monthly | admin | Monthly aggregates grouped by user and key. Query params year, month. |
Admin — Users
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/users | admin | List users (paginated; ?page=&limit=&search=) |
POST | /api/admin/users | admin | Create a user ({ name, email, password, role? }; role: "admin" is reserved to superadmins) |
GET | /api/admin/users/:id | admin | Get a user with application access and roles |
PATCH | /api/admin/users/:id | admin | Update a user (name, global role, isMfaRequired) |
DELETE | /api/admin/users/:id | admin | Delete a user (cascades) |
POST | /api/admin/users/:id/disable | admin | Ban (disable) a user |
POST | /api/admin/users/:id/enable | admin | Unban (enable) a user |
POST | /api/admin/users/import | admin | Create up to 500 users: { users: [{ name, email, password?, role?, id?, emailVerified?, passwordHash? }], sendVerification? }. role is user or admin; admin, id, emailVerified and passwordHash are reserved to superadmins. id becomes the user id and the token sub. passwordHash is { algorithm: "bcrypt", hash } or { algorithm: "pbkdf2", digest: "sha256"|"sha512", iterations, salt, hash } (base64), replaced by a native hash at the first sign-in; send it instead of password. sendVerification (default true) e-mails unverified rows. Answers per row created or error; a temporary password is returned once for rows with neither password nor hash |
POST | /api/admin/users/:id/send-verification | admin | Send the verification e-mail again |
POST | /api/admin/users/:id/verify-email | admin | Mark the address verified without e-mail (strictly lower-ranked users only; 403 AUTH_011 otherwise) |
POST | /api/admin/users/:id/set-password | admin | Set a new password for a strictly lower-ranked user and revoke their sessions and OAuth tokens ({ newPassword }) |
Admin — Organizations
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/organizations | admin | List organizations (paginated; ?page=&limit=&search=&sortBy=&sortOrder=) |
POST | /api/admin/organizations | admin | Create an organization |
GET | /api/admin/organizations/:id | admin | Get an organization |
PATCH | /api/admin/organizations/:id | admin | Update an organization (name, slug, logo, metadata) |
DELETE | /api/admin/organizations/:id | admin | Delete an organization |
GET | /api/admin/organizations/:id/members | admin | List members (?limit=&offset=&search=); returns { members, total } |
POST | /api/admin/organizations/:id/members | admin | Add a member ({ userId, role }) |
PATCH | /api/admin/organizations/:id/members/:memberId/role | admin | Update a member's role (owner / admin / member) |
DELETE | /api/admin/organizations/:id/members/:userId | admin | Remove a member |
GET | /api/admin/organizations/:id/invitations | admin | List pending invitations |
POST | /api/admin/organizations/:id/invitations | admin | Send an invitation ({ email, role }) |
DELETE | /api/admin/organizations/:id/invitations/:invitationId | admin | Cancel an invitation |
Admin — Sessions
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/sessions | admin | List active sessions (paginated; ?page=&limit=&search=). applications always starts with Auth Service (every session is one of its own), then the applications the user obtained tokens for since; system applications are hidden except the LiteLLM admin interface, shown as LiteLLM |
Admin — Statistics
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/stats/active-users | admin | Count of distinct users with a non-expired session |
GET | /api/admin/stats/logins | admin | Daily login series. Query params range (7d / 30d) and optional appId. |
GET | /api/admin/stats/registrations | admin | Daily account-creation series, platform-wide. Query param range (7d / 30d). Returns { range, series, total } |
GET | /api/admin/stats/applications-activity | admin | Per-application activity (online count, recent logins, 7-day sparkline) |
Admin — Real-time channel
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/events | admin | Server-Sent Events stream. Frames name an event type (login.recorded, wallet.changed, subscription.changed…) and carry at most a timestamp — never a payload. The client re-fetches through the REST endpoints. |
Admin — Services
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/admin/services | admin | Which optional integrations (Stripe, social providers) are configured, and the active compatibility profile (compat) |
Stripe webhooks
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/webhooks/stripe | Stripe signature | Stripe event receiver (subscription and invoice lifecycle events) |
The receiver parses the raw body and verifies Stripe-Signature before any state change. Responses:
| Status | When |
|---|---|
200 | Event applied, or a duplicate delivery (same event.id) acknowledged without re-applying state |
400 APP_001 | Missing or invalid signature. The message is generic; the event is not claimed, so a valid retry is still processed |
503 SRV_002 | STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET is not configured |
500 SRV_001 | Handler failure. The idempotency claim is released so Stripe's retry is processed |
Invoice links are cached only when they are https:// URLs. The webhook carries no session and must stay reachable without authentication; do not put a session guard in front of it.