Skip to main content

Multi-Factor Authentication

Add TOTP-based (Time-based One-Time Password) two-factor authentication to your application. Compatible with authenticator apps like Google Authenticator, Authy, and 1Password. Includes backup codes for account recovery.

MFA Enrollment Flow

1. Your app calls enroll      ──▶  Zyphr returns secret + QR code + backup codes
2. User scans QR code ──▶ Authenticator app
3. User enters TOTP code ──▶ Your app calls verify-enrollment
4. MFA is now enabled ──▶ Future logins require TOTP

Enroll User in MFA

Start enrollment by requesting a TOTP secret and QR code:

curl -X POST https://api.zyphr.dev/v1/auth/mfa/enroll \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{ "user_id": "usr_abc123" }'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/enroll', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: userId }),
});

Parameters

ParameterTypeRequiredDescription
user_idstring (UUID)YesThe end user's ID

Response

{
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qr_code": "data:image/png;base64,...",
"backup_codes": [
"a1b2c3d4e5",
"f6g7h8i9j0",
"k1l2m3n4o5",
"p6q7r8s9t0",
"u1v2w3x4y5",
"z6a7b8c9d0",
"e1f2g3h4i5",
"j6k7l8m9n0"
]
},
"meta": {
"message": "Scan the QR code with your authenticator app, then verify with a code"
}
}

Display the QR code to the user and instruct them to save the backup codes securely.

Error Responses

StatusCodeCause
400email_requiredUser must have an email to enable MFA
404user_not_foundUser ID not found in application
409mfa_already_enabledMFA is already active for this user

Verify Enrollment

After the user scans the QR code, they enter a TOTP code from their authenticator to confirm setup:

curl -X POST https://api.zyphr.dev/v1/auth/mfa/verify-enrollment \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"user_id": "usr_abc123",
"totp_code": "123456"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/verify-enrollment', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: userId,
totp_code: totpCode,
}),
});

Parameters

ParameterTypeRequiredDescription
user_idstring (UUID)YesThe end user's ID
totp_codestringYes6-digit code from authenticator app

Response

{
"data": {
"mfa_enabled": true
},
"meta": {
"message": "MFA has been enabled successfully"
}
}

Login with MFA

When a user with MFA enabled calls the login endpoint, they receive an MFA challenge instead of tokens. Complete the flow with one of these endpoints:

Verify with TOTP Code

curl -X POST https://api.zyphr.dev/v1/auth/mfa/verify \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"challenge_token": "mfa_challenge_xxxx",
"totp_code": "123456"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/verify', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
challenge_token: challengeToken,
totp_code: totpCode,
}),
});

Parameters

ParameterTypeRequiredDescription
challenge_tokenstringYesFrom the login response's mfa_challenge.token
totp_codestringYes6-digit code from authenticator app

Response

On success, returns the same user + tokens response as a standard login:

{
"data": {
"user": {
"id": "usr_abc123",
"email": "jane@example.com",
"name": "Jane Doe",
"avatar_url": null,
"email_verified": true,
"mfa_enabled": true
},
"tokens": {
"access_token": "eyJhbGci...",
"refresh_token": "zrt_xxxx",
"expires_in": 3600,
"token_type": "Bearer"
}
}
}

Verify with Backup Code

If the user doesn't have access to their authenticator app:

curl -X POST https://api.zyphr.dev/v1/auth/mfa/verify-backup \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"challenge_token": "mfa_challenge_xxxx",
"backup_code": "a1b2c3d4e5"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/verify-backup', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
challenge_token: challengeToken,
backup_code: backupCode,
}),
});

Parameters

ParameterTypeRequiredDescription
challenge_tokenstringYesFrom the login response's mfa_challenge.token
backup_codestringYesOne of the backup codes (8–12 chars). Each code can only be used once.

The response includes a remaining_backup_codes count. When it's low, warn the user:

{
"data": {
"user": { "..." : "..." },
"tokens": { "..." : "..." },
"remaining_backup_codes": 2
},
"meta": {
"warning": "Low backup codes remaining. Consider regenerating."
}
}

MFA Management

Check MFA Status

curl https://api.zyphr.dev/v1/auth/mfa/status/usr_abc123 \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx"
Node.js
const response = await fetch(`https://api.zyphr.dev/v1/auth/mfa/status/${userId}`, {
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
},
});

Response

{
"data": {
"mfa_enabled": true,
"status": "verified",
"enabled_at": "2025-01-15T10:00:00Z",
"remaining_backup_codes": 6
}
}

Status values: not_enrolled, pending, verified.

Disable MFA

Requires verification with a TOTP code or backup code:

curl -X POST https://api.zyphr.dev/v1/auth/mfa/disable \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"user_id": "usr_abc123",
"verification_code": "123456"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/disable', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: userId,
verification_code: totpCode, // 6-digit TOTP or backup code
}),
});
ParameterTypeRequiredDescription
user_idstring (UUID)YesThe end user's ID
verification_codestringYesCurrent TOTP code (6 digits) or a backup code (8–12 chars)

Regenerate Backup Codes

Generate new backup codes. Existing codes are invalidated:

curl -X POST https://api.zyphr.dev/v1/auth/mfa/regenerate-backup-codes \
-H "X-Application-Key: za_pub_xxxx" \
-H "X-Application-Secret: za_sec_xxxx" \
-H "Content-Type: application/json" \
-d '{
"user_id": "usr_abc123",
"totp_code": "123456"
}'
Node.js
const response = await fetch('https://api.zyphr.dev/v1/auth/mfa/regenerate-backup-codes', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_id: userId,
totp_code: totpCode,
}),
});
ParameterTypeRequiredDescription
user_idstring (UUID)YesThe end user's ID
totp_codestringYesCurrent 6-digit TOTP code for verification

Response

{
"data": {
"backup_codes": [
"new1code01",
"new2code02",
"..."
]
},
"meta": {
"message": "New backup codes generated. Previous codes are now invalid."
}
}

Complete MFA-Aware Login Example

Node.js — MFA-aware login flow
async function login(email, password) {
// Step 1: Attempt login
const loginResponse = await fetch('https://api.zyphr.dev/v1/auth/users/login', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});

const { data } = await loginResponse.json();

// Step 2: Check if MFA is required
if (data.mfa_required) {
// Prompt user for TOTP code
const totpCode = await promptForTOTP();

// Step 3: Verify MFA
const mfaResponse = await fetch('https://api.zyphr.dev/v1/auth/mfa/verify', {
method: 'POST',
headers: {
'X-Application-Key': process.env.ZYPHR_APP_PUBLIC_KEY,
'X-Application-Secret': process.env.ZYPHR_APP_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
challenge_token: data.mfa_challenge.token,
totp_code: totpCode,
}),
});

return mfaResponse.json();
}

// No MFA — already have tokens
return { data };
}

Endpoint Reference

MethodEndpointAuthDescription
POST/v1/auth/mfa/enrollApp credentialsStart MFA enrollment
POST/v1/auth/mfa/verify-enrollmentApp credentialsComplete enrollment
POST/v1/auth/mfa/verifyApp credentialsVerify TOTP during login
POST/v1/auth/mfa/verify-backupApp credentialsVerify backup code during login
GET/v1/auth/mfa/status/:userIdApp credentialsGet MFA status
POST/v1/auth/mfa/disableApp credentialsDisable MFA
POST/v1/auth/mfa/regenerate-backup-codesApp credentialsGenerate new backup codes