Skip to content

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

json
{
  "accountName": "Acme Corp",
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@acme.com",
  "password": "min8chars"
}
FieldTypeRequiredNotes
accountNamestringYes
firstNamestringYes
lastNamestringYes
emailstringYesMust be unique across all users
passwordstringYesMinimum 8 characters

Response — 201 Created

json
{
  "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

CodeCause
409 ConflictEmail already registered
422 UnprocessableValidation 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

json
{
  "token": "<raw_token_from_email_link>"
}

Response — 200 OK

json
{
  "accessToken": "<jwt>",
  "refreshToken": "<opaque_token>",
  "user": { ... }
}

Errors

CodeCause
400 Bad RequestToken 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

json
{
  "email": "jane@acme.com"
}

Response — 200 OK

json
{
  "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

json
{
  "email": "jane@acme.com",
  "password": "min8chars"
}

Response — 200 OK

json
{
  "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

CodeCause
401 UnauthorizedEmail 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

json
{
  "refreshToken": "<opaque_token>"
}

Response — 200 OK

json
{
  "accessToken": "<new_jwt>",
  "refreshToken": "<new_opaque_token>"
}

Errors

CodeCause
401 UnauthorizedToken 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

json
{
  "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

json
{
  "email": "jane@acme.com"
}

Response — 200 OK

json
{
  "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

json
{
  "token": "<raw_token_from_email_link>",
  "newPassword": "newSecurePassword1"
}
FieldTypeRequiredNotes
tokenstringYesFrom the reset email link
newPasswordstringYesMinimum 8 characters

Response — 200 OK

json
{
  "message": "Password updated. You can now log in."
}

Errors

CodeCause
400 Bad RequestToken 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

json
{
  "email": "jane@acme.com"
}

Response — 200 OK

json
{
  "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

json
{
  "token": "<raw_token_from_email_link>"
}

Response — 200 OK

json
{
  "accessToken": "<jwt>",
  "refreshToken": "<opaque_token>",
  "user": { ... }
}

Errors

CodeCause
400 Bad RequestToken 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:

ProviderInitiateCallback
GoogleGET /auth/googlePOST /auth/google/callback
MicrosoftGET /auth/microsoftPOST /auth/microsoft/callback
GitHubGET /auth/githubPOST /auth/github/callback
AppleGET /auth/applePOST /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.

Internal use only — Sema Link Engineering