📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Guides
Step-up auth on high-value transfers

Step-up auth on high-value transfers

Re-verify a customer's biometric identity at the moment of a sensitive action — high-value transfer, withdraw-to-bank, password reset, new-device login.

The shape

Customer initiates sensitive action (e.g. transfer > IDR 10M)


1. Backend: POST /api/identity/face/flash-challenge
     │   pass challenge to client SDK

2. Client SDK: captures live selfie under flash challenge


3. Backend: POST /api/identity/auth/step-up
   {
     "customerId": "...",
     "riskLevel": "high",
     "actionLabel": "Withdraw IDR 50,000,000 to BCA 1234567890",
     "trigger": "high_value_transfer",
     "bundle": { /* signed bundle from SDK */ }
   }

     ├─ allow = true → proceed with action, attach verification.id to txn record
     └─ allow = false → reject; surface "biometric verification failed" to user

Threshold tuning per action

riskLevel drives both the match threshold and the freshness gate.

ActionriskLevelThreshold (default)Freshness gate (default)
View transaction history(none)n/an/a
Update phone numberlow0.45none
Transfer ≤ IDR 5Mmedium0.45none
Transfer ≤ IDR 50Mhigh0.5560 min
Withdraw to bank (any amount)high0.5560 min
Change passwordhigh0.5560 min
First-time login from new devicemedium0.45none

Per-org override these from the dashboard's Identity Settings → Risk Tiers page.

Reusing a fresh capture across multiple actions

A single liveness + match is valid for the configured freshness window (default 60 min). Within that window, you can reference the verification ID directly without re-running the bundle:

// First action — captures a fresh bundle
const stepUp = await qe.identity.authStepUp({
  customerId, riskLevel: 'high', actionLabel: 'transfer 50M',
  trigger: 'high_value_transfer', bundle,
});
const verificationId = stepUp.verification.id;
// commit transfer with verificationId on the record
 
// Second action within 60 min — no new bundle needed
const isValid = await qe.identity.verifications.checkFreshness({
  verificationId,
  maxAgeMinutes: 60,
});
if (isValid) {
  // proceed with second action; same verificationId on the record
}

This is the "logged-in session with biometric anchor" pattern — a fresh face match holds for an hour, then customer re-prompts on the next sensitive action.

Combining with anti-fraud

The strongest pattern stacks biometric step-up + anti-fraud evaluate:

// 1. Step-up biometric
const stepUp = await qe.identity.authStepUp({ ... });
if (!stepUp.allow) return reject('biometric_failed');
 
// 2. Anti-fraud evaluate (lane=transaction)
const fraud = await qe.antiFraud.evaluate({
  customerId,
  lane: 'transaction',
  externalId: txn.id,
  amount: txn.amount,
  currency: 'IDR',
  data: {
    channel: 'transfer',
    beneficiaryName: txn.beneficiary.name,
    beneficiaryAccount: txn.beneficiary.account,
    ipAddress: req.ip,
  },
});
if (fraud.decision === 'block') return reject('fraud_block');
if (fraud.decision === 'review') return hold('fraud_review');
 
// 3. Commit transaction
await commitTransfer(txn);

The anti-fraud signal incorporates device + velocity + counterparty risk; step-up confirms the person is who they claim. Together: the right human, doing the right thing, in the right context.

Failure UX

When allow: false, look at the response to guide the user:

FailurePossible UX
liveness.verdict = spoof"Please try again in better lighting — make sure your face is the only thing on screen."
liveness.verdict = unclear"We couldn't process your video. Please try again."
match.verdict = no_match"Identity verification failed. Contact support."
match.verdict = grey_zoneSurface as "needs review" not failure — queue for manual approval.

Always offer one retry. Lock out on second failure to prevent enumeration of the threshold.

Audit & compliance

Every step-up call writes an audit row with riskLevel, trigger, actionLabel, full liveness + match results. For OJK / BI examiner reviews, this is your "biometric authentication evidence" for any disputed transaction. Pull via:

GET /api/identity/attempts?method=face_match&customerId=cus_...&from=2026-01-01&to=2026-12-31

Export to your audit warehouse quarterly.