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:
/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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address. Must be unique within the application. |
password | string | Yes | Must meet the application's password policy (min 8 chars, mixed case, number, special character). |
name | string | Yes | Display name for the user. Max 255 characters. |
metadata | object | No | Arbitrary key-value pairs stored against the user. |
Response
201 Created
{
"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
| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_MULTIPLE_ERRORS | One or more fields failed validation |
| 400 | VALIDATION_INVALID_FORMAT | Email is not a valid address |
| 409 | RESOURCE_ALREADY_EXISTS | Email already registered for this application |
| 403 | tier_limit_exceeded | Application 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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User's email address |
password | string | Yes | User's password |
remember_me | boolean | No | Extend session lifetime. Defaults to false. |
Response — Standard Login
200 OK
{
"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
{
"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
| Status | Code | Description |
|---|---|---|
| 401 | AUTH_INVALID_CREDENTIALS | Wrong email or password |
| 403 | AUTH_ACCOUNT_SUSPENDED | Account has been suspended |
| 429 | AUTH_ACCOUNT_LOCKED | Too 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
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | Yes | The 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
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token | string | Yes | A currently valid refresh token |
Response
200 OK
{
"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
| Status | Code | Description |
|---|---|---|
| 401 | AUTH_INVALID_REFRESH_TOKEN | Token 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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The user's email address |
Response
200 OK
{
"data": {
"message": "If an account with that email exists, a password reset link has been sent."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 429 | AUTH_PASSWORD_RESET_RATE_LIMITED | Too 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
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The reset token from the email link |
email | string | Yes | The user's email address |
password | string | Yes | New password — must meet password policy |
Response
200 OK
{
"data": {
"message": "Your password has been reset successfully."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | AUTH_INVALID_RESET_TOKEN | Token is invalid or does not match email |
| 410 | AUTH_RESET_TOKEN_EXPIRED | Token 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
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The email verification token |
Response
200 OK
{
"data": {
"message": "Email address verified successfully."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | AUTH_INVALID_VERIFICATION_TOKEN | Token is invalid |
| 410 | AUTH_VERIFICATION_TOKEN_EXPIRED | Token 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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The user's email address |
Response
200 OK
{
"data": {
"message": "If an account with that email exists and is not verified, a verification email has been sent."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 429 | AUTH_VERIFICATION_RATE_LIMITED | Too many resend requests |
Request Magic Link
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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | The user's email address |
redirect_url | string | No | URL to redirect to after successful verification. Must be an allowed redirect URL for the application. Max 2048 characters. |
Response
200 OK
{
"data": {
"message": "If an account with that email exists, a magic link has been sent."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 429 | AUTH_MAGIC_LINK_RATE_LIMITED | Too many magic link requests |
Verify Magic Link
POST /api/v1/applications/{applicationId}/users/magic-link/verify
Verify a magic link token and complete passwordless login.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The magic link token from the email |
Response
200 OK
{
"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
| Status | Code | Description |
|---|---|---|
| 400 | AUTH_INVALID_MAGIC_LINK | Token is invalid or already used |
| 410 | AUTH_MAGIC_LINK_EXPIRED | Token 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
| Field | Type | Required | Description |
|---|---|---|---|
challenge_token | string | Yes | The challenge_token from the login response |
code | string | Yes | 6-digit TOTP code or 8-character backup code |
Response
200 OK
{
"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
| Status | Code | Description |
|---|---|---|
| 401 | AUTH_INVALID_MFA_CODE | Code is incorrect |
| 410 | AUTH_MFA_CHALLENGE_EXPIRED | Challenge token has expired |
| 429 | AUTH_MFA_LOCKED | Too 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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | No | Display label shown in the authenticator app. Defaults to the application name. |
Response
200 OK
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
method_id | string (UUID) | Yes | The method_id from the setup response |
code | string | Yes | 6-digit TOTP code from the authenticator app |
Response
201 Created
{
"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
| Status | Code | Description |
|---|---|---|
| 422 | MFA_INVALID_CODE | The 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
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | Current account password |
Response
204 No Content
Error Responses
| Status | Code | Description |
|---|---|---|
| 422 | INVALID_PASSWORD | Password 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Yes | Current account password |
Response
200 OK
{
"data": {
"backup_codes": [
"ABC12345",
"DEF67890",
"GHI11223",
"JKL44556",
"MNO77889",
"PQR00112",
"STU33445",
"VWX66778"
],
"message": "New backup codes generated. Previous codes are now invalid."
}
}
Error Responses
| Status | Code | Description |
|---|---|---|
| 422 | INVALID_PASSWORD | Password verification failed |
| 400 | MFA_NOT_ENABLED | MFA must be enabled before generating backup codes |