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" }'
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
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The 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
| Status | Code | Cause |
|---|---|---|
| 400 | email_required | User must have an email to enable MFA |
| 404 | user_not_found | User ID not found in application |
| 409 | mfa_already_enabled | MFA 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"
}'
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
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The end user's ID |
totp_code | string | Yes | 6-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"
}'
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
| Parameter | Type | Required | Description |
|---|---|---|---|
challenge_token | string | Yes | From the login response's mfa_challenge.token |
totp_code | string | Yes | 6-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"
}'
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
| Parameter | Type | Required | Description |
|---|---|---|---|
challenge_token | string | Yes | From the login response's mfa_challenge.token |
backup_code | string | Yes | One 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"
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"
}'
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
}),
});
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The end user's ID |
verification_code | string | Yes | Current 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"
}'
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,
}),
});
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string (UUID) | Yes | The end user's ID |
totp_code | string | Yes | Current 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
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
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST | /v1/auth/mfa/enroll | App credentials | Start MFA enrollment |
POST | /v1/auth/mfa/verify-enrollment | App credentials | Complete enrollment |
POST | /v1/auth/mfa/verify | App credentials | Verify TOTP during login |
POST | /v1/auth/mfa/verify-backup | App credentials | Verify backup code during login |
GET | /v1/auth/mfa/status/:userId | App credentials | Get MFA status |
POST | /v1/auth/mfa/disable | App credentials | Disable MFA |
POST | /v1/auth/mfa/regenerate-backup-codes | App credentials | Generate new backup codes |