Sign-in API
The app's own end-user accounts: sign-up, password and magic-link sign-in, social sign-in, two-factor codes, sessions, admin user management and organisations.
On this page
- Authentication
- Endpoints
- POST /auth/signup
- POST /auth/login
- GET /auth/me
- GET /auth/signup-options
- POST /auth/logout
- POST /auth/verify/request
- GET /auth/verify/confirm
- POST /auth/reset/request
- POST /auth/reset/confirm
- POST /auth/magic/request
- GET /auth/magic/consume
- GET /auth/users
- GET /auth/roles
- POST /auth/users
- PATCH /auth/users/:id
- GET /auth/set-password
- POST /auth/set-password
- GET /auth/oauth/providers
- GET /auth/oauth/:provider/start
- GET /auth/oauth/:provider/callback
- GET /auth/oauth/complete
- POST /auth/oauth/config
- DELETE /auth/oauth/config/:provider
- POST /auth/mfa/enroll/start
- POST /auth/mfa/enroll/confirm
- POST /auth/mfa/verify
- POST /auth/mfa/disable
- POST /auth/mfa/backup-codes/regenerate
- POST /orgs/invite
- GET /orgs/invite/accept
- GET /orgs/members
- PATCH /orgs/members/:userId
- DELETE /orgs/members/:userId
- GET /orgs/current
Base URL: your app's origin ($APP_ORIGIN): the published host, a custom domain, or the platform origin for the builder preview. Errors answer { ok: false, error: { code, message } } unless noted. Add x-appbricx-env: dev or prod to pick the database explicitly; otherwise the host decides (preview means dev, a published host means prod).
Authentication
- App key
- A project key or preview token identifies the app. No user session is needed.
Authorization: Bearer $APP_KEY - App key + admin session
- A project key and the session of an app user with the admin flag. Anyone else gets 403.
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION - App key + signed-in user
- A project key or preview token and a signed-in app session (header or cookie). Signed-out callers get 401.
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Endpoints
Sign up
POST/__appbricx/auth/signup
Create an end-user account and sign in. Sets the appbricx_app_session cookie and returns the session token.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
passwordrequired | string | At least 8 characters. |
nameoptional | string | Display name (200 chars max). |
personaoptional | string | A self-signup persona (see signup-options). |
Response
When verification is required: { ok: true, pending: true, message }.
{
"ok": true,
"token": "eyJhbGciOi…",
"user": {
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"isAdmin": false,
"role": "customer",
"orgId": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | INVALID_EMAIL | Malformed address. |
| 400 | WEAK_PASSWORD | Under 8 characters. |
| 403 | SIGNUP_DISABLED | Sign-ups are closed. |
| 403 | SIGNUP_INVITE_ONLY | Invite-only app. |
| 429 | (rate limit) | Too many sign-ups from this address. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/signup" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery","name":"Ada"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/signup`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com",
"password": "correct horse battery",
"name": "Ada"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:309
Sign in
POST/__appbricx/auth/login
Password sign-in. Sets the session cookie and returns the token. Accounts with two-factor sign-in get a pending token to complete with /auth/mfa/verify.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
passwordrequired | string | The password. |
Response
Two-factor: { ok: true, needsMfa: true, pending, user }.
{
"ok": true,
"token": "eyJhbGciOi…",
"user": {
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"role": "customer",
"isAdmin": false,
"orgId": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | INVALID_CREDENTIALS | Email or password missing. |
| 401 | INVALID_CREDENTIALS | "Incorrect email or password." |
| 403 | ACCOUNT_DISABLED | The account is disabled. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/login" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"email":"ada@example.com","password":"correct horse battery"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/login`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com",
"password": "correct horse battery"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:538
Current user
GET/__appbricx/auth/me
The signed-in user from the session cookie or x-appbricx-app-session, or null.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Response
{
"ok": true,
"user": {
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"role": "customer",
"isAdmin": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
curl "$APP_ORIGIN/__appbricx/auth/me" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/me`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:602
Sign-up options
GET/__appbricx/auth/signup-options
The signup policy and the personas a visitor may pick at sign-up.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Response
{
"ok": true,
"signupPolicy": "open",
"personas": [
"customer"
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
curl "$APP_ORIGIN/__appbricx/auth/signup-options" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/signup-options`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:619
Sign out
POST/__appbricx/auth/logout
Clear the session cookie.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
curl -X POST "$APP_ORIGIN/__appbricx/auth/logout" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/logout`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:630
Request email verification
POST/__appbricx/auth/verify/request
Email a verification link. Constant response.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | INVALID_EMAIL | Malformed address. |
| 429 | (rate limit) | Too many mail requests. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/verify/request" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"email":"ada@example.com"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/verify/request`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:736
Confirm email
GET/__appbricx/auth/verify/confirm
The emailed verification link. Redirects to the app with ?verified=1 when the app's origin is known, else answers JSON.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Query parameters
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the email. |
Response
{
"ok": true,
"verified": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_TOKEN | No token. |
| 400 | INVALID_TOKEN | Expired or forged. |
| 400 | TOKEN_USED | Already used. |
curl "$APP_ORIGIN/__appbricx/auth/verify/confirm?token=eyJ%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/verify/confirm?token=eyJ%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:757
Request password reset
POST/__appbricx/auth/reset/request
Email a reset link. Constant response whether or not the account exists.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | INVALID_EMAIL | Malformed address. |
| 429 | (rate limit) | Too many mail requests. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/reset/request" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"email":"ada@example.com"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/reset/request`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:781
Set a new password
POST/__appbricx/auth/reset/confirm
Complete a reset with the emailed token.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the email. |
newPasswordrequired | string | At least 8 characters. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_TOKEN | No token. |
| 400 | WEAK_PASSWORD | Under 8 characters. |
| 400 | INVALID_TOKEN | Expired or forged. |
| 400 | TOKEN_USED | Already used. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/reset/confirm" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"token":"eyJ…","newPassword":"a much longer passphrase"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/reset/confirm`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"token": "eyJ…",
"newPassword": "a much longer passphrase"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:798
Request a magic link
POST/__appbricx/auth/magic/request
Email a one-time sign-in link. Constant response.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 400 | INVALID_EMAIL | Malformed address. |
| 429 | (rate limit) | Too many mail requests. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/magic/request" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"email":"ada@example.com"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/magic/request`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "ada@example.com"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:822
Use a magic link
GET/__appbricx/auth/magic/consume
The emailed link: signs the user in (sets the cookie) and redirects to the app with ?magic=1, else answers JSON with the session.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Query parameters
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the email. |
Response
{
"ok": true,
"token": "eyJhbGciOi…",
"user": {
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"isAdmin": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_TOKEN | No token. |
| 400 | INVALID_TOKEN | Expired or forged. |
| 400 | TOKEN_USED | Already used. |
| 400 | USER_NOT_FOUND | The account is gone or disabled. |
curl "$APP_ORIGIN/__appbricx/auth/magic/consume?token=eyJ%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/magic/consume?token=eyJ%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:839
List users (admin)
GET/__appbricx/auth/users
The app's accounts with their roles. Backs the TeamAdmin block.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Query parameters
| Name | Type | Description |
|---|---|---|
limitoptional | number | Page size. |
offsetoptional | number | Rows to skip. |
Response
{
"ok": true,
"users": [
{
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"isAdmin": false,
"disabled": false,
"role": "customer"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 403 | none | "Admin required". |
curl "$APP_ORIGIN/__appbricx/auth/users?limit=200" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/users?limit=200`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:881
Assignable roles (admin)
GET/__appbricx/auth/roles
The personas an admin may hand out.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Response
{
"ok": true,
"roles": [
{
"id": "staff",
"label": "Staff",
"kind": "staff"
},
{
"id": "customer",
"label": "Customer",
"kind": "consumer"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 403 | none | "Admin required". |
curl "$APP_ORIGIN/__appbricx/auth/roles" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/roles`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:980
Invite a user (admin)
POST/__appbricx/auth/users
Create an account with a role.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | The address. |
passwordrequired | string | At least 8 characters. |
nameoptional | string | Display name. |
roleoptional | string | One of GET /auth/roles. |
isAdminoptional | boolean | Grant the admin flag. |
Response
201 Created. warning is set when the account exists but its role couldn't be recorded.
{
"ok": true,
"user": {
"id": "u_43",
"email": "sam@example.com",
"name": "Sam",
"role": "staff"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 403 | none | "Admin required". |
| 400 | INVALID_EMAIL | Malformed address. |
| 400 | WEAK_PASSWORD | Under 8 characters. |
| 400 | INVALID_ROLE | Not an assignable role. |
| 409 | EMAIL_TAKEN | An account exists. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/users" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION" \
-H "content-type: application/json" \
-d '{"email":"sam@example.com","password":"temporary-pass-123","name":"Sam","role":"staff"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/users`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "sam@example.com",
"password": "temporary-pass-123",
"name": "Sam",
"role": "staff"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:991
Change a user (admin)
PATCH/__appbricx/auth/users/:id
Change name, email, role, admin flag or disabled state.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Path parameters
| Name | Type | Description |
|---|---|---|
:idrequired | string | The user id. |
Body
| Name | Type | Description |
|---|---|---|
nameoptional | string | Display name. |
emailoptional | string | Address. |
roleoptional | string | A persona id. |
isAdminoptional | boolean | Admin flag. |
disabledoptional | boolean | Disable the account. |
Response
{
"ok": true,
"user": {
"id": "u_43",
"email": "sam@example.com",
"name": "Sam",
"role": "manager",
"disabled": false
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 403 | none | "Admin required". |
| 400 | SELF_LOCKOUT | You can't disable or demote yourself. |
| 400 | INVALID_ROLE | Not an assignable role. |
| 404 | NOT_FOUND | No such user. |
curl -X PATCH "$APP_ORIGIN/__appbricx/auth/users/u_43" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION" \
-H "content-type: application/json" \
-d '{"role":"manager"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/users/u_43`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"role": "manager"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-auth.ts:1046
Set-password page
GET/__appbricx/auth/set-password
An HTML form an invited user opens from their email to choose a password (links work once, for 1 hour). Reset links that land on the app as /?reset=<token> are forwarded here.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Query parameters
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the email. |
Response
text/html.
<!doctype html> … Set your password …Errors
| Status | Code | When |
|---|---|---|
| 400 | (html) | "This link has expired". |
curl "$APP_ORIGIN/__appbricx/auth/set-password?token=eyJ%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/set-password?token=eyJ%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const text = await res.text();Source: services/api/src/routes/app-auth.ts:1185
Submit set-password form
POST/__appbricx/auth/set-password
The form post behind the set-password page (application/x-www-form-urlencoded: token, password, confirm).
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the link. |
passwordrequired | string | At least 8 characters. |
confirmrequired | string | Same again. |
Response
text/html.
<!doctype html> … Password saved …Errors
| Status | Code | When |
|---|---|---|
| 400 | (html) | Expired link, password under 8 characters, passwords that don't match, or a used link. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/set-password" \
-H "Authorization: Bearer $APP_KEY" \
--data "token=eyJ…&password=a+much+longer+passphrase&confirm=a+much+longer+passphrase"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/set-password`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
body: new URLSearchParams("token=eyJ…&password=a+much+longer+passphrase&confirm=a+much+longer+passphrase"),
});
const text = await res.text();Source: services/api/src/routes/app-auth.ts:1195
Social providers
GET/__appbricx/auth/oauth/providers
The social sign-in providers turned on for this app.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Response
{
"ok": true,
"providers": [
{
"name": "google",
"label": "Google"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
curl "$APP_ORIGIN/__appbricx/auth/oauth/providers" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/providers`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-oauth.ts:165
Start social sign-in
GET/__appbricx/auth/oauth/:provider/start
Redirect to the provider (302), or return the URL with json=1. The user returns to returnTo signed in, with ?oauth=1 (or ?oauth_error=<code>).
- Auth
- App key:
Authorization: Bearer $APP_KEY
Path parameters
| Name | Type | Description |
|---|---|---|
:providerrequired | string | "google" or "github". |
Query parameters
| Name | Type | Description |
|---|---|---|
returnTooptional | string | Where to land afterwards: a full URL on one of the app's hosts, or a path. |
jsonoptional | "1" | Answer { ok, url } instead of redirecting. |
Response
{
"ok": true,
"url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=…"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid Authorization bearer ("Missing Authorization: Bearer <token>", "Invalid API key"). |
| 404 | UNKNOWN_PROVIDER | Not a supported provider. |
| 400 | PROVIDER_NOT_CONFIGURED | Not turned on for this app. |
| 400 | ORIGIN_NOT_ALLOWED | returnTo isn't one of the app's hosts. |
curl "$APP_ORIGIN/__appbricx/auth/oauth/google/start?json=1&returnTo=https%3A%2F%2Ftablefor.apps.appbricx.com%2F" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/google/start?json=1&returnTo=https%3A%2F%2Ftablefor.apps.appbricx.com%2F`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-oauth.ts:180
Social sign-in callback
GET/__appbricx/auth/oauth/:provider/callback
The redirect URI to register with your own OAuth client (https://<app host>/__appbricx/auth/oauth/<provider>/callback). Exchanges the code, signs the user in and redirects back.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Path parameters
| Name | Type | Description |
|---|---|---|
:providerrequired | string | "google" or "github". |
Query parameters
| Name | Type | Description |
|---|---|---|
coderequired | string | From the provider. |
staterequired | string | The signed state. |
Response
302 Found → <returnTo>?oauth=1Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_STATE | Expired or forged state. |
| 400 | PROVIDER_MISMATCH | State is for another provider. |
| 400 | MISSING_CODE | No code. |
curl "$APP_ORIGIN/__appbricx/auth/oauth/github/callback?code=abc&state=eyJ%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/github/callback?code=abc&state=eyJ%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const text = await res.text();Source: services/api/src/routes/app-oauth.ts:231
Complete shared sign-in
GET/__appbricx/auth/oauth/complete
Last hop of Appbricx's shared Google sign-in: redeems a one-time code on the app's own host, sets the session cookie and redirects with ?oauth=1.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Query parameters
| Name | Type | Description |
|---|---|---|
coderequired | string | The one-time handoff code. |
Response
302 Found → <returnUrl>?oauth=1Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_OR_EXPIRED_CODE | Used, expired, or redeemed on another host. |
curl "$APP_ORIGIN/__appbricx/auth/oauth/complete?code=h_9f2%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/complete?code=h_9f2%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
},
});
const text = await res.text();Source: services/api/src/routes/app-oauth.ts:459
Configure a provider (admin)
POST/__appbricx/auth/oauth/config
Store your own OAuth client for a provider.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Body
| Name | Type | Description |
|---|---|---|
providerrequired | string | "google" or "github". |
client_idrequired | string | Your client id. |
client_secretrequired | string | Your client secret. |
enabledoptional | boolean | Default true. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | AUTH_REQUIRED | No session. |
| 401 | INVALID_SESSION | Bad session. |
| 403 | ADMIN_REQUIRED | Not an admin. |
| 400 | UNKNOWN_PROVIDER | Unsupported provider. |
| 400 | MISSING_CREDENTIALS | client_id or client_secret missing. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/oauth/config" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION" \
-H "content-type: application/json" \
-d '{"provider":"github","client_id":"Iv1.abc","client_secret":"…","enabled":true}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/config`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"provider": "github",
"client_id": "Iv1.abc",
"client_secret": "…",
"enabled": true
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-oauth.ts:472
Remove a provider config (admin)
DELETE/__appbricx/auth/oauth/config/:provider
Delete your own OAuth client for a provider.
- Auth
- App key + admin session:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $ADMIN_SESSION
Path parameters
| Name | Type | Description |
|---|---|---|
:providerrequired | string | "google" or "github". |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 401 | AUTH_REQUIRED | No session. |
| 403 | ADMIN_REQUIRED | Not an admin. |
curl -X DELETE "$APP_ORIGIN/__appbricx/auth/oauth/config/github" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $ADMIN_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/oauth/config/github`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": ADMIN_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-oauth.ts:511
Start two-factor enrolment
POST/__appbricx/auth/mfa/enroll/start
Create a TOTP secret for the signed-in user. Show otpauth_url as a QR code.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Response
{
"ok": true,
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/Tablefor:ada%40example.com?secret=JBSW…"
}Errors
| Status | Code | When |
|---|---|---|
| 401 | none | Not signed in. |
| 400 | ALREADY_ENROLLED | Two-factor is already on. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/mfa/enroll/start" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/mfa/enroll/start`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-mfa.ts:211
Confirm two-factor enrolment
POST/__appbricx/auth/mfa/enroll/confirm
Prove the authenticator works with a 6-digit code. Returns one-time backup codes (shown once).
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Body
| Name | Type | Description |
|---|---|---|
coderequired | string | The current 6-digit code. |
Response
{
"ok": true,
"backupCodes": [
"8f3k-2m9d",
"…"
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | NO_PENDING_ENROLLMENT | Call enroll/start first. |
| 400 | INVALID_CODE | Wrong code. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/mfa/enroll/confirm" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"code":"123456"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/mfa/enroll/confirm`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"code": "123456"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-mfa.ts:236
Complete a two-factor sign-in
POST/__appbricx/auth/mfa/verify
Exchange the pending token from /auth/login plus a 6-digit or backup code for a session.
- Auth
- App key:
Authorization: Bearer $APP_KEY
Body
| Name | Type | Description |
|---|---|---|
pendingrequired | string | From the login answer. |
coderequired | string | TOTP or backup code. |
Response
{
"ok": true,
"token": "eyJhbGciOi…",
"user": {
"id": "u_42",
"email": "ada@example.com",
"name": "Ada",
"isAdmin": false
},
"verifiedVia": "totp"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_INPUT | pending or code missing. |
| 400 | INVALID_PENDING | Expired pending token. |
| 400 | INVALID_CODE | Wrong code. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/mfa/verify" \
-H "Authorization: Bearer $APP_KEY" \
-H "content-type: application/json" \
-d '{"pending":"eyJ…","code":"123456"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/mfa/verify`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
"pending": "eyJ…",
"code": "123456"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-mfa.ts:269
Turn off two-factor
POST/__appbricx/auth/mfa/disable
Disable two-factor for the signed-in user (needs a current code).
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Body
| Name | Type | Description |
|---|---|---|
coderequired | string | TOTP or backup code. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_CODE | Wrong code. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/mfa/disable" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"code":"123456"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/mfa/disable`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"code": "123456"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-mfa.ts:325
New backup codes
POST/__appbricx/auth/mfa/backup-codes/regenerate
Replace the backup codes (needs a current code).
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Body
| Name | Type | Description |
|---|---|---|
coderequired | string | TOTP or backup code. |
Response
{
"ok": true,
"backupCodes": [
"2m9d-8f3k",
"…"
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | INVALID_CODE | Wrong code. |
curl -X POST "$APP_ORIGIN/__appbricx/auth/mfa/backup-codes/regenerate" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"code":"123456"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/auth/mfa/backup-codes/regenerate`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"code": "123456"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-mfa.ts:343
Invite to the organisation
POST/__appbricx/orgs/invite
Email an invitation to join the caller's active organisation.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Body
| Name | Type | Description |
|---|---|---|
emailrequired | string | Invitee address. |
roleoptional | "owner" | "admin" | "member" | Default member. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | NO_ORG | The session has no active organisation. |
| 403 | ADMIN_REQUIRED | Only owners and admins invite. |
| 400 | INVALID_EMAIL | Malformed address. |
| 400 | INVALID_ROLE | Unknown role. |
curl -X POST "$APP_ORIGIN/__appbricx/orgs/invite" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"email":"sam@example.com","role":"member"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/invite`, {
method: "POST",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"email": "sam@example.com",
"role": "member"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-invites.ts:126
Accept an invitation
GET/__appbricx/orgs/invite/accept
The emailed link: joins the organisation and redirects to the app with ?joined=1.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Query parameters
| Name | Type | Description |
|---|---|---|
tokenrequired | string | From the email. |
Response
302 Found → /?joined=1Errors
| Status | Code | When |
|---|---|---|
| 400 | MISSING_TOKEN | No token. |
| 400 | INVALID_TOKEN | Unknown token. |
| 400 | TOKEN_USED | Already accepted. |
| 400 | TOKEN_EXPIRED | Expired. |
| 403 | EMAIL_MISMATCH | Signed in as another address. |
curl "$APP_ORIGIN/__appbricx/orgs/invite/accept?token=inv_%E2%80%A6" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/invite/accept?token=inv_%E2%80%A6`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
},
});
const text = await res.text();Source: services/api/src/routes/app-invites.ts:185
Organisation members
GET/__appbricx/orgs/members
Members of the caller's active organisation.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Response
{
"ok": true,
"members": [
{
"userId": "u_42",
"email": "ada@example.com",
"role": "owner"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 403 | NOT_A_MEMBER | Not a member of the active organisation. |
curl "$APP_ORIGIN/__appbricx/orgs/members" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/members`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-invites.ts:244
Change a member's role
PATCH/__appbricx/orgs/members/:userId
Set a member's organisation role.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Path parameters
| Name | Type | Description |
|---|---|---|
:userIdrequired | string | The member's user id. |
Body
| Name | Type | Description |
|---|---|---|
rolerequired | "owner" | "admin" | "member" | The new role. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | NO_ORG | No active organisation. |
| 403 | ADMIN_REQUIRED | Owners and admins only. |
| 400 | INVALID_ROLE | Unknown role. |
curl -X PATCH "$APP_ORIGIN/__appbricx/orgs/members/u_43" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION" \
-H "content-type: application/json" \
-d '{"role":"admin"}'const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/members/u_43`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
"content-type": "application/json",
},
body: JSON.stringify({
"role": "admin"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-invites.ts:274
Remove a member
DELETE/__appbricx/orgs/members/:userId
Remove a member from the organisation.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Path parameters
| Name | Type | Description |
|---|---|---|
:userIdrequired | string | The member's user id. |
Response
{
"ok": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | NO_ORG | No active organisation. |
| 403 | ADMIN_REQUIRED | Owners and admins only. |
| 400 | LAST_OWNER | Can't remove the last owner. |
curl -X DELETE "$APP_ORIGIN/__appbricx/orgs/members/u_43" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/members/u_43`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-invites.ts:299
Current organisation
GET/__appbricx/orgs/current
The caller's active organisation and their role in it, or null.
- Auth
- App key + signed-in user:
Authorization: Bearer $APP_KEYx-appbricx-app-session: $APP_SESSION
Response
{
"ok": true,
"org": {
"id": "org_7",
"name": "Acme",
"createdAt": "2026-09-01T10:00:00Z",
"role": "owner"
}
}Errors
| Status | Code | When |
|---|---|---|
| 401 | none | Not signed in. |
curl "$APP_ORIGIN/__appbricx/orgs/current" \
-H "Authorization: Bearer $APP_KEY" \
-H "x-appbricx-app-session: $APP_SESSION"const res = await fetch(`${APP_ORIGIN}/__appbricx/orgs/current`, {
headers: {
Authorization: `Bearer ${APP_KEY}`,
"x-appbricx-app-session": APP_SESSION,
},
});
const data = await res.json();
if (!res.ok) throw new Error(data.error?.message ?? data.error ?? res.statusText);Source: services/api/src/routes/app-invites.ts:333