Go to developer

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.

QuestionAnswer (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 session

Challenge 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 verificationToken for a challengeId.
  • 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: true on your backend.

Who does what

ActorResponsibility
Browser / login UICollect primary credentials; display QR / deep link; never hold secret keys.
Your backendCall Partner 2FA APIs with secrets; poll; consume; create/manage your application session.
KEYRAEnrollment + challenge orchestration, user verification, verification tokens.
User deviceOpen 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      │                  │                     │
     │◄───────────────────┤                  │                     │
Enrolled user login with Partner 2FA