MFA API Reference
Complete reference for the MFA Verification API. All user-facing endpoints require a user access token with the mfa:verify scope. Management endpoints require an M2M token.
Base URL: https://app.voxkey.io/api/v1/{realmUUID}
List MFA factors
Returns the user's enrolled MFA factors and available (not yet enrolled) factor types.
GET /mfa/factors
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <user_access_token> |
Response 200 OK:
{
"enrolled": [
{
"type": "totp",
"friendly_name": "Google Authenticator",
"created_at": "2026-01-15T12:00:00Z"
},
{
"type": "email",
"friendly_name": "t***@example.com",
"created_at": "2026-02-01T09:00:00Z"
}
],
"available": ["sms", "webauthn", "backup_code"]
}
Enrolled factor fields:
| Field | Type | Description |
|---|---|---|
type | string | Factor type: totp, email, sms, webauthn, backup_code |
friendly_name | string | Human-readable label (masked for email/sms) |
created_at | string | ISO 8601 enrollment timestamp |
Create MFA challenge
Creates a new challenge for a specific factor type. For email and sms factors, this triggers delivery of a one-time code.
POST /mfa/challenges
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <user_access_token> |
Content-Type | application/json |
Request body:
| Parameter | Type | Required | Description |
|---|---|---|---|
factor_type | string | Yes | One of: totp, email, sms, webauthn, backup_code |
purpose | string | Yes | Action being verified (e.g., account.delete). Pattern: /^[a-z][a-z0-9_.]{2,50}$/ |
resource_id | string | No | Optional resource identifier for audit (e.g., order:456) |
issue_step_up_token | boolean | No | Return a signed JWT after verification. Default: false |
Request example:
curl -X POST https://app.voxkey.io/api/v1/{realmUUID}/mfa/challenges \
-H "Authorization: Bearer <user_access_token>" \
-H "Content-Type: application/json" \
-d '{
"factor_type": "totp",
"purpose": "password.change",
"issue_step_up_token": true
}'
Response by factor type
TOTP -- 201 Created:
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"factor_type": "totp",
"purpose": "password.change",
"expires_at": "2026-04-03T10:05:00Z",
"challenge_type": "code"
}
No delivery triggered -- user enters the current code from their authenticator app.
Email -- 201 Created:
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440001",
"factor_type": "email",
"purpose": "password.change",
"expires_at": "2026-04-03T10:10:00Z",
"challenge_type": "code",
"delivery": {
"destination": "t***@example.com",
"sent": true
}
}
A 6-digit code is sent to the user's verified email address.
SMS -- 201 Created:
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440002",
"factor_type": "sms",
"purpose": "password.change",
"expires_at": "2026-04-03T10:10:00Z",
"challenge_type": "code",
"delivery": {
"destination": "+1***4567",
"sent": true
}
}
A 6-digit code is sent via SMS.
WebAuthn -- 201 Created:
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440003",
"factor_type": "webauthn",
"purpose": "password.change",
"expires_at": "2026-04-03T10:05:00Z",
"challenge_type": "webauthn",
"webauthn_options": {
"challenge": "base64url-encoded-challenge",
"rpId": "app.voxkey.io",
"allowCredentials": [
{
"type": "public-key",
"id": "base64url-credential-id"
}
],
"userVerification": "required",
"timeout": 300000
}
}
Pass webauthn_options to the browser's navigator.credentials.get() API.
Backup code -- 201 Created:
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440004",
"factor_type": "backup_code",
"purpose": "password.change",
"expires_at": "2026-04-03T10:10:00Z",
"challenge_type": "code"
}
User enters one of their pre-generated backup codes.
Error 422 -- Factor not enrolled:
{
"error": "factor_not_enrolled",
"message": "User does not have totp factor enrolled",
"available_factors": ["email", "backup_code"]
}
Rate limit: 5 challenges per minute per user+client. Exceeding returns 429.
Verify MFA challenge
Submit the verification code (or WebAuthn assertion) to complete the challenge.
POST /mfa/challenges/{challengeId}/verify
Headers:
| Header | Value |
|---|---|
Authorization | Bearer <user_access_token> |
Content-Type | application/json |
Request body (TOTP / Email / SMS / Backup code)
| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The 6-digit verification code (or backup code) |
curl -X POST https://app.voxkey.io/api/v1/{realmUUID}/mfa/challenges/{challengeId}/verify \
-H "Authorization: Bearer <user_access_token>" \
-H "Content-Type: application/json" \
-d '{"code": "482916"}'
Request body (WebAuthn)
| Parameter | Type | Required | Description |
|---|---|---|---|
credential | object | Yes | The navigator.credentials.get() response |
curl -X POST https://app.voxkey.io/api/v1/{realmUUID}/mfa/challenges/{challengeId}/verify \
-H "Authorization: Bearer <user_access_token>" \
-H "Content-Type: application/json" \
-d '{
"credential": {
"id": "base64url-credential-id",
"rawId": "base64url-raw-id",
"response": {
"authenticatorData": "base64url-auth-data",
"clientDataJSON": "base64url-client-data",
"signature": "base64url-signature"
},
"type": "public-key"
}
}'
Success response 200 OK
Without step-up token:
{
"verified": true,
"factor_type": "totp",
"verified_at": "2026-04-03T10:04:30Z"
}
With step-up token (when issue_step_up_token: true was set on the challenge):
{
"verified": true,
"factor_type": "totp",
"verified_at": "2026-04-03T10:04:30Z",
"step_up_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6InN0ZXBfdXArand0In0..."
}
See Step-Up Token for how to validate the token.
Error responses
| Status | Error code | Meaning |
|---|---|---|
410 | mfa.challenge_expired | Challenge TTL has elapsed (5 min for TOTP/WebAuthn, 10 min for email/SMS) |
410 | mfa.challenge_not_active | Challenge already verified or permanently failed |
410 | mfa.challenge_max_attempts | 5 failed attempts -- challenge is locked. Create a new one. |
422 | invalid_code | Wrong verification code. Response includes attempts_remaining. |
422 | factor_not_enrolled | User has not enrolled this factor type. Response includes available_factors. |
429 | -- | Rate limit exceeded. Retry-After header indicates wait time in seconds. |
422 response with remaining attempts:
{
"error": "invalid_code",
"attempts_remaining": 3
}
410 response (challenge exhausted):
{
"error": "mfa.challenge_max_attempts",
"message": "Maximum verification attempts exceeded. Create a new challenge."
}
429 response (rate limited):
{
"error": "rate_limited",
"message": "Too many attempts. Try again later.",
"retry_after": 60
}
Management API
These endpoints require an M2M token (client_credentials grant). They allow administrators to view and remove MFA factors for any user in the realm.
List user's MFA factors
GET /api/v1/{realmUUID}/users/{userId}/mfa-factors
Required scope: mfa:read
curl https://app.voxkey.io/api/v1/{realmUUID}/users/{userId}/mfa-factors \
-H "Authorization: Bearer <m2m_token>"
Response 200 OK:
[
{
"type": "totp",
"friendly_name": "Google Authenticator",
"created_at": "2026-01-15T12:00:00Z"
},
{
"type": "email",
"friendly_name": "t***@example.com",
"created_at": "2026-02-01T09:00:00Z"
}
]
Delete user's MFA factor
Removes a specific MFA factor from a user. Useful for support scenarios where a user has lost access to their authenticator.
DELETE /api/v1/{realmUUID}/users/{userId}/mfa-factors/{type}
Required scope: mfa:write
curl -X DELETE https://app.voxkey.io/api/v1/{realmUUID}/users/{userId}/mfa-factors/totp \
-H "Authorization: Bearer <m2m_token>"
Response 204 No Content on success.
Deleting a user's only MFA factor effectively disables MFA for that user. If the realm's MFA policy is set to "Required", the user will be prompted to re-enroll on their next sign-in.