Skip to main content

Authentication API

Endpoints for registering users, logging in, logging out, refreshing tokens, resetting passwords, verifying email addresses, and passwordless authentication via magic links. MFA challenge verification is also handled here.

All endpoints live under the per-application prefix:

text
/api/v1/applications/{applicationId}/users/...

These are public endpoints — no prior authentication is required, except where noted.

Register

POST /api/v1/applications/{applicationId}/users/register

Create a new user account for the given application. After registration, a verification email is sent to the user.

Request Body

FieldTypeRequiredDescription
emailstringYesValid email address. Must be unique within the application.
passwordstringYesMust meet the application's password policy (min 8 chars, mixed case, number, special character).
namestringYesDisplay name for the user. Max 255 characters.
metadataobjectNoArbitrary key-value pairs stored against the user.

Response

201 Created

json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "alice@example.com",
    "name": "Alice",
    "email_verified": false,
    "created_at": "2026-02-25T12:00:00Z"
  },
  "message": "Registration successful. Please check your email to verify your account."
}

Error Responses

StatusCodeDescription
400VALIDATION_MULTIPLE_ERRORSOne or more fields failed validation
400VALIDATION_INVALID_FORMATEmail is not a valid address
409RESOURCE_ALREADY_EXISTSEmail already registered for this application
403tier_limit_exceededApplication user limit reached for current plan

Login

POST /api/v1/applications/{applicationId}/users/login

Authenticate a user with email and password. If the user has MFA enabled, a challenge token is returned instead of access tokens.

Request Body

FieldTypeRequiredDescription
emailstringYesUser's email address
passwordstringYesUser's password
remember_mebooleanNoExtend session lifetime. Defaults to false.

Response — Standard Login

200 OK

json
{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiJ9...",
    "refresh_token": "ref_a1b2c3d4...",
    "token_type": "Bearer",
    "expires_in": 900,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "alice@example.com",
      "name": "Alice"
    }
  }
}

Response — MFA Required

200 OK

json
{
  "data": {
    "mfa_required": true,
    "challenge_token": "mfa_challenge_xyz789",
    "mfa_methods": ["totp"]
  }
}

Use the challenge_token with the Verify MFA endpoint to complete login.

Error Responses

StatusCodeDescription
401AUTH_INVALID_CREDENTIALSWrong email or password
403AUTH_ACCOUNT_SUSPENDEDAccount has been suspended
429AUTH_ACCOUNT_LOCKEDToo many failed attempts — account temporarily locked

Logout

POST /api/v1/applications/{applicationId}/users/logout

Revoke a refresh token and blacklist the associated access token. The access token is invalidated for its remaining lifetime via Redis.

Request Body

FieldTypeRequiredDescription
refresh_tokenstringYesThe refresh token to revoke

Response

204 No Content


Refresh Token

POST /api/v1/applications/{applicationId}/users/token/refresh

Exchange a valid refresh token for a new access token and refresh token pair. The previous refresh token is rotated (invalidated) as part of this operation.

Request Body

FieldTypeRequiredDescription
refresh_tokenstringYesA currently valid refresh token

Response

200 OK

json
{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiJ9...",
    "refresh_token": "ref_d4e5f6a7...",
    "token_type": "Bearer",
    "expires_in": 900,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "alice@example.com",
      "name": "Alice"
    }
  }
}

Error Responses

StatusCodeDescription
401AUTH_INVALID_REFRESH_TOKENToken is invalid, expired, or already revoked

Forgot Password

POST /api/v1/applications/{applicationId}/users/password/forgot

Request a password reset email. Always returns a success response to prevent email enumeration. Rate limited to 3 requests per 15 minutes per email address.

Request Body

FieldTypeRequiredDescription
emailstringYesThe user's email address

Response

200 OK

json
{
  "data": {
    "message": "If an account with that email exists, a password reset link has been sent."
  }
}

Error Responses

StatusCodeDescription
429AUTH_PASSWORD_RESET_RATE_LIMITEDToo many reset requests

Reset Password

POST /api/v1/applications/{applicationId}/users/password/reset

Complete a password reset using the token received via email.

Request Body

FieldTypeRequiredDescription
tokenstringYesThe reset token from the email link
emailstringYesThe user's email address
passwordstringYesNew password — must meet password policy

Response

200 OK

json
{
  "data": {
    "message": "Your password has been reset successfully."
  }
}

Error Responses

StatusCodeDescription
400AUTH_INVALID_RESET_TOKENToken is invalid or does not match email
410AUTH_RESET_TOKEN_EXPIREDToken has expired

Verify Email

POST /api/v1/applications/{applicationId}/users/email/verify

Verify a user's email address using the token received in the verification email.

Request Body

FieldTypeRequiredDescription
tokenstringYesThe email verification token

Response

200 OK

json
{
  "data": {
    "message": "Email address verified successfully."
  }
}

Error Responses

StatusCodeDescription
400AUTH_INVALID_VERIFICATION_TOKENToken is invalid
410AUTH_VERIFICATION_TOKEN_EXPIREDToken has expired

Resend Verification Email

POST /api/v1/applications/{applicationId}/users/email/resend

Send a new verification email. Always returns success to prevent email enumeration. Rate limited to 2 requests per minute.

Request Body

FieldTypeRequiredDescription
emailstringYesThe user's email address

Response

200 OK

json
{
  "data": {
    "message": "If an account with that email exists and is not verified, a verification email has been sent."
  }
}

Error Responses

StatusCodeDescription
429AUTH_VERIFICATION_RATE_LIMITEDToo many resend requests

POST /api/v1/applications/{applicationId}/users/magic-link

Send a passwordless login link to the user's email address. Always returns success to prevent email enumeration. Rate limited to 3 requests per 5 minutes per email.

Request Body

FieldTypeRequiredDescription
emailstringYesThe user's email address
redirect_urlstringNoURL to redirect to after successful verification. Must be an allowed redirect URL for the application. Max 2048 characters.

Response

200 OK

json
{
  "data": {
    "message": "If an account with that email exists, a magic link has been sent."
  }
}

Error Responses

StatusCodeDescription
429AUTH_MAGIC_LINK_RATE_LIMITEDToo many magic link requests

POST /api/v1/applications/{applicationId}/users/magic-link/verify

Verify a magic link token and complete passwordless login.

Request Body

FieldTypeRequiredDescription
tokenstringYesThe magic link token from the email

Response

200 OK

json
{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiJ9...",
    "refresh_token": "ref_a1b2c3d4...",
    "token_type": "Bearer",
    "expires_in": 900,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "alice@example.com",
      "name": "Alice"
    },
    "redirect_url": "https://your-app.com/dashboard"
  }
}

Error Responses

StatusCodeDescription
400AUTH_INVALID_MAGIC_LINKToken is invalid or already used
410AUTH_MAGIC_LINK_EXPIREDToken has expired

Verify MFA

POST /api/v1/applications/{applicationId}/users/mfa/verify

Complete a login that requires MFA by submitting the TOTP code or backup code along with the challenge token returned by the login endpoint.

Request Body

FieldTypeRequiredDescription
challenge_tokenstringYesThe challenge_token from the login response
codestringYes6-digit TOTP code or 8-character backup code

Response

200 OK

json
{
  "data": {
    "access_token": "eyJhbGciOiJSUzI1NiJ9...",
    "refresh_token": "ref_a1b2c3d4...",
    "token_type": "Bearer",
    "expires_in": 900,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "alice@example.com",
      "name": "Alice"
    }
  }
}

Error Responses

StatusCodeDescription
401AUTH_INVALID_MFA_CODECode is incorrect
410AUTH_MFA_CHALLENGE_EXPIREDChallenge token has expired
429AUTH_MFA_LOCKEDToo many failed MFA attempts

Setup TOTP

POST /api/v1/applications/{applicationId}/users/{userId}/mfa/totp/setup

Begin TOTP (authenticator app) setup for a user. Returns a provisioning URI that can be rendered as a QR code, and the raw secret for manual entry.

Authentication

Requires Authorization: Bearer {access_token}. The token's user must match {userId}.

Request Body

FieldTypeRequiredDescription
labelstringNoDisplay label shown in the authenticator app. Defaults to the application name.

Response

200 OK

json
{
  "data": {
    "method_id": "8f14e45f-ceea-467a-a866-9ea7e3c3b2e2",
    "provisioning_uri": "otpauth://totp/MyApp%3Aalice%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp",
    "secret": "JBSWY3DPEHPK3PXP"
  }
}

Confirm TOTP Setup

POST /api/v1/applications/{applicationId}/users/{userId}/mfa/totp/confirm

Verify the TOTP setup by submitting a valid code from the authenticator app. On success, MFA is activated and backup codes are returned.

Authentication

Requires Authorization: Bearer {access_token}. The token's user must match {userId}.

Request Body

FieldTypeRequiredDescription
method_idstring (UUID)YesThe method_id from the setup response
codestringYes6-digit TOTP code from the authenticator app

Response

201 Created

json
{
  "data": {
    "message": "MFA has been enabled successfully.",
    "backup_codes": [
      "ABC12345",
      "DEF67890",
      "GHI11223",
      "JKL44556",
      "MNO77889",
      "PQR00112",
      "STU33445",
      "VWX66778"
    ]
  }
}

Store backup codes in a secure location. They are shown only once and cannot be retrieved afterwards.

Error Responses

StatusCodeDescription
422MFA_INVALID_CODEThe TOTP code is incorrect

Disable TOTP

DELETE /api/v1/applications/{applicationId}/users/{userId}/mfa/totp

Disable TOTP MFA for a user. Requires password re-authentication.

Authentication

Requires Authorization: Bearer {access_token}. The token's user must match {userId}.

Request Body

FieldTypeRequiredDescription
passwordstringYesCurrent account password

Response

204 No Content

Error Responses

StatusCodeDescription
422INVALID_PASSWORDPassword verification failed

MFA Status

GET /api/v1/applications/{applicationId}/users/{userId}/mfa/status

Get the current MFA status for a user, including active methods and remaining backup codes.

Authentication

Requires Authorization: Bearer {access_token}. The token's user must match {userId}.

Response

200 OK

json
{
  "data": {
    "mfa_enabled": true,
    "methods": [
      {
        "id": "8f14e45f-ceea-467a-a866-9ea7e3c3b2e2",
        "type": "totp",
        "label": "My Authenticator",
        "is_primary": true,
        "verified_at": "2026-02-20T10:00:00Z",
        "last_used_at": "2026-02-25T08:30:00Z"
      }
    ],
    "backup_codes_remaining": 6
  }
}

Regenerate Backup Codes

POST /api/v1/applications/{applicationId}/users/{userId}/mfa/backup-codes/regenerate

Generate a new set of backup codes. All previous backup codes are immediately invalidated. Requires password re-authentication.

Authentication

Requires Authorization: Bearer {access_token}. The token's user must match {userId}.

Request Body

FieldTypeRequiredDescription
passwordstringYesCurrent account password

Response

200 OK

json
{
  "data": {
    "backup_codes": [
      "ABC12345",
      "DEF67890",
      "GHI11223",
      "JKL44556",
      "MNO77889",
      "PQR00112",
      "STU33445",
      "VWX66778"
    ],
    "message": "New backup codes generated. Previous codes are now invalid."
  }
}

Error Responses

StatusCodeDescription
422INVALID_PASSWORDPassword verification failed
400MFA_NOT_ENABLEDMFA must be enabled before generating backup codes