Writing

A payment platform has to work beyond the checkout

Apps · 6 min read

The brief from a growing online retailer fit in one sentence:

Keep one checkout, connect two payment providers, and give our team one place to manage payments.

It is a reasonable brief. It is also about a third of the product.

A payment orchestration platform decides, for each payment, which provider handles it. The routing can follow rules on currency, card country, amount or anything else the business cares about, and the platform can coordinate retries and collect every attempt in one place. Stripe's orchestration documentation (opens in a new tab) describes the pattern well. For this retailer, the motivation was a second market and a reluctance to depend on a single provider.

Connecting the second provider is the easy part. The hard part is that the platform now has to explain what happened to a payment, refuse to do the same thing twice, and help finance account for the money. The quickest way to see those requirements is to follow three people through the system.

The buyer waiting for an answer

A buyer clicks Pay. The spinner runs. The page shows an error.

The buyer now has a problem the retailer created: they do not know whether an order exists, and they do not know whether pressing Pay again will charge them twice. Most of them will press it again.

The engineering fact that drives everything here is that a missing response does not mean the payment failed. The provider may have received the request and acted on it, and only the reply was lost. Stripe's notes on low-level errors (opens in a new tab) are blunt about this: an unknown outcome has to be resolved, not treated as a free retry.

So the platform has an explicit pending state. The order keeps a stable reference while the system checks the original attempt. The message to the buyer says that confirmation is being established and says nothing about success or failure until one of them is true.

WITH A PENDING STATE Pay Pending the original attempt is checked Paid. The order is kept. Failed. The buyer is told the truth. WITHOUT ONE Pay no answer Pay again the buyer resolves your uncertainty Charged twice.
A missing response is not a failed payment. The pending state is where the platform finds out which it was.

Repeated requests get handled with idempotency keys, which let a client resend a request without the provider creating the operation twice. Stripe supports this (opens in a new tab), as do most providers. But a key is only known to the provider that issued it. Provider B has no idea that Provider A may already have taken the money. The orchestration layer therefore has to keep its own record of the intended payment and every attempt made against it, and an unresolved attempt on one provider must never automatically become a second charge on the other.

Cross-provider retry is also narrower than it sounds. A payment that failed 3D Secure, or used a feature the second provider does not support, cannot simply be re-sent elsewhere; Stripe's retry documentation (opens in a new tab) lists the conditions. "Try the other gateway whenever anything fails" is not a policy, it is an incident waiting for a date.

None of this reaches the buyer as technical detail. What they see is a checkout that keeps their order, tells them the truth about its status, and never asks them to resolve the merchant's uncertainty by hammering a button.

The support colleague who has to explain it

The buyer emails support. Support opens the admin screen.

If that screen is two provider dashboards behind adjacent tabs, support is doing the orchestration by hand. The unit that actually helps is one payment history attached to the order: every attempt, which provider handled it, what came back, and whether anything is still unresolved. A plain-language status sits on top. The raw provider responses sit underneath, one click away, because the engineer who gets escalated to needs them.

Much of that history arrives by webhook, and webhooks arrive late, twice, or out of order. Stripe documents (opens in a new tab) duplicate delivery and the absence of ordering guarantees, and the receiving system has to be built for both. In this platform a repeated event does not trigger a second fulfilment, and an older event cannot overwrite a newer verified outcome. The order system and the payment system each own defined events rather than both trying to interpret everything.

Support actions need the same precision. A refund is issued against the attempt that actually succeeded, through the provider that processed it, with the colleague's permissions applied and the request logged. If a refund hangs, it has a named owner and an escalation route, not a spinner that is still going on Monday.

Card data stays out of the admin screen entirely. Support does not need it and should not be able to see it. Using a third-party provider reduces the systems that touch card numbers, but the PCI Security Standards Council is explicit (opens in a new tab) that it does not remove the merchant's PCI DSS obligations, so who handles what gets settled at the architecture stage, not after the first audit letter.

The job of the support colleague is to help the customer. They should not have to reconstruct a payment from logs, decode two providers' error vocabularies, or ask an engineer whether it is safe to click Refund.

The finance colleague closing the month

The checkout showed a successful payment weeks before finance can say what happened to the money.

Fees, refunds, disputes and payout timing all sit between what was sold and what landed in the bank. Reconciliation is the work of matching those records and explaining the gaps, and matching provider payouts to bank deposits is only one part of it. Stripe's reconciliation guide (opens in a new tab) covers the mechanics for one provider; the retailer is about to have two.

That is why this gets designed before the new routing rule goes live, not after the first month-end with two sets of numbers. Each transaction has to be traceable from the order, to the successful attempt, to the provider's records, to the payout. A total on a dashboard is not traceability. The platform keeps each provider's original references alongside the retailer's own identifiers, imported records keep their source and date, and anything unmatched stays visible as unmatched instead of being forced into a reassuring "reconciled" state.

A total on a dashboard. Nothing connects it to anything. That is not traceability. Order Successful attempt Provider's record Payout
What finance needs: each payment traceable from the order to the payout, with every link kept.

The two providers' accounting data will not look the same. Stripe, for instance, records payments routed to other providers differently (opens in a new tab) from balance transactions for money it processed itself. Reports written around one provider's assumptions will need changing, and it is better to find that out in a test environment.

What finance needs is a traceable story for every pound, without a side spreadsheet rebuilt from exports.

What "done" means

A successful test payment is an early milestone. The one that matters is a complete path: payment confirmed, order handled, refund issued where applicable, and every resulting record reconciled.

The platform is also now a dependency in its own right. Two working providers are no help if the software choosing between them is down, so the operating plan covers the platform's own failures, safe changes to routing rules, and recovery that does not lose payments already in flight.

For this retailer the first release proves one full lifecycle through one provider. The second provider is added afterwards with explicit rules, tested exception paths and a small share of traffic, not switched on because the connection test passed.

The client still gets one checkout and one place to manage payments. But "one place" now means a consistent account of what the business knows, what it has done and what is still open. Nobody should need to understand the routing policy to find out whether a customer has paid.

More writing