Troubleshooting: Verify factor suite (TOTP, push, passkey, backup codes)
The MFA factor suite — TOTP, push, passkeys, and backup codes — proves possession of a secret or device key instead of delivering an OTP, so its failures surface inside your own application’s factor flow rather than on a/verify/send row. Work the symptom map first, then the per-factor
cause table. (OTP send/check failures are on the Verify OTP page.)
Symptom map
Cause table per factor
TOTP (authenticator app)
A rejected TOTP returnsINVALID_CODE (422) with either “TOTP code is
incorrect or expired” or “Recovery code is invalid or already used”.
Creation is immediate — POST /verify/factors/totp returns the
otpauth:// URI and secret once, and the factor is live from that
response; the first /verify call is what reveals a bad pairing.
Push (device key + signed challenge)
Push failures split into issuing-time and verify-time causes:Passkeys (WebAuthn / FIDO2)
Attestation and assertion verification run server-side; RP id and origin are pinned when the challenge is issued and cross-checked againstclientDataJSON on verify, which is what makes a phishing-origin relay
fail closed.
Backup codes
SNA / flashcall possession channels
channel: "sna" and channel: "flashcall" prove possession without an
OTP, and both fail closed with 503 SERVICE_UNAVAILABLE and
details.reason: "PROVIDER_NOT_WIRED" when their dependency is missing:
Data-retention and revoke circuit: reading factor state and re-registering
All factor state is tenant-scoped to your organization, readable through the list endpoints, and removable through the delete/revoke endpoints — re-registration is a client-side flow you own, in line with tenancy being yours to operate:- TOTP / backup codes: list with
GET /verify/factors/totporGET /verify/factors/backup-codes; remove with the matchingDELETE(backup-code delete is idempotent). - Push: revoke with
POST /verify/push/factors/{factorId}/revoke(irreversible — register a fresh factor after revoking). - Passkeys: revoke with
POST /verify/passkey/factors/{factorId}/revoke(idempotent — a second call returns the same response).
otpauth:// URI, plaintext backup codes) are returned once at creation;
if you lost them, the only clean path is re-enrollment.
What not to do
- Do not retry a factor challenge unchanged.
CHALLENGE_TERMINAL,CHALLENGE_EXPIRED, andFACTOR_REVOKEDare state, not rate limits — issue a fresh challenge or re-enroll. - Do not loosen the RP-id / origin to make a failing ceremony pass. The origin cross-check is the phishing resistance; a loosened check is a security regression, not a fix.
- Do not disable the passkey counter regression revoke. That signal means the credential was cloned; re-register the credential and tell the user.
- Do not treat the 30-minute
/checklockout or recipient rate limits as factor problems. Those belong to OTP sends — see Verify OTP.
Escalation packet
If a factor still fails after you have worked the cause table for its type, email support@devotel.io with:- Your tenant id (returned as
organizationIdbyGET /api/v1/me, and shown in the dashboard under Settings → Organization). - The factor id (
vftotp_…for TOTP,vfbc_…for backup codes,vpf_…for push,vpk_…for passkeys) of the affected enrollment. - The challenge id (
vpc_…for push,vpkc_…for passkeys) of one failing ceremony. - The error code and
details.reasonfrom the response envelope, verbatim.
See also
- Verify overview: MFA factors — the full factor suite contract: endpoints, scope requirements, and enrollment flow.
- Verify Push API reference and Passkey API reference — every endpoint, challenge TTL bounds, and response shapes.
- Troubleshoot Verify OTP sends and checks — the OTP send/check symptom map this page explicitly does not cover.
- Secure factor events — the factor-event
catalog (
totp.create,backup_code.verify,push.factor.verify,passkey.challenge.verify) for driving recovery from your own systems.