Backend checkout integration
The reference integration: a customer pays on your checkout page, your backend asks PayInference what should happen, executes the instruction, and reports the outcome.
The flow#
Customer clicks Pay
│
▼
Checkout page ──(your endpoint, your auth)──▶ Merchant backend
│ 1. POST /v1/decision
▼
PayInference ──▶ one instruction
│
▼
Merchant backend executes through
its existing payment stack (PSP SDK,
merchant credentials)
│ 2. PSP result
▼
POST /v1/outcomes (fire and forget)
Two hard rules:
- PayInference is never called from the browser. The checkout page talks to your backend only. API keys are secrets, and the browser must never hold one.
- Card data never goes to PayInference. Card fields go to your PSP as they do today. The decision request carries safe context only, and the schema rejects anything else.
Backend endpoint#
A complete Express handler using the Node SDK (an equivalent runnable example lives in examples/node-express-checkout, and a Next.js route handler version in examples/nextjs-server-checkout):
import express from 'express';
import { Decision, PayInferenceClient, Provider } from '@payinference/sdk-node';
const payinference = new PayInferenceClient({
apiKey: process.env.PAYINFERENCE_API_KEY!,
baseUrl: process.env.PAYINFERENCE_BASE_URL ?? 'http://localhost:4000',
merchantId: 'm_123',
timeoutMs: 50,
// If PayInference is unreachable, continue on your default provider
// instead of failing checkout.
fallback: { provider: 'stripe' },
});
const app = express();
app.use(express.json());
app.post('/checkout', async (req, res) => {
const { order_id, amount, currency = 'USD', country = 'US' } = req.body;
// 1. DECIDE. Safe context only: amount, currency, country, method.
let decision: Decision;
try {
decision = await payinference.decide({
order_id,
transaction: {
amount,
currency,
country,
payment_method: 'card',
transaction_type: 'authorization',
customer_type: 'returning',
},
available_providers: ['stripe', 'adyen', 'paypal'],
});
} catch (error) {
// Only validation and API errors land here; network errors used the fallback.
return res.status(500).json({ error: 'decision_failed' });
}
// 2. EXECUTE the instruction through your existing payment stack.
switch (decision.action) {
case 'block':
void reportOutcome(decision, null, 'blocked', 0);
return res.status(403).json({
error: 'payment_blocked',
decision_id: decision.decision_id,
});
case 'step_up':
// Kick off 3DS/SCA; execution continues after the challenge completes.
return res.json({
next: 'step_up',
decision_id: decision.decision_id,
provider: decision.route?.primary_provider ?? null,
});
case 'route':
case 'failover':
case 'retry':
case 'use_default_route': {
const provider = decision.route!.primary_provider;
const result = await chargeWithProvider(provider, { order_id, amount, currency });
// 3. REPORT, fire and forget. Never block the response on it.
void reportOutcome(decision, provider, result.outcome, result.latencyMs, result.reasonCode);
return res.json({
status: result.outcome,
provider,
decision_id: decision.decision_id,
});
}
}
});
async function reportOutcome(
decision: Decision,
provider: Provider | null,
outcome: string,
latencyMs: number,
reasonCode?: string,
) {
await payinference
.recordOutcome({
decision_id: decision.decision_id,
order_id: decision.merchant_id, // use your real order_id here
provider_used: provider ?? 'stripe',
outcome: outcome as never,
provider_latency_ms: latencyMs,
provider_reason_code: reasonCode,
amount: 14900,
currency: 'USD',
})
.catch(() => undefined); // outcome reporting must never break checkout
}
chargeWithProvider is your existing PSP integration; PayInference has no part in it.
The browser side#
The checkout page needs at most the safe subset of the decision your backend chooses to forward: the action, the route, and reason codes. The @payinference/sdk-js package provides typed helpers for exactly that and, by design, nothing else:
import { BackendDecisionClient, shouldProceed, requiresStepUp } from '@payinference/sdk-js';
const client = new BackendDecisionClient({ endpoint: '/api/checkout' });
const decision = await client.getDecision({ order_id, amount });
if (requiresStepUp(decision)) startThreeDS();
else if (shouldProceed(decision)) openPspSession(decision.route!.primary_provider);
else showPaymentUnavailable();
No API key, no PayInference URL, no card data. See Browser helpers.
Checklist#
- Decision call happens in your backend, with the API key from your secret store
- Client timeout set (50 to 250 ms) and a fallback configured
- Every
actionvalue handled, includingblockand a safedefault -
decision_idlogged next to your order id and PSP charge id - Outcome reported for every executed decision, asynchronously
- Card fields flow only to your PSP
Next: Retry integration, Step-up integration, Production integration.