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 userThreshold tuning per action
riskLevel drives both the match threshold and the freshness gate.
| Action | riskLevel | Threshold (default) | Freshness gate (default) |
|---|---|---|---|
| View transaction history | (none) | n/a | n/a |
| Update phone number | low | 0.45 | none |
| Transfer ≤ IDR 5M | medium | 0.45 | none |
| Transfer ≤ IDR 50M | high | 0.55 | 60 min |
| Withdraw to bank (any amount) | high | 0.55 | 60 min |
| Change password | high | 0.55 | 60 min |
| First-time login from new device | medium | 0.45 | none |
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:
| Failure | Possible 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_zone | Surface 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-31Export to your audit warehouse quarterly.