Authentication API
Every endpoint under /api/v1/auth/<collection>/... and the admin auth flow.
All endpoints below operate on a collection of type: "auth". Calls against
non-auth collections return 422.
For the conceptual overview see Authentication.
Admin auth
Section titled “Admin auth”POST /api/v1/admin/setupX-Setup-Key: <setup key>{ "email": "...", "password": "..." }One-time setup — creates the first admin. After that, returns 400.
The setup key is VAULTBASE_SETUP_KEY, or the key the server prints to its
log at first boot. Send it as the X-Setup-Key header or as setup_key in
the body; without it the call returns 401. The password must meet the
password policy.
POST /api/v1/admin/auth/login{ "email": "...", "password": "..." }→ { "data": { "token": "<jwt>", "admin": { "id": "...", "email": "..." } } }
GET /api/v1/admin/auth/me→ { "data": <jwt payload> }User register / login
Section titled “User register / login”POST /api/v1/auth/<col>/register{ "email": "...", "password": "...", ...the collection's own fields (unknown keys are ignored) } → 403 when the collection's create_rule doesn't allow the sign-up
POST /api/v1/auth/<col>/login{ "email": "...", "password": "..." }/login returns one of:
{ "data": { "token": "<jwt>", "record": { "id": "...", "email": "..." } } }
{ "data": { "mfa_required": true, "mfa_token": "..." } }POST /api/v1/auth/<col>/login/mfa{ "mfa_token": "...", "code": "<6 digits>" }
# OR — single-use recovery code instead of TOTP codePOST /api/v1/auth/<col>/login/mfa{ "mfa_token": "...", "recovery_code": "ABCD-EF23" }GET /api/v1/auth/me ← user JWTPOST /api/v1/auth/refresh ← user OR admin JWT, re-issues 7d tokenA refreshed token keeps the original’s collection and anonymous status.
Impersonation tokens can’t be refreshed (403): impersonation sessions end
when their token expires.
Refreshing revokes the token you sent: keep the new one and drop the old
one (the SDK does this for you). Refreshing the same token again within 30
seconds (another tab, a retried request) returns the same new token; after
that it returns 401.
A sign-in can be kept alive by refreshing for at most
auth.session.max_seconds (default 30 days, 0 = no cap), counted from the
original sign-in, which refreshed tokens carry as the auth_time claim. The
new token never outlives that cap; past it, /refresh returns 401 and the
user or admin signs in again. Anonymous sessions are exempt — a guest has no
credential to sign in with again.
Email verification
Section titled “Email verification”POST /api/v1/auth/<col>/request-verify ← auth required, no bodyPOST /api/v1/auth/<col>/verify-email { "token": "..." }Verification emails are sent via the verify template (Settings → Email
templates). The token is the long random hex from the email link.
Password reset
Section titled “Password reset”POST /api/v1/auth/<col>/request-password-reset { "email": "..." } → always 200 (no enumeration), even if email isn't registeredPOST /api/v1/auth/<col>/confirm-password-reset { "token": "...", "password": "..." }password must meet the password policy
(12 characters minimum by default). reset template, 1-hour token TTL.
A successful reset signs out every existing session of that user.
Email change
Section titled “Email change”POST /api/v1/auth/<col>/request-email-change { "newEmail": "..." } ← user token requiredPOST /api/v1/auth/<col>/confirm-email-change { "token": "...", "password": "..." }The request emails a link to the new address (email_change template,
1-hour TTL). Confirming needs that token and the user’s current password,
so a stolen session alone can’t move an account to someone else’s inbox.
A newer request voids older links. On success the new email is marked
verified and every existing session ends, so the user signs in again.
Requests are limited to one a minute per user (429). If the new address
already belongs to another account, the request still
answers { "sent": true } but no link is sent, so the endpoint can’t be used
to find out who has an account. Wrong passwords on confirm count towards the
sign-in lockout.
Users who signed up with OAuth2 have no password they know. They set one first with the password reset flow. Anonymous accounts use promote instead.
Invitations
Section titled “Invitations”POST /api/v1/admin/users/<col>/invite { "email": "...", "data": { ... } } ← admin tokenPOST /api/v1/auth/<col>/accept-invite { "token": "...", "password": "..." }An admin creates the user (custom fields in data; the beforeCreate /
afterCreate hooks run) without a usable password, and an invite email
goes out with a 7-day link. Accepting sets the password and verifies the
email. Until then the user can’t sign in. Records → Invite user does the
same from the admin. 409 if the email is taken; 502 (with the new user’s
id) if the user was created but the email failed to send.
Hosted pages
Section titled “Hosted pages”If you don’t build these screens yourself, the email links can point at
pages vaultbase serves: /auth/verify, /auth/reset, /auth/otp,
/auth/confirm-email-change and /auth/invite (each takes token and
collection query params). Set App URL (Settings → Email templates) to
the vaultbase origin to use them.
OTP / magic link
Section titled “OTP / magic link”Gated by Settings → Auth features → OTP / magic link.
POST /api/v1/auth/<col>/otp/request { "email": "..." } ← always 200POST /api/v1/auth/<col>/otp/auth { "token": "..." }POST /api/v1/auth/<col>/otp/auth { "email": "...", "code": "<6 digits>" }Either token (from the link) or code (from the email) authenticates.
10-minute expiry. SMTP must be configured.
Gated by Settings → Auth features → MFA / TOTP.
POST /api/v1/auth/<col>/totp/setup ← user JWT → { "data": { "secret": "<base32>", "otpauth_url": "otpauth://..." } }
POST /api/v1/auth/<col>/totp/confirm { "code": "<6 digits>" } → enables MFA on this user; returns { enabled, recovery_codes }
# /totp/setup answers 409 while MFA is already enabled — disable it with a# current code first.
POST /api/v1/auth/<col>/totp/disable { "code": "<6 digits>" } → clears MFA on this userdisable is intentionally not gated by the feature flag — once a user has
MFA enabled, disabling it shouldn’t break when an admin turns the feature off
globally. Same for /login/mfa. /disable also wipes any stored recovery
codes for the user.
Recovery codes
Section titled “Recovery codes”POST /api/v1/auth/<col>/totp/recovery/regenerate ← user JWT{ "code": "<current 6-digit TOTP code>" } → { "data": { "codes": ["ABCD-EF23", "GH45-JKLM", ...] } } // 10 codes, plaintext, ONCE
GET /api/v1/auth/<col>/totp/recovery/status ← user JWT → { "data": { "total": 10, "remaining": 7 } }10 single-use 8-character codes per user, formatted XXXX-XXXX (uppercase
letters and digits, no 0/O/1/I), hashed at rest. Plaintext is
returned once at generation time — there’s no way to retrieve them
again. The first batch comes back from /totp/confirm as recovery_codes
when MFA is switched on. Regenerating needs a current authenticator code (a
session alone isn’t enough) and invalidates the previous batch.
OAuth2
Section titled “OAuth2”GET /api/v1/auth/<col>/oauth2/providers → { "data": [ { "name": "google", "displayName": "Google" }, ... ] }
GET /api/v1/auth/<col>/oauth2/authorize ?provider=google&redirectUri=https://app.example.com/callback&state=<csrf> → { "data": { "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?...", "state": "<csrf>", "state_secret": "...", "pkce": "none" } }
POST /api/v1/auth/<col>/oauth2/exchange{ "provider": "google", "code": "...", "redirectUri": "...", "state": "<state from /authorize>", "state_secret": "<state_secret from /authorize>" } → { "data": { "token": "<jwt>", "record": { "id": "...", "email": "..." } } }state and state_secret are required on /exchange (400 otherwise).
Configure providers at Settings → OAuth2 (16 supported out of the box).
See OAuth2 API for the full flow.
Anonymous
Section titled “Anonymous”POST /api/v1/auth/<col>/anonymous → { "data": { "token": "<jwt>", "record": { "id": "...", "email": "anon_xxx@anonymous.invalid", "anonymous": true } } }30-day JWT carrying anonymous: true claim. Needs anonymous sign-in turned on, and a
create_rule that allows the sign-up (403 otherwise).
Promote an anonymous account
Section titled “Promote an anonymous account”POST /api/v1/auth/<col>/promote ← anonymous user JWT{ "email": "alice@x.com", "password": "correct-horse-battery" } → { "data": { "token": "<jwt>", "record": { "id": "...", "email": "alice@x.com" } } }Upgrades an existing anonymous record in place — keeps the user id, sets
the email + password, clears is_anonymous, and re-issues a fresh
non-anonymous JWT. Returns 422 if the caller’s token isn’t anonymous (or
the body fails validation or the password policy), 403 if the collection’s
create_rule rejects the body, 409 if the email is already taken in
this collection.
Admin impersonation
Section titled “Admin impersonation”POST /api/v1/admin/impersonate/<col>/<userId> ← admin auth required → { "data": { "token": "<jwt>", "record": { ... }, "impersonated_by": "<admin_id>" } }1-hour user JWT carrying impersonated_by: <admin_id> for audit.
Admin user management
Section titled “Admin user management”GET /api/v1/<col>?page=1&perPage=30 # list (records flow)GET /api/v1/<col>/<id> # onePATCH /api/v1/<col>/<id> # update non-credential fieldsDELETE /api/v1/<col>/<id>v0.11 promoted auth collections to first-class — admins manage users
through the same records endpoints as any other collection. The legacy
/api/v1/admin/users/<col> endpoint was removed.
Auth-system columns (password_hash, totp_secret, password_reset_at)
never appear in API responses, and incoming PATCH bodies that try to set
them are silently stripped — credential changes go through the dedicated
auth flows (/totp/disable, /confirm-password-reset, …). Creating a
user via POST /api/v1/<auth-col> is rejected; use
POST /api/v1/auth/<col>/register instead so passwords get properly
hashed and verification email gets sent.
Token shape
Section titled “Token shape”User JWT (audience "user"):
{ "iat": 1730000000, "exp": 1730604800, "aud": "user", "id": "<user_id>", "email": "...", "collection": "<col_name>", "anonymous": true, // optional "impersonated_by": "<admin_id>" // optional}Admin JWT (audience "admin"):
{ "iat": 1730000000, "exp": 1730604800, "aud": "admin", "id": "<admin_id>", "email": "..."}