One-time recovery codes generated in a batch, for when a user's primary second factor — a TOTP app, an SMS-registered phone — isn't available. Generation follows the same step-up shape as TOTP; verification is the one real exception.
await auth.login("user@example.com"); // establish a session first
const { codes } = await auth.generateBackupCodes();
// 10 codes, shown to you exactly once — store them nowhere else,
// display them once for the user to save.
Generating a new batch immediately invalidates every previously-generated code for that user — this matches how GitHub and Google handle backup-code regeneration. There's no way to view a previously-generated batch again; if the user lost them, generate a fresh set.
// No session required — this IS a login, the account-recovery path.
const user = await auth.verifyBackupCode("user@example.com", "ABCDE-FGHJK");
// or: auth.verifyBackupCode({ externalUserId: "your_user_123" }, "ABCDE-FGHJK")
Unlike enrollTotp()/verifyTotp()/sendSmsOtp(), verifyBackupCode() does not require an existing session — the user may have none, which is the entire point of a recovery code. A successful call establishes a brand-new session and fires onAfterAuth, exactly like verifyOtp() does. It is deliberately not part of the automatic register()/login() cascade — a backup code only exists if the user already saved one, and silently checking "is this a valid backup code?" during every ordinary login attempt would be surprising, unrequested behavior. Call this directly from a "Use a backup code instead" path you build.
0/O, 1/I/L) — roughly 50 bits of entropy per code.XXXXX-XXXXX. Entry is tolerant of formatting — dashes, spacing, and case are all normalized server-side, so however the user types it back works.None of these are currently configurable.
verifyBackupCode() returns invalid_code for a wrong or already-used code, and rate_limited past the 15-minute attempt limit. See the Error Reference for details.