How Partner 2FA Works
Understand the objects and state transitions before writing integration code.
On this page
Mental model
Identity (your user ↔ KEYRA)
│
├── Enrollment (usually once)
│
└── Authentication challenges (each login / step-up)
│
└── Consume (single-use verification token)Identity
KEYRA associates your application user with a KEYRA identity using an externalUserId you provide.
| Question | Answer (verified) |
|---|---|
Who generates externalUserId? | Your application. KEYRA does not generate it. |
| Should it be stable? | Yes. Changing it creates a different KEYRA identity mapping. |
| Can it be email? | Technically any string ≤ 128 characters. Prefer a stable opaque ID (UUID/integer). Using PII (email/phone) is discouraged for logging and portability reasons. |
| Where to store it? | In your user database — it is your primary key (or a dedicated field) you already own. |
What is identityId? | KEYRA-generated identifier (prefix kid_) returned after enrollment/status. |
Identity status values from the API: none, pending, active, suspended, revoked. SDKs expose enrolled / enabled as DX aliases when status is active.
Enrollment
Enrollment connects an existing application user with KEYRA verification. It is normally performed once per user (until disabled/revoked).
NOT ENROLLED (status: none)
↓
enable2FA(externalUserId)
↓
PENDING (enrollment session, expires in 600s)
↓
User opens enrollmentUrl / scans QR
↓
User completes verification on device
↓
COMPLETED (identity status: active)Authentication challenge
After enrollment, each login/step-up creates a short-lived challenge (default expiry 120 seconds).
ENROLLED USER (identity active)
↓
startAuthentication(externalUserId)
↓
PENDING challenge
↓
User opens challengeUrl / scans QR and verifies
↓
APPROVED (+ verificationToken on first poll)
↓
consumeChallenge → consumed
↓
YOUR application sessionChallenge statuses observed in implementation: pending, scanned, otp_sent, approved, denied, expired, consumed.
Consume
Challenge status: approved
│
│ verificationToken (returned once on poll)
▼
consumeChallenge(challengeId, verificationToken)
│
▼
{ consumed: true, identityId }
│
▼
YOUR application creates its own session
(KEYRA does not create your app session)- What is consumed: the one-time
verificationTokenfor achallengeId. - Why it exists: proves your backend — not only a browser — finalized the verification event.
- Single-use: second consume returns
already_consumed(HTTP 409). - Token lifetime: ~60 seconds after approval.
- What you receive:
{ consumed, challengeId, identityId }— not your application session. - When to create a session: only after
consumed: trueon your backend.
Who does what
| Actor | Responsibility |
|---|---|
| Browser / login UI | Collect primary credentials; display QR / deep link; never hold secret keys. |
| Your backend | Call Partner 2FA APIs with secrets; poll; consume; create/manage your application session. |
| KEYRA | Enrollment + challenge orchestration, user verification, verification tokens. |
| User device | Open enrollment/challenge URL and complete verification. |
Full login flow
YOUR LOGIN UI YOUR BACKEND KEYRA USER DEVICE
│ │ │ │
│ Credentials │ │ │
├───────────────────►│ │ │
│ │ get2FAStatus │ │
│ ├─────────────────►│ │
│ │ enrolled: true │ │
│ │◄─────────────────┤ │
│ │ startAuthentication │
│ ├─────────────────►│ │
│ │ challengeUrl │ │
│ │◄─────────────────┤ │
│ Show QR │ │ │
│◄───────────────────┤ │ │
│ │ │◄──── open/scan ─────┤
│ │ │──── user verifies ──►│
│ │ waitForChallengeApproval │
│ ├─────────────────►│ │
│ │ approved + token │ │
│ │◄─────────────────┤ │
│ │ consumeChallenge │ │
│ ├─────────────────►│ │
│ │ consumed: true │ │
│ │◄─────────────────┤ │
│ │ Create YOUR app session │
│ Authenticated │ │ │
│◄───────────────────┤ │ │