Implementation Guide
Add a second payment provider without rewriting your product
Route pay-ins across more than one provider while your product keeps one payment lifecycle and one API.
The rule to remember: your product asks for a product_key, and the routes behind
it decide the provider. Trying a different provider for the same purchase is safe only when the first payment
is final and failed. A timeout or an unknown outcome is not a failure.
The job
A merchant, or a PSP serving merchants, takes PSE bank transfers in Colombia through one provider. It wants a second provider: for an outage, for better terms, or because the first one does not cover a case it needs. The checkout, the order logic and the webhook handler should not change when that happens, and the team needs to know exactly what happens when a payment fails on one provider.
Before you start
- A working integration through one product key, for example from Accept a local bank-transfer payment.
-
A contract and credentials with the second provider. In the catalog,
bank_transfer.pseis offered by more than one provider, each with its own status on the integrations page. - Access to Orangepill Console to add the integration and set route priority for your tenant.
- Maturity: pay-in routing by priority is in production. Automatic failover exists only for payment requests. Payout failover is in development.
Flow
- product_key What to collect
- Routes Ordered by priority
- Selected provider First active route
- Attempt Provider call recorded
- Outcome completed / failed
Implementation
1. Keep provider names out of your product
Create payments with a product_key and no provider. Your code then looks the same whichever
provider runs the payment. The response tells you which one did, in provider_key.
curl -X POST "$ORANGEPILL_API/v4/checkout/payments" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "9900000",
"currency": "COP",
"product_key": "bank_transfer.pse",
"channel_type": "redirect",
"return_url": "https://merchant.example.co/checkout/return",
"metadata": { "order_ref": "order-8812" }
}'
If you must pin a provider for one payment, pass provider_instance_id. Use it for specific cases,
such as the second payment described in step 5, and not as the default.
2. Add the second provider as a route
In Console, add the new integration and give it a priority for the product. Routes are configured per tenant.
For POST /v4/checkout/payments and payment intents, Orangepill uses the first active route for the
product by priority. To move traffic, change the priority. Your API calls stay the same.
3. Inspect how routing evaluates your configuration
POST /v4/payments/explain shows the routes for a product, their order, whether each one is
eligible and why, and which one would be picked. It is advisory: it evaluates your configuration with its own
rules and is not a record of which provider will run a given payment. Use it to check a configuration change,
and use attempts to see what actually ran.
curl -X POST "$ORANGEPILL_API/v4/payments/explain" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_key": "bank_transfer.pse",
"capability": "payin",
"country": "CO",
"currency": "COP",
"amount": "9900000"
}' {
"success": true,
"data": {
"routing_summary": {
"strategy": { "type": "priority_failover", "role_order": ["primary", "secondary", "fallback", "sandbox"] },
"routes": [
{ "position": 1, "provider_key": "<provider A>", "role": "primary",
"eligibility": { "eligible": true, "reason_codes": [] } },
{ "position": 2, "provider_key": "<provider B>", "role": "secondary",
"eligibility": { "eligible": true, "reason_codes": [] } }
],
"summary": { "eligible_count": 2, "fallback_count": 0, "has_primary": true }
},
"decision": { "selected_integration_id": "…", "reason_code": "…", "reason_text": "…", "position": 1 },
"evaluation": { "evaluated_count": 2, "degraded_integrations": [], "skipped_integrations": [] }
}
} 4. Read the attempts as well as the payment
A payment is the purchase. An attempt is one call to one provider, with its own status, provider id and error. Support and reconciliation work from attempts.
curl "$ORANGEPILL_API/v4/payments/$PAYMENT_ID/attempts" \
-H "Authorization: Bearer $ORANGEPILL_API_KEY" 5. Decide when another provider is safe
On POST /v4/checkout/payments and payment intents, one provider handles each payment and there is
no automatic failover. If you want the purchase to go through a different provider, that is a new payment, and
your code decides when to create it.
| What you see | What it means | Safe to try another provider? |
|---|---|---|
Status failed or cancelled | Final. The provider declined or the payment was ended. | Yes, as a new payment linked to the same order. |
Status pending, processing, requires_action | Not final. The payer may still be approving in their bank. | No. Wait for the webhook or read the status again. |
| Your create request timed out | Unknown. A payment may exist. This route has no idempotency key. | No. Find the payment first. |
| No webhook yet | Unknown. Delivery can be late or retried. | No. Read GET /v4/payments/{paymentId}/status. |
Status completed | Paid. | No. A second payment would charge twice. |
// Your product decides whether to offer a second payment. Orangepill does not do it for you on this route.
const { status } = await getStatus(firstPaymentId);
if (status === 'completed') return markPaid(order);
if (status !== 'failed' && status !== 'cancelled') return keepWaiting(order); // not final: never start another
// Final failure. A new payment is safe. Pin the other route if you want a different provider.
await createPayment({
...sameOrder,
provider_instance_id: otherProviderInstanceId, // optional; omit to let priority choose again
metadata: { order_ref: order.ref, previous_payment_id: firstPaymentId },
}); 6. Where automatic failover runs today
Payment requests (POST /v4/checkout/payment-requests, used for Bre-B dynamic QR and key) and
attempts made inside hosted checkout sessions move to the next eligible provider on their own, in two cases
only: the provider's configuration is unusable, or the network failed without any confirmed result. They stop
on a provider rejection, a duplicate reference or an unknown error, because in those cases the first provider
may hold a payment. The failover trace is not exposed to clients. Read attempts for what ran.
Payouts are different. In Development A payout that fails on one provider is not retried on another.
7. Check what each provider can do
The same product key does not make two providers identical. Before adding a route, compare for that product:
- Currencies and amount limits for the product on each provider.
- Refunds. Some products are not refundable through the API on some providers and are handled manually.
- The payer details each provider requires, which are confirmed during evaluation.
- Channel type and what the payer sees: a redirect, a QR or a reference.
- How and when each provider reports a final status.
What can go wrong
- You create a second payment while the first is still
pending. The payer can complete both. Gate a new payment on the first beingfailedorcancelled. - The top-priority route is misconfigured. On
/v4/checkout/paymentsthe request does not move to the next route on its own. Check explain after every configuration change. - Explain and reality disagree. Explain is advisory. Attempts show what ran.
- A provider rejects a payment request. Failover stops. Treat the rejection as the outcome and decide in your product what to offer next.
- The second provider needs payer details the first did not. The payment fails on input. Collect what both require before you switch traffic.
What Orangepill guarantees today
- One API and one payment lifecycle across providers for the same product key.
- Pay-in route selection by priority, configured per tenant.
- Each provider call recorded as an attempt with its own status and error.
- Automatic failover on payment requests only for configuration and network errors, and never after a rejection, a duplicate or an unknown outcome.
What Orangepill does not guarantee
- Automatic failover on
POST /v4/checkout/paymentsor payment intents. - Payout failover. It is in development.
- That explain predicts the provider for a specific payment.
- Identical behaviour across providers for the same product: limits, refunds and payer fields differ.
- Exactly-once behaviour across external providers. Two payments created by your code are two payments.
Production considerations
- Production has its own routes, priorities and provider credentials. Configure and check them separately from sandbox.
- Persist
payment_id,provider_keyand your order reference on every payment, and the link between a failed payment and the one that replaced it. - Monitor failure rate and time to final status per provider, and payments still non-final past your normal window.
- Test in sandbox: a declined first payment followed by a second, a timeout on create, and a route you disable.
- Your operations team should be able to see, per order, every payment and attempt and which provider handled each.
- Agree with each provider how disputes and manual refunds are handled before routing traffic to it.
The reasoning behind the decision table is in When payment failover is actually safe.
Build this with us.
Sandbox access and API credentials are provided during evaluation.