Outcome feedback
After your backend executes an instruction, report what actually happened. One call closes the loop:
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_declinevshard_declinevstechnical) 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#
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#
{
"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.