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.

12 min to implement · Verified against the v4 API on 2026-10-11

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.pse is 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

  1. product_key What to collect
  2. Routes Ordered by priority
  3. Selected provider First active route
  4. Attempt Provider call recorded
  5. 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.

Create a payment by product
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.

Explain routing
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"
  }'
Response (abridged)
{
  "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.

List 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.
Offering a second payment (JavaScript)
// 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 being failed or cancelled.
  • The top-priority route is misconfigured. On /v4/checkout/payments the 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/payments or 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_key and 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.