Two-Factor Authentication
Secure your account with TOTP-based two-factor authentication (2FA)
Add an extra layer of security to your MailLand account with TOTP-based two-factor authentication. When enabled, logging in requires both your password and a time-based code from an authenticator app.
Setup 2FA
Initiate setup
POST https://api.mail.land/v1/auth/2fa/setupcurl -X POST https://api.mail.land/v1/auth/2fa/setup \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Returns a TOTP secret, QR code, and your backup codes:
{
"secret": "JBSWY3DPEHPK3PXP",
"secretMasked": "JBSW...3PXP",
"otpAuthUri": "otpauth://totp/MailLand:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=MailLand",
"qrCodeDataUri": "data:image/png;base64,iVBORw0KGgo...",
"backupCodes": [
"A1B2C3D4",
"E5F6G7H8",
"I9J0K1L2",
"M3N4O5P6",
"Q7R8S9T0",
"U1V2W3X4",
"Y5Z6A7B8",
"C9D0E1F2",
"G3H4I5J6",
"K7L8M9N0"
]
}| Field | Type | Description |
|---|---|---|
secret | string | Base32-encoded TOTP secret (for manual entry) |
secretMasked | string | Masked form of the secret, safe to display in logs/UI |
otpAuthUri | string | URI for authenticator apps |
qrCodeDataUri | string | QR code image as data URI |
backupCodes | string[] | 10 single-use backup codes, 8 characters each (A-Z2-9) |
Save the 10 backup codes now: the setup response is the only place they are ever shown. enable only confirms 2FA is on; it does not return the codes again.
Scan the QR code with an authenticator app (Google Authenticator, Authy, 1Password, etc.) or enter the secret manually.
Enable 2FA
Confirm setup by providing a code from your authenticator app:
POST https://api.mail.land/v1/auth/2fa/enable{
"code": "123456"
}curl -X POST https://api.mail.land/v1/auth/2fa/enable \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'{
"message": "Two-factor authentication enabled successfully"
}The enable response does not repeat the backup codes: they were only shown once, in the setup response above. Each code can be used once to log in if you lose access to your authenticator app.
Verify a Code
When login returns requiresTwoFactor: true, complete authentication by verifying a TOTP code:
POST https://api.mail.land/v1/auth/2fa/verify{
"challengeId": "2fa_challenge_abc123",
"code": "123456",
"trustDevice": true
}trustDevice is optional. When true, the device is remembered for 30 days and 2FA is skipped on subsequent logins from it.
curl -X POST https://api.mail.land/v1/auth/2fa/verify \
-H "Content-Type: application/json" \
-d '{
"challengeId": "2fa_challenge_abc123",
"code": "123456"
}'{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_a1b2c3d4e5...",
"deviceTrusted": false
}There is no expiresIn and no user object in this response. deviceTrusted is true when trustDevice was true. The device token itself is never in the body: the API sets it as the HttpOnly __Host-trusted_device cookie, and the browser sends it with the next sign-in.
Use a Backup Code
If you lose access to your authenticator app, use a backup code instead:
POST https://api.mail.land/v1/auth/2fa/verify-backup{
"challengeId": "2fa_challenge_abc123",
"code": "a1b2c3d4"
}The field is code (8–10 characters), not backupCode.
curl -X POST https://api.mail.land/v1/auth/2fa/verify-backup \
-H "Content-Type: application/json" \
-d '{
"challengeId": "2fa_challenge_abc123",
"code": "a1b2c3d4"
}'Returns the same response as TOTP verification. Each backup code can only be used once.
Regenerate Backup Codes
Generate a new set of backup codes (invalidates all previous codes). Requires a current TOTP code:
POST https://api.mail.land/v1/auth/2fa/backup-codes/regenerate{
"code": "123456"
}curl -X POST https://api.mail.land/v1/auth/2fa/backup-codes/regenerate \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'[
"X2Y3Z4A5",
"B6C7D8E9",
"F2G3H4J5",
"K6L7M8N9",
"P2Q3R4S5",
"T6U7V8W9",
"X6Y7Z8A9",
"B2C3D4E5",
"F6G7H8J9",
"K2L3M4N5"
]The response is a bare JSON array of strings, not { "backupCodes": [...] }.
Check 2FA Status
GET https://api.mail.land/v1/auth/2fa/statuscurl https://api.mail.land/v1/auth/2fa/status \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"isEnabled": true,
"enabledAt": "2026-01-15T10:00:00Z",
"backupCodesRemaining": 6,
"trustedDevicesCount": 2
}Trusted Devices
When verifying 2FA, the device can be remembered to skip 2FA on future logins from the same browser.
List Trusted Devices
GET https://api.mail.land/v1/auth/2fa/devicescurl https://api.mail.land/v1/auth/2fa/devices \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."[
{
"id": "dev_a1b2c3d4e5",
"deviceName": "Chrome on macOS",
"ipAddress": "203.0.113.10",
"createdAt": "2026-01-20T14:00:00Z",
"expiresAt": "2026-02-19T14:00:00Z",
"lastUsedAt": "2026-02-17T09:00:00Z",
"isCurrent": true
},
{
"id": "dev_f6g7h8i9j0",
"deviceName": "Firefox on Windows",
"ipAddress": "198.51.100.24",
"createdAt": "2026-02-01T08:00:00Z",
"expiresAt": "2026-03-03T08:00:00Z",
"lastUsedAt": "2026-02-16T15:30:00Z",
"isCurrent": false
}
]The response is a bare array, not { "devices": [...] }, and the fields are deviceName, ipAddress, expiresAt and isCurrent rather than name.
Revoke a Device
DELETE https://api.mail.land/v1/auth/2fa/devices/{id}Returns 204 on success, or 404 RESOURCE_NOT_FOUND if the device id does not belong to the current user.
curl -X DELETE https://api.mail.land/v1/auth/2fa/devices/dev_a1b2c3d4e5 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Revoke All Devices
DELETE https://api.mail.land/v1/auth/2fa/devicescurl -X DELETE https://api.mail.land/v1/auth/2fa/devices \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Revokes all trusted devices, requiring 2FA verification on the next login from every device.
Changing the password, resetting it and signing out everywhere (POST /v1/auth/revoke-all-tokens) also revoke every trusted device, in the same way.
Disable 2FA
POST https://api.mail.land/v1/auth/2fa/disable{
"code": "123456"
}curl -X POST https://api.mail.land/v1/auth/2fa/disable \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'Requires a valid TOTP or backup code to confirm the action: a TOTP code is tried first, and a backup code is accepted only if the TOTP check fails. Disabling 2FA invalidates all backup codes and trusted devices.
Error Responses
Most failures (an invalid code, an already-enabled/disabled state, an expired challenge, a reused backup code) return 400 INVALID_REQUEST with a message. A malformed request body (wrong code length, missing challengeId, etc.) fails request validation first and returns 400 VALIDATION_FAILED instead. An unknown device returns 404 RESOURCE_NOT_FOUND. There is no separate INVALID_CODE, TWO_FACTOR_* or CHALLENGE_EXPIRED code.
| Status | Code | Description |
|---|---|---|
400 | INVALID_REQUEST | Invalid or expired code, wrong state, reused backup code, etc. |
400 | VALIDATION_FAILED | Request body failed field validation (e.g. wrong code length, missing field) |
404 | RESOURCE_NOT_FOUND | Device not found |
Next Steps
- Authentication: login and token management
- Team Management: manage team access
- Workspaces: workspace settings