TagPayTagPay

API Reference

Two-Factor Authentication

Two-factor authentication (2FA) endpoints for the dashboard. Enrollment is a two-step flow (`initiate` → `confirm`); subsequent logins go through `verify`. The merchant-wide enforcement policy is a separate, owner-only toggle that requires every active team member to enroll within a 7-day grace window.

Start 2FA enrollment

POST/v1/auth/2fa/initiate

Begins the enrollment flow for the authenticated user. Authenticated with a JWT Bearer token (not a secret API key).

For method=totp, the response includes the base32 secret and a pre-rendered qr_code data URL — show the QR to the user, ask them to scan it in their authenticator app, then call /v1/auth/2fa/confirm with the first 6-digit code.

For method=email, a 6-digit code is emailed to the user (10-minute TTL) and the response carries only a masked sent_to address. The same /v1/auth/2fa/confirm endpoint finishes the flow.

two_factor_enabled stays false until confirm succeeds — calling initiate alone does not lock the user out.

Body parameters

methodenum<'totp' | 'email'>required

totp mints a persistent secret for an authenticator app (Google Authenticator, Authy, 1Password). email sends a fresh 6-digit code to the user's email on every login.

Response

200
methodenum<'totp' | 'email'>

Echoed enrollment method.

secretstring

Base32 TOTP secret. **Only present when `method=totp`.** Returned once — show in QR + manual entry, then discard client-side.

qr_codestring

Data URL (`data:image/png;base64,…`) of the otpauth QR code. **Only present when `method=totp`.**

sent_tostring

Masked email address the setup OTP was sent to (e.g. `j***@example.com`). **Only present when `method=email`.**

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.
ALREADY_ENABLED4002FA is already enabled. Disable it first to re-enroll.

Confirm 2FA enrollment

POST/v1/auth/2fa/confirm

Verifies the first 6-digit code against the pending secret saved by /v1/auth/2fa/initiate. Authenticated with a JWT Bearer token.

On success: flips two_factor_enabled to true, stamps two_factor_enrolled_at, and returns ten one-time backup codes. The codes are returned only here — the database holds bcrypt hashes. Lose them and the user has to call /v1/auth/2fa/backup-codes/regenerate.

Body parameters

codestringrequired

The 6-digit TOTP code from the authenticator app, or the 6-digit OTP from the setup email.

Response

200
methodenum<'totp' | 'email'>

Method that was confirmed.

backup_codesarray<string>

Ten single-use backup codes. Format `XXXXX-XXXXX`. Display once, then never again.

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.
INVALID_CODE401The submitted code does not match the pending secret/OTP.
ALREADY_ENABLED4002FA is already enabled.
NOT_STARTED400Call /v1/auth/2fa/initiate first.

Complete login with 2FA

POST/v1/auth/2fa/verify

Used during login. When /v1/auth/login returns requires_two_factor: true, it also returns a short-lived partial token. Send that partial token as the Authorization: Bearer … header to this endpoint together with one of:

- a 6-digit TOTP code (TOTP-method users) - a 6-digit email OTP (email-method users — a fresh OTP was emailed at login) - a single-use backup_code (works regardless of method)

On success the response sets the full session via httpOnly cookies and the partial token is replaced by full access/refresh tokens. Exactly one of code and backup_code must be supplied.

Body parameters

codestringoptional

6-digit TOTP code or email OTP. Required when backup_code is not provided.

backup_codestringoptional

Single-use backup code (format XXXXX-XXXXX). Required when code is not provided.

Response

200
access_tokenstring

Short-lived JWT access token.

refresh_tokenstring

Long-lived refresh token (also set as an httpOnly cookie).

expires_ininteger

Access-token lifetime in seconds.

Errors

INVALID_TOKEN401Partial 2FA token missing, expired, or invalid.
INVALID_CODE401Submitted code or backup code did not match.
CODE_REQUIRED400Either `code` or `backup_code` must be supplied.
OTP_EXPIRED401Email OTP expired — request a new one via /v1/auth/2fa/resend.
TOO_MANY_ATTEMPTS401Email OTP attempt limit exceeded — request a new code.
NOT_CONFIGURED4002FA is not configured for this account.

Resend email-OTP login code

POST/v1/auth/2fa/resend

Re-issues a fresh 6-digit code to the user's email and starts a new 10-minute window. Authenticated with the partial 2FA token (same Bearer token used for /v1/auth/2fa/verify).

Rate-limited to one request per 60 seconds per user. Only meaningful for users enrolled with the email method — TOTP users get a 400 (their authenticator app already generates codes on demand).

Response

200
sent_tostring

Masked email address the new OTP was sent to.

Errors

INVALID_TOKEN401Partial 2FA token missing, expired, or invalid.
NOT_EMAIL_METHOD400Email-based 2FA is not enabled for this account.
RESEND_COOLDOWN400Wait 60 seconds before requesting another code.

Disable 2FA

POST/v1/auth/2fa/disable

Requires re-authentication: the current password plus either a current code (TOTP or email OTP) or an unused backup_code. Authenticated with a JWT Bearer token.

The handler clears the secret, method, enabled flag, enrolled timestamp, and every backup code in a single transaction — the user can never land in a half-disabled state. Exactly one of code and backup_code must be supplied.

Body parameters

passwordstringrequired

Current account password.

codestringoptional

6-digit TOTP code or email OTP. Required when backup_code is not provided.

backup_codestringoptional

Single-use backup code. Required when code is not provided.

Response

200
disabledboolean

Always `true` on success.

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.
INVALID_PASSWORD401Password is incorrect.
INVALID_CODE401TOTP code, email OTP, or backup code did not match.
CODE_REQUIRED400Either `code` or `backup_code` must be supplied.
NOT_ENABLED4002FA is not currently enabled.

Get 2FA status

GET/v1/auth/2fa/status

Returns the 2FA state for the authenticated user — used by the dashboard's security page to render "Enabled / Disabled" and the unused-backup-code count. Authenticated with a JWT Bearer token.

Response

200
enabledboolean

Whether 2FA is enabled for this user.

methodenum<'totp' | 'email'> | null

Enrolled method, or `null` when `enabled=false`.

enrolled_atstring | null

ISO-8601 timestamp of when 2FA was confirmed, or `null` when `enabled=false`.

unused_backup_codesinteger

Count of backup codes that have not been consumed yet. `0` when 2FA is disabled.

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.

Regenerate backup codes

POST/v1/auth/2fa/backup-codes/regenerate

Replaces the existing backup-code set with ten freshly-minted codes. The previous set is invalidated atomically in the same transaction. Authenticated with a JWT Bearer token.

Requires either a current code (TOTP or email OTP) or an unused backup_code — a stolen access token alone cannot rotate backup codes for persistent access. Exactly one must be supplied.

Body parameters

codestringoptional

6-digit TOTP code or email OTP. Required when backup_code is not provided.

backup_codestringoptional

Single-use backup code. Required when code is not provided.

Response

200
methodenum<'totp' | 'email'>

The user's current 2FA method.

backup_codesarray<string>

Ten freshly-minted single-use codes. **Returned only here** — store immediately.

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.
INVALID_CODE401TOTP code, email OTP, or backup code did not match.
CODE_REQUIRED400Either `code` or `backup_code` must be supplied.
NOT_ENABLED4002FA is not currently enabled.

Set merchant-wide 2FA policy

PATCH/v1/auth/merchant/policy/two-factor

Toggles mandatory 2FA at the merchant level. Owner only — non-owner callers get 403. Authenticated with a JWT Bearer token and merchant_profile:write permission.

When enabled=true:

- The owner must already have 2FA enabled (else 400) — otherwise they'd lock themselves out the moment enforcement kicks in. - A 7-day grace period begins. During the grace window non-enrolled team members can still sign in (the dashboard renders a banner). After the deadline, sign-in is blocked until the user enrolls. - Every non-enrolled active team member is emailed a grace-period notification with an enrollment link.

When enabled=false: the policy is removed and grace_period_ends_at is cleared. Distinct from per-user 2FA — disabling the policy does not turn off 2FA for anyone who has it on.

Body parameters

enabledbooleanrequired

When true, all active team members must have 2FA enabled to sign in once the 7-day grace period elapses.

Response

200
requires_two_factorboolean

New policy value.

grace_period_ends_atstring | null

ISO-8601 deadline. `null` when `enabled=false`.

Errors

INVALID_TOKEN401JWT missing, expired, or invalid.
FORBIDDEN403Only the account owner can change the 2FA policy.
OWNER_NOT_ENROLLED400You must enable 2FA on your own account before enforcing it for the team.