Skip to main content

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:

HeaderValue
AuthorizationBearer <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:

FieldTypeDescription
typestringFactor type: totp, email, sms, webauthn, backup_code
friendly_namestringHuman-readable label (masked for email/sms)
created_atstringISO 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:

HeaderValue
AuthorizationBearer <user_access_token>
Content-Typeapplication/json

Request body:

ParameterTypeRequiredDescription
factor_typestringYesOne of: totp, email, sms, webauthn, backup_code
purposestringYesAction being verified (e.g., account.delete). Pattern: /^[a-z][a-z0-9_.]{2,50}$/
resource_idstringNoOptional resource identifier for audit (e.g., order:456)
issue_step_up_tokenbooleanNoReturn 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:

HeaderValue
AuthorizationBearer <user_access_token>
Content-Typeapplication/json

Request body (TOTP / Email / SMS / Backup code)

ParameterTypeRequiredDescription
codestringYesThe 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)

ParameterTypeRequiredDescription
credentialobjectYesThe 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

StatusError codeMeaning
410mfa.challenge_expiredChallenge TTL has elapsed (5 min for TOTP/WebAuthn, 10 min for email/SMS)
410mfa.challenge_not_activeChallenge already verified or permanently failed
410mfa.challenge_max_attempts5 failed attempts -- challenge is locked. Create a new one.
422invalid_codeWrong verification code. Response includes attempts_remaining.
422factor_not_enrolledUser 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.

caution

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.