Decision instructions
Every decision response carries exactly one instruction in the action field. Your backend executes it through your existing payment stack. This page defines each instruction, when it is returned, and how to handle it.
Two words you will not see as instructions: approve and decline. PayInference returns an instruction before submission. Issuers and PSPs approve or decline after you submit. When PayInference wants a payment stopped, the instruction is block.
| Action | Meaning |
|---|---|
route |
Send the payment through the selected provider in route.primary_provider |
use_default_route |
Continue through your default payment path |
step_up |
Require an additional control before continuing (3DS, SCA, review) |
hold |
Pause the payment in a pending state until review or approval completes |
retry |
Try the payment again under retry policy |
failover |
Retry on a different provider than the one that failed |
block |
Stop the transaction from being submitted |
How the instruction is chosen#
The engine resolves the action in a fixed order, so the sections below are also the precedence rules:
- A matched policy
blockrule is terminal: the decision isblockand nothing can un-block it. - A matched policy
holdrule returnshold(the route is still computed and populated). Hold outranks step_up: a pause is stronger than a control. - A matched policy
step_uprule returnsstep_up(the route is still computed and populated). - If
previous_attemptis present: ano_retrymatch returnsblock; a technical failure (timeoutorerror) on an unhealthy provider returnsfailoveron the next-ranked eligible provider โ the failed provider is excluded from ranking, and if no other provider is eligible, step 5 applies instead; a technical failure with aretry_on_timeoutrule returnsretry. - Otherwise the decision is
routewhen an eligible provider exists,use_default_routewhen none does but your policy names a default, andblockwhen neither holds.
Policy rules drive the action. The deterministic risk object in every response (score, band, recommended_action) is advisory evidence for your logs and review flows; it never changes the instruction by itself. To make risk change the instruction, put a rule on it, for example when: { risk_score_gte: 70 }, then: { type: "step_up" } against the risk_score you supply.
route#
Send the payment through the selected provider. route.primary_provider is the choice; route.fallback_provider is the pre-approved second option if your call to the primary fails technically.
This is the normal instruction for a first attempt: at least one provider passed health and policy filtering, and the engine picked the best-scoring one.
{
"decision_id": "dec_01a7c2",
"action": "route",
"route": { "primary_provider": "adyen", "fallback_provider": "stripe" },
"reason_codes": ["ADYEN_HEALTHY_FOR_SEGMENT", "HIGHER_EXPECTED_APPROVAL_RATE"]
}
Handle it by creating the payment on route.primary_provider with your own PSP SDK and credentials.
use_default_route#
Continue through your default payment path. This is the "proceed" instruction, and it is a last resort, not the happy path: it is returned when no provider survived health and policy filtering (every candidate was excluded as critical, unavailable, or ruled out by your policy) but your published policy defines a default_provider that is in your available_providers. Rather than blocking a payment for operational reasons, PayInference tells you to continue on your configured default.
{
"decision_id": "dec_02b8d3",
"action": "use_default_route",
"route": { "primary_provider": "stripe", "fallback_provider": null },
"reason_codes": ["NO_HEALTHY_PROVIDER_AVAILABLE", "DEFAULT_ROUTE_APPLIED"]
}
route.primary_provider names the default so your handler code stays uniform; route.fallback_provider is null in this case. The Node SDK's local fallback decision also uses this action (with SDK_FALLBACK_USED in the reason codes) when PayInference itself is unreachable.
If no eligible provider exists and no default is configured, the decision is block with NO_HEALTHY_PROVIDER_AVAILABLE.
step_up#
Require an additional control before continuing. What the control is belongs to you and your policy: 3DS, SCA, KYC, manual review, human approval, or agent approval. PayInference does not run the control; it tells you one is required.
Returned when a policy rule with a step_up action matched (reason code STEP_UP_REQUIRED_BY_POLICY). Rule conditions can key off amount, region, payment method, customer type, and the risk_score or risk_level you supply in the transaction, so "step up when my fraud tool scores above 60" is a one-rule policy. The deterministic risk object in the response is advisory evidence; it does not itself flip the action.
{
"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"]
}
After the control completes, report step_up_completed or step_up_failed as the outcome, then proceed or stop accordingly. See Step-up integration.
retry#
Try the payment again under a defined retry policy. Returned when the previous_attempt you sent failed technically (timeout or error), your policy has a retry_on_timeout rule, and the failed provider itself still looks healthy.
{
"decision_id": "dec_04d0f5",
"action": "retry",
"route": { "primary_provider": "adyen", "fallback_provider": "paypal" },
"reason_codes": ["RETRY_ALLOWED_BY_POLICY"]
}
With use_different_provider (the default for retry rules), the provider that just failed is excluded from scoring, so route.primary_provider usually names a different provider than the failed one. Policies can also suppress retries for specific provider reason codes (RETRY_SUPPRESSED_BY_POLICY), in which case the decision is block. See Retry integration.
failover#
Retry on a different provider than the one that just failed. This is the retry variant used when the failed provider itself is the problem: its cached health state is degraded, critical, or unavailable at decision time. Unlike retry, it does not require a retry rule in your policy; a technical failure on an unhealthy provider triggers it directly.
Two guarantees distinguish it from the other route-carrying instructions:
route.primary_provideris never the provider that failed. The failed provider is excluded from ranking before selection โ regardless of your retry rule'suse_different_providersetting, which governs plainretryonly โ so the failover route is the next-ranked eligible provider (the second-ranked overall, or the third and beyond when intermediate candidates are themselves excluded by health or policy).- If excluding the failed provider leaves no eligible provider, the decision is not a same-provider "failover": it falls through to
use_default_routewhen your policy names a default (your explicit operational escape hatch, even if that default is the failed provider), orblockwithNO_HEALTHY_PROVIDER_AVAILABLEwhen it does not.
That is also the line between failover and use_default_route: failover is ranking-driven (the engine picked the best remaining candidate mid-payment), while use_default_route is config-driven (nothing survived ranking, so you continue on the path you configured).
{
"decision_id": "dec_05e1a6",
"action": "failover",
"route": { "primary_provider": "checkout", "fallback_provider": null },
"reason_codes": ["FAILOVER_FROM_UNHEALTHY_PROVIDER", "CHECKOUT_HEALTHY_FOR_SEGMENT"]
}
Handle it exactly like route: execute on route.primary_provider.
block#
Stop the transaction from being submitted. route is null. There are three triggers, each with a distinct reason code:
| Trigger | Reason code |
|---|---|
A policy rule with a block action matched (conditions can include your supplied risk_score, amount, region, and so on) |
RISK_SCORE_ABOVE_BLOCK_THRESHOLD |
A retry was suppressed because previous_attempt.provider_reason_code is on a policy no_retry list |
RETRY_SUPPRESSED_BY_POLICY |
| No eligible provider survived filtering and no default provider is configured | NO_HEALTHY_PROVIDER_AVAILABLE |
{
"decision_id": "dec_06f2b7",
"action": "block",
"route": null,
"risk": {
"score": 91,
"band": "extreme",
"recommended_action": "block_review",
"reason_codes": ["MERCHANT_SUPPLIED_HIGH_RISK", "REPEATED_DECLINES", "IP_COUNTRY_MISMATCH"]
},
"reason_codes": ["RISK_SCORE_ABOVE_BLOCK_THRESHOLD", "RISK_HIGH_BLOCK_REVIEW"]
}
Do not submit the payment. Log decision_id and surface an appropriate message to your customer. If you keep a review queue, risk.recommended_action of block_review signals the case is worth a human look. Report the outcome as blocked.
hold#
Pause the transaction and place it in a pending state until a review, approval, compliance check, or other policy condition completes. Unlike step_up (an interactive control the customer or agent completes now), hold means the payment waits in your systems until someone or something releases it.
Returned when a policy rule with a hold action matched (reason code HOLD_REQUIRED_BY_POLICY).
{
"decision_id": "dec_07a3c8",
"action": "hold",
"route": { "primary_provider": "adyen", "fallback_provider": "stripe" },
"reason_codes": ["HOLD_REQUIRED_BY_POLICY"]
}
route is still populated: it names where to execute once the hold is released. Park the order in your own pending state, run your review, then either execute on route.primary_provider (respecting ttl_ms: if it has elapsed, request a fresh decision first) or stop and report the outcome as blocked. See Hold and review integration.
Handling unknown instructions#
New instructions may be added over time. Treat an unrecognized action as your merchant-defined safe default rather than throwing in the payment path:
default:
logUnknownAction(decision);
return chargeWithDefaultProvider(order);