PayinferenceDocs
payinference.com

Outcome feedback

After your backend executes an instruction, report what actually happened. One call closes the loop:

text
POST /v1/outcomes

Requires the outcome:write scope.

Why outcomes matter#

Outcome feedback is how PayInference learns which decisions worked. Concretely, it powers:

  • Provider intelligence. Approval rates, latency, timeout and error rates per provider segment come from your outcomes plus synthetic probes. Missing outcomes mean health scores lean on probes alone and carry lower confidence.
  • Retry intelligence. Failure classification (soft_decline vs hard_decline vs technical) determines whether a retry is worth attempting for future decisions.
  • Observability. The decision log joins each decision to its outcome, which is how you and PayInference evaluate whether routing choices paid off.
  • Policy evaluation. Approval and failure rates per rule show whether a policy helps or hurts.

Report outcomes for every executed decision, including blocked ones. Fire and forget is fine; the call is idempotent per decision_id.

The request#

bash
curl -s http://localhost:4000/v1/outcomes \
  -H "content-type: application/json" \
  -H "x-api-key: $PAYINFERENCE_API_KEY" \
  -d '{
    "decision_id": "dec_9f3a1c0b22d14e55",
    "order_id": "order_456",
    "provider_used": "adyen",
    "outcome": "approved",
    "provider_latency_ms": 842,
    "provider_reason_code": "approved",
    "amount": 14900,
    "currency": "USD"
  }'

Field tables are in the API reference.

Outcome values#

Use the canonical categories. They describe what your provider told you, not what PayInference decided.

Outcome Report when
approved The provider approved the payment
soft_declined Declined in a way that may succeed on retry (for example insufficient_funds, issuer unavailable)
hard_declined Declined definitively (for example stolen_card, do_not_honor variants that never recover)
technical_failed The attempt failed technically: 5xx, connection error, malformed response
timeout The provider did not answer within your budget
rate_limited The provider throttled you (HTTP 429)
blocked You did not submit the payment, usually because the decision was block
step_up_completed The required step-up control (3DS, SCA, review) completed successfully
step_up_failed The customer or agent failed or abandoned the step-up control

Legacy aliases declined, error, and cancelled are still accepted and normalized server-side, but new integrations should send the canonical values: the split between soft and hard declines directly improves retry decisions.

For technical failures you can add technical_failure_type (timeout, 5xx, connection, rate_limited, unknown); when omitted it is derived from the outcome and reason code.

The response#

json
{
  "outcome_id": "out_7c1d92",
  "decision_id": "dec_9f3a1c0b22d14e55",
  "outcome": "approved",
  "failure_class": "none",
  "technical_failure_type": null,
  "approved": true
}

failure_class is PayInference's normalization of the failure (technical, soft_decline, hard_decline, fraud_block, or none) and is what retry intelligence consumes.

Idempotency and corrections#

Outcomes upsert by decision_id: one decision holds at most one outcome, and reporting again updates it. If a result changes after the fact, for example a timeout that your PSP later resolves to approved, send the corrected outcome with the same decision_id.

Common integration mistakes#

  • Reporting only successes. Failure outcomes are the valuable half; without them provider health and retry logic cannot see problems.
  • Blocking the checkout response on the outcome call. Report asynchronously. The Node SDK's recordOutcome() has its own generous timeout and network-error retries for exactly this.
  • Inventing outcome strings. Unknown values fail validation. Map your PSP's response codes onto the table above and pass the raw code in provider_reason_code (64 chars max) for diagnostics.