Decision API
The Decision API is one synchronous endpoint. Your backend sends payment context, PayInference returns one instruction.
POST /v1/decision
- Authentication:
x-api-keyheader with a key that has thedecision:writescope. See Authentication. - Call it from your backend only. Never from the checkout browser: the API key is a secret, and the browser should never talk to PayInference directly.
- Designed for a sub-50 ms server-side decision budget.
The request#
{
"merchant_id": "m_123",
"order_id": "order_456",
"transaction": {
"amount": 14900,
"currency": "USD",
"country": "US",
"payment_method": "card",
"card_network": "visa",
"transaction_type": "authorization",
"customer_type": "returning",
"risk_score": 42
},
"available_providers": ["stripe", "adyen", "paypal"],
"risk_signals": {
"velocity_count_1h": 3,
"previous_success_count": 12,
"device_risk_band": "low",
"ip_country_match": true
}
}
Three rules shape the request:
- Safe fields only. The schema is strict. Unknown fields, including anything that looks like card data, cause a
400 validation_error. PayInference must never see a PAN, CVV, or email. - Amounts are minor units.
14900means $149.00. available_providersis what you can execute on. PayInference only routes among providers you actually hold credentials for.
Field-by-field tables are in the API reference.
Retry context#
When this is not the first attempt for an order, include previous_attempt so retry and failover policy can apply:
{
"previous_attempt": {
"provider": "stripe",
"outcome": "timeout",
"provider_reason_code": "gateway_timeout"
}
}
See Retry integration.
The response#
{
"decision_id": "dec_9f3a1c0b22d14e55",
"merchant_id": "m_123",
"mode": "live",
"decision_mode": "enforce",
"action": "route",
"route": { "primary_provider": "adyen", "fallback_provider": "stripe" },
"risk": {
"score": 0,
"band": "low",
"recommended_action": "route",
"reason_codes": [
"NORMAL_AMOUNT_FOR_MERCHANT",
"RETURNING_CUSTOMER",
"PRIOR_SUCCESSFUL_PAYMENTS",
"MERCHANT_SUPPLIED_ELEVATED_RISK"
]
},
"provider_health": {
"adyen": { "score": 94, "state": "healthy", "confidence": 91, "source": "probe" },
"stripe": { "score": 68, "state": "degraded", "confidence": 88, "source": "probe" }
},
"policy": {
"policy_id": "policy_m_123_v1",
"policy_version": "1.0.0",
"matched_rules": ["avoid_degraded_providers"]
},
"model": {
"model_version": "deterministic-v1",
"route_scores": { "adyen": 0.91, "stripe": 0.64 }
},
"cost": null,
"economics": null,
"reason_codes": ["ADYEN_HEALTHY_FOR_SEGMENT", "HIGHER_EXPECTED_APPROVAL_RATE", "RISK_ACCEPTABLE"],
"decision_latency_ms": 11.4,
"ttl_ms": 3000
}
What to read, in order of importance:
actionis the instruction. Execute it. Everything else is evidence.routenames the provider to use. It isnullwhen the action isblock.decision_idis the join key. Log it, and send it back with the outcome.reason_codesexplain the decision in stable machine-readable strings.ttl_msis the freshness window. If you have not executed within it, request a new decision instead of acting on stale data.costandeconomicsare populated on cost-aware (deterministic-v2) decisions — merchants with policy cost controls or resolved pricing. Ondeterministic-v1decisions and on blocks they arenull.
Shadow and enforce modes#
Every decision request carries a decision_mode. When the request omits it, the workspace default set in the dashboard (Merchant Workspace → Merchant → Decision mode) applies; when that is unset too, the mode is enforce. The per-request field always wins, so you can shadow one flow (for example renewals) while the rest of your traffic enforces.
- Enforce is the normal contract: your stack executes the returned instruction and reports the outcome.
- Shadow makes the instruction advisory. The decision is computed and recorded identically — same cached policy, provider health, and risk — but your existing payment logic keeps executing exactly as it does today. Report the outcome as usual;
provider_usedis whatever your own logic did.
Shadow exists so you can evaluate decision quality against real traffic with zero execution risk. Because every shadow decision is joined to the real outcome of the payment, GET /v1/decisions/shadow-report can tell you how often your logic already agrees with the instruction, and whether payments did better when it did. Run new integrations (or a policy change on one flow, such as renewals) in shadow first, then flip decision_mode to enforce when the numbers convince you.
Two things shadow never does: a shadow hold does not enter the review queue (there is nothing to resolve — your logic already executed), and shadow decisions are billed and rate-limited exactly like enforce decisions since they run the same decision path.
Handling every instruction#
switch (decision.action) {
case 'route':
// Execute on the selected provider; keep route.fallback_provider for failover.
return charge(decision.route!.primary_provider, order);
case 'use_default_route':
// Continue through your default payment path.
return charge(defaultProvider, order);
case 'step_up':
// Require an additional control (3DS, SCA, review) before continuing.
return startStepUp(order, decision.decision_id);
case 'hold':
// Pause in a pending state until review/approval completes.
return parkForReview(order, decision.decision_id);
case 'retry':
// Try again under your retry policy, usually with route.primary_provider.
return retry(order, decision.route);
case 'failover':
// Retry on a different provider than the one that just failed.
return charge(decision.route!.primary_provider, order);
case 'block':
// Do not submit the payment. route is null.
return reject(order, decision.decision_id, decision.reason_codes);
default: {
// Future-proofing: treat unknown instructions as your safe default.
const _exhaustive: never = decision.action;
return charge(defaultProvider, order);
}
}
Example responses for each instruction are on the Decision instructions page.
The latency model#
PayInference is designed for sub-50 ms decisions. That budget holds because nothing slow is allowed on the request path:
- Continuously updated payment intelligence. Intelligence Agents probe providers, ingest webhooks, and aggregate outcomes in the background, then write snapshots to a cache. The Decision API reads the cached snapshot in sub-millisecond time. See Payment Intelligence.
- Cached policy. Your published policy is cached and evaluated as pure computation.
- Low-latency inference. Risk scoring and route scoring are deterministic in-process computation. No PSP call, no database write, and no model service call blocks the response.
- Fire-and-forget persistence. The decision log is written off the critical path after the response is sent.
Decisions are therefore made on data that is seconds old, by design. Each response carries ttl_ms so you know how long it stays fresh, and decision_latency_ms so you can monitor the budget.
On your side, set a hard client timeout and a fallback. The Node SDK defaults to a 250 ms budget for decide() and can build a safe local fallback decision if PayInference cannot answer in time. See Production integration.
What the Decision API never does#
- It never calls a PSP, so it cannot leak provider outages into your checkout.
- It never approves or declines a payment. Issuers and PSPs do that after you submit.
- It never sees or stores card data. The schema rejects it at the boundary.