<!-- section: Auth API · status: building · source: docs/auth-api/07-two-factor.md -->

> TOTP and single-use recovery codes. Enable, verify, and what sign-in returns once it is on.


# Two-factor

TOTP and recovery codes. **No SMS** — it costs money per message, is the weakest
common second factor, and its main appeal is familiarity rather than security.

All three calls take a **publishable** key and a session cookie.

## POST /v1/auth/two-factor/enable

```bash Request
curl -X POST "$BASE/v1/auth/two-factor/enable" \
  -H "authorization: Bearer $PK" \
  -H "content-type: application/json" \
  -b "session cookie" \
  -d '{"password":"their-current-password"}'
```

```json Response
{
  "totpURI": "otpauth://totp/Acme:ada@example.com?secret=…&issuer=Acme",
  "backupCodes": ["3f2a-9c11", "…"]
}
```

Render `totpURI` as a QR code. **The issuer in the authenticator app is your
project's name**, so somebody adding a code for their Acme account sees Acme.

The ten backup codes are single-use and shown once. They are the answer to a lost
phone, so tell the person to keep them somewhere that is not the phone.

The current password is required: enabling a second factor on a session someone
walked away from is how you lock a person out of their own account.

## POST /v1/auth/two-factor/verify-totp

Finishes enabling it, and finishes every sign-in afterwards.

```bash Request
curl -X POST "$BASE/v1/auth/two-factor/verify-totp" \
  -H "authorization: Bearer $PK" \
  -H "content-type: application/json" \
  -b "session cookie" \
  -d '{"code":"492013"}'
```

```json Response
{ "token": "…", "user": { "id": "Nrzmwy…", "email": "ada@example.com" } }
```

This call is what returns the session on a two-factor sign-in.

## What sign-in returns once it is on

[Sign in](/docs/auth-api/sign-in) stops returning a session and returns this:

```json
{ "twoFactorRedirect": true, "twoFactorMethods": ["totp"] }
```

Send them to your second-factor screen and call `verify-totp` with the six
digits.

## POST /v1/auth/two-factor/disable

Takes the password again, for the same reason enabling does.

## Where the secrets live

**The TOTP secret and the backup codes are encrypted at rest**, under your
project's own signing secret — a key no other project holds. A dump of one
project's database is useless against another's.

## Errors

| Status | Code | Means |
|---|---|---|
| `401` | `INVALID_PASSWORD` | Wrong current password on enable or disable |
| `401` | `INVALID_TWO_FACTOR_CODE` | Wrong or expired six digits |
| `400` | `TWO_FACTOR_NOT_ENABLED` | Verifying against an account that never enabled it |
| `429` | — | Rate limited; see [limits](/docs/auth-api/limits) |
