PayinferenceDocs
payinference.com

Retry integration

When a payment attempt fails, the question is whether trying again is worth anything, and where. PayInference answers it per attempt, using failure classification, provider health, and your retry policy.

The loop#

  1. First attempt fails (timeout, soft decline, technical error).
  2. Report the outcome for the failed attempt to POST /v1/outcomes.
  3. Request a new decision for the same order_id, now including previous_attempt.
  4. Execute the new instruction: retry, failover, use_default_route, or block.
ts
// Attempt 1 failed with a timeout on stripe.
await payinference.recordOutcome({
  decision_id: firstDecision.decision_id,
  order_id: 'order_456',
  provider_used: 'stripe',
  outcome: 'timeout',
  provider_latency_ms: 5000,
  amount: 14900,
  currency: 'USD',
});

// Ask again with the failure context.
const second = await payinference.decide({
  order_id: 'order_456', // same order_id ties the attempts together
  transaction: { amount: 14900, currency: 'USD', country: 'US', payment_method: 'card' },
  available_providers: ['stripe', 'adyen', 'paypal'],
  previous_attempt: {
    provider: 'stripe',
    outcome: 'timeout',
    provider_reason_code: 'gateway_timeout',
  },
});

previous_attempt.outcome accepts timeout, declined, or error.

What comes back#

Instruction Meaning for the retry
retry The failure was technical (timeout or error), your policy has a retry_on_timeout rule, and the failed provider still looks healthy. Execute on route.primary_provider (RETRY_ALLOWED_BY_POLICY)
failover The failed provider itself is unhealthy (degraded, critical, or unavailable); execute on the different provider in route.primary_provider (FAILOVER_FROM_UNHEALTHY_PROVIDER). Needs no retry rule
route No retry rule applied (for example the previous outcome was declined with no no_retry match); a fresh routing decision was made
use_default_route No provider survived health and policy filtering, but your policy default applies (NO_HEALTHY_PROVIDER_AVAILABLE, DEFAULT_ROUTE_APPLIED)
block Retrying is suppressed: the provider_reason_code matched a policy no_retry list (RETRY_SUPPRESSED_BY_POLICY), or no eligible provider and no default exists

Two behaviors worth knowing:

  • With use_different_provider on your retry rule (the default), the provider from previous_attempt is excluded from scoring whenever the failure was technical, so retry and failover typically route to a different provider than the one that failed.
  • retry and failover fire on technical failures only. A declined previous attempt does not trigger them; declines either produce a normal route decision or a block when the decline code is on a no_retry list. That is exactly why reporting soft_declined vs hard_declined outcomes and passing provider_reason_code matters.

Policy controls#

Two policy actions govern retries (see Policy Manager):

json
{
  "rule_id": "retry_timeouts_once",
  "name": "Retry timeouts on a different provider",
  "priority": 10,
  "enabled": true,
  "when": {},
  "then": { "type": "retry_on_timeout", "max_retries": 1, "use_different_provider": true }
}
json
{
  "rule_id": "never_retry_fatal",
  "name": "Never retry fatal decline codes",
  "priority": 5,
  "enabled": true,
  "when": {},
  "then": {
    "type": "no_retry",
    "provider_reason_codes": ["stolen_card", "pickup_card", "fraudulent"]
  }
}

max_retries caps at 3. no_retry matches on the raw provider_reason_code you pass in previous_attempt and in outcomes.

Why outcome classification matters here#

Retry quality depends on the difference between soft_declined (retry may succeed: issuer unavailable, insufficient funds) and hard_declined (retry never succeeds: stolen card, closed account). Report the canonical outcome values rather than the legacy declined, and always pass the raw provider_reason_code. See Outcome feedback.

Guardrails#

  • Bound your loop. Policy caps retries, but keep your own attempt counter per order_id as defense in depth.
  • One decision per attempt. Do not reuse a prior decision for a retry; the situation changed, ask again.
  • Report every attempt's outcome, including the final one, so the decision log shows the full story of the order.
  • Do not retry HTTP errors from PayInference itself. A 4xx is an answer. See Idempotency and retries.