PayinferenceDocs
payinference.com

Step-up integration

step_up means: require an additional control before continuing. PayInference decides that a control is needed; you choose and run the control, then report how it went.

What the control is depends on your business: 3DS or SCA for cards, KYC for a new account, manual review, human approval, or agent approval in agent-driven systems.

When step_up is returned#

step_up is returned when a policy rule with a step_up action matched: reason code STEP_UP_REQUIRED_BY_POLICY. Rule conditions can combine amount, region, payment method, customer type, and the risk_score or risk_level you supply in the transaction, so your existing fraud tooling drives step-ups through a rule like when: { risk_score_gte: 60 }, then: { type: "step_up" }.

The deterministic risk object in the response is advisory: when the band is high its recommended_action is step_up and the top-level reason codes include RISK_ELEVATED, but the assessment by itself does not change the instruction. If you want it enforced, express the threshold as a policy rule.

json
{
  "decision_id": "dec_03c9e4",
  "action": "step_up",
  "route": { "primary_provider": "adyen", "fallback_provider": null },
  "risk": {
    "score": 74,
    "band": "high",
    "recommended_action": "step_up",
    "reason_codes": ["ELEVATED_AMOUNT", "NEW_CUSTOMER_HIGH_AMOUNT", "HIGH_VELOCITY_10M"]
  },
  "reason_codes": ["STEP_UP_REQUIRED_BY_POLICY", "RISK_ELEVATED"]
}

route is still populated: it tells you where to execute once the control passes.

The flow with 3DS as the control#

ts
const decision = await payinference.decide(params);

if (decision.action === 'step_up') {
  // 1. Run your control. For cards this is typically a 3DS challenge
  //    through your PSP's own SDK.
  const challenge = await startThreeDSChallenge(order, decision.route?.primary_provider);

  if (challenge.completed) {
    // 2a. Report the control result.
    await payinference.recordOutcome({
      decision_id: decision.decision_id,
      order_id: order.id,
      provider_used: decision.route!.primary_provider,
      outcome: 'step_up_completed',
      provider_latency_ms: challenge.durationMs,
      amount: order.amount,
      currency: order.currency,
    });

    // 3a. Execute the payment on the routed provider, with the 3DS proof
    //     attached the way your PSP expects.
    return chargeWithProvider(decision.route!.primary_provider, order, challenge.proof);
  }

  // 2b. The customer failed or abandoned the challenge.
  await payinference.recordOutcome({
    decision_id: decision.decision_id,
    order_id: order.id,
    provider_used: decision.route!.primary_provider,
    outcome: 'step_up_failed',
    provider_latency_ms: challenge.durationMs,
    amount: order.amount,
    currency: order.currency,
  });
  return rejectPayment(order, decision.decision_id);
}

For asynchronous pauses (manual review, human approval, compliance checks) prefer the dedicated hold instruction, which policy can demand with a hold rule; see Hold and review integration. step_up is for interactive controls the customer or agent completes in the flow.

Defining step-up rules#

json
{
  "rule_id": "step_up_high_value_new",
  "name": "Step up new customers above 500",
  "priority": 10,
  "enabled": true,
  "when": { "amount_gte": 50000, "customer_type_in": ["new", "guest"] },
  "then": { "type": "step_up" }
}

Conditions can combine amount, risk score, region, payment method, and more. See Policy Manager.

Why report step-up outcomes#

step_up_completed and step_up_failed are first-class outcome values. Reporting them tells you (and PayInference's analytics) how much friction your controls add and how often stepped-up payments convert, which is exactly the data you need to tune the thresholds in your policy.