Auth Endpoints
Base path: /api/v1/auth
These endpoints do not require an Authorization header — they are used to obtain or refresh tokens.
All requests must still include Cloudflare Access service token headers. See Authentication for details.
Authentication Flow Overview
POST /api/v1/auth/register
Creates a new account and an owner user. Seeds an empty credit ledger for the account. Sends a verification email — the user is not issued tokens until they verify.
Request Body
{
"accountName": "Acme Corp",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"password": "min8chars"
}| Field | Type | Required | Notes |
|---|---|---|---|
accountName | string | Yes | |
firstName | string | Yes | |
lastName | string | Yes | |
email | string | Yes | Must be unique across all users |
password | string | Yes | Minimum 8 characters |
Response — 201 Created
{
"message": "Account created. Please check your email to verify your account.",
"user": {
"id": "<uuid>",
"accountId": "<uuid>",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"role": "owner",
"isVerified": false,
"isActive": true,
"lastLoginAt": null,
"createdAt": "2025-01-01T00:00:00.000Z"
},
"account": {
"id": "<uuid>",
"name": "Acme Corp"
}
}Errors
| Code | Cause |
|---|---|
409 Conflict | Email already registered |
422 Unprocessable | Validation failure |
POST /api/v1/auth/verify-email
Redeems the single-use token from the verification email. Marks the user as is_verified = true and issues a full token pair so the user is immediately logged in after verifying.
Request Body
{
"token": "<raw_token_from_email_link>"
}Response — 200 OK
{
"accessToken": "<jwt>",
"refreshToken": "<opaque_token>",
"user": { ... }
}Errors
| Code | Cause |
|---|---|
400 Bad Request | Token expired, already used, or not found |
POST /api/v1/auth/resend-verification
Re-sends the verification email. Always returns success regardless of whether the email exists (prevents user enumeration).
Request Body
{
"email": "jane@acme.com"
}Response — 200 OK
{
"message": "Verification email sent if account exists."
}POST /api/v1/auth/login
Verifies credentials and issues a new token pair. Requires the user to have verified their email. Updates last_login_at on the user record.
Request Body
{
"email": "jane@acme.com",
"password": "min8chars"
}Response — 200 OK
{
"accessToken": "<jwt>",
"refreshToken": "<opaque_token>",
"user": {
"id": "<uuid>",
"accountId": "<uuid>",
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acme.com",
"role": "owner",
"isVerified": true,
"isActive": true,
"lastLoginAt": "2025-01-01T00:00:00.000Z",
"createdAt": "2025-01-01T00:00:00.000Z"
}
}Errors
| Code | Cause |
|---|---|
401 Unauthorized | Email not found, wrong password, unverified email, or deactivated account |
POST /api/v1/auth/refresh
Rotates the refresh token. The supplied token is immediately revoked and a new token pair is issued. Each refresh token can only be used once.
Request Body
{
"refreshToken": "<opaque_token>"
}Response — 200 OK
{
"accessToken": "<new_jwt>",
"refreshToken": "<new_opaque_token>"
}Errors
| Code | Cause |
|---|---|
401 Unauthorized | Token not found, already revoked, or expired |
POST /api/v1/auth/logout
Revokes the supplied refresh token. The token cannot be used again after this call.
Request Body
{
"refreshToken": "<opaque_token>"
}Response — 204 No Content
POST /api/v1/auth/forgot-password
Sends a password reset email. Always returns success (prevents user enumeration). The token is valid for 1 hour.
Request Body
{
"email": "jane@acme.com"
}Response — 200 OK
{
"message": "If that email is registered you will receive a reset link shortly."
}POST /api/v1/auth/reset-password
Redeems the password reset token and sets a new password. The token is marked used_at and cannot be replayed.
Request Body
{
"token": "<raw_token_from_email_link>",
"newPassword": "newSecurePassword1"
}| Field | Type | Required | Notes |
|---|---|---|---|
token | string | Yes | From the reset email link |
newPassword | string | Yes | Minimum 8 characters |
Response — 200 OK
{
"message": "Password updated. You can now log in."
}Errors
| Code | Cause |
|---|---|
400 Bad Request | Token expired, already used, or newPassword too short |
POST /api/v1/auth/magic-link
Sends a passwordless login link to the supplied email. Valid for 15 minutes.
Request Body
{
"email": "jane@acme.com"
}Response — 200 OK
{
"message": "Magic link sent if that email is registered."
}POST /api/v1/auth/magic-link/verify
Redeems the magic link token and issues a token pair — the user is logged in immediately.
Request Body
{
"token": "<raw_token_from_email_link>"
}Response — 200 OK
{
"accessToken": "<jwt>",
"refreshToken": "<opaque_token>",
"user": { ... }
}Errors
| Code | Cause |
|---|---|
400 Bad Request | Token expired, already used, or not found |
OAuth Flows
All OAuth flows share the same pattern: redirect to provider → receive callback → find-or-create user → issue tokens.
GET /api/v1/auth/google
Returns a Google OAuth redirect URL. The frontend redirects the browser to this URL.
Response: { url: "https://accounts.google.com/o/oauth2/v2/auth?..." }
POST /api/v1/auth/google/callback
Exchanges the OAuth code for user details. Find-or-creates the user and links the oauth_accounts row.
Request: { code: "<authorization_code>" }
Response: { accessToken, refreshToken, user }
The same pattern applies for:
| Provider | Initiate | Callback |
|---|---|---|
GET /auth/google | POST /auth/google/callback | |
| Microsoft | GET /auth/microsoft | POST /auth/microsoft/callback |
| GitHub | GET /auth/github | POST /auth/github/callback |
| Apple | GET /auth/apple | POST /auth/apple/callback |
Note on Apple: Apple uses a JWT id_token (not a standard JSON response) and only returns the user's name on the first OAuth consent. Subsequent sign-ins do not include the name — the frontend must collect it separately if needed.