Payments · API Integration · FinTech
Building Reliable Payment and Transfer Integrations
A field guide to integrating banks, mobile-money operators and billers, written from the perspective that every provider will eventually behave in a way its documentation does not describe.
The documentation describes the happy path. Your integration has to survive everything else.
This is a practical guide to integrating third-party financial providers, partner banks, mobile-money operators, utility and airtime billers. The advice generalises, but it comes from a specific observation: the difficulty of this work is almost never the protocol. It is the gap between what a provider's contract says and how a provider behaves at 2am on a Friday.
Establish what "success" means for each provider#
Before writing any code, answer one question per provider:
When this API returns 200, has value moved?
There are three common answers and they demand different integrations:
- Settled. The operation is complete. Rare, and lovely when you get it.
- Accepted. The request is queued; the outcome arrives later by callback, or by you asking. Most common.
- Ambiguous. The response indicates receipt but the provider's own documentation does not commit to what that implies.
Category three is not hypothetical, and the correct handling is to treat it as category two until proven otherwise. Optimism here produces double payments.
Write the answer down in the adapter. In code, not in a wiki:
export const providerProfile = {
id: "provider-a",
successSemantics: "accepted", // NOT "settled"
callbackSupported: true,
pollingSupported: true,
timeoutMs: 20_000,
// What the provider does on a repeated request with the same reference.
duplicateBehaviour: "returns-original",
} as const;
Six months later, the person debugging a stuck transaction should be able to learn a provider's semantics by reading the adapter rather than by experiment.
Own the request identity#
Generate your own reference for every operation, attach it to the provider request, and never rely on the provider's identifier as your primary key. Two reasons:
- You need a key before the call. If the request times out before you receive a provider reference, an integration keyed on their identifier has no handle on the transaction it just started.
- Provider identifiers are not stable contracts. Formats change. Some providers reuse them across environments.
Your reference plus their reference, stored together, is the minimum record. The first lets you find the transaction; the second lets you ask them about it.
Timeouts are a design decision, not a default#
A default HTTP client timeout is almost always wrong for financial calls, too short and you manufacture indeterminate states, too long and one slow provider consumes your request capacity.
Set them per provider, derive them from observed behaviour rather than optimism, and make sure of one thing above all:
A timeout is not a failure. It is an unknown.
Code that treats a timeout as failure, releasing holds, telling the user it did not work, allowing a retry as a fresh operation, is how the same payment gets made twice. The correct response to a timeout is to record the transaction as indeterminate and hand it to the resolution path.
Build the resolution path before you need it#
Every integration needs a way to answer what actually happened for a transaction that did not resolve inline. Three mechanisms, in increasing order of reliability:
Callbacks. Fast, and completely untrustworthy on their own. Callbacks get lost, arrive out of order, arrive twice, and arrive for transactions you have no record of. Treat every callback as an unvalidated hint: verify it against the provider before acting, and make handling idempotent.
Polling. Slower, more reliable. For each indeterminate transaction, ask the provider periodically with backoff, up to a defined ceiling. The ceiling matters, unbounded polling turns a stuck transaction into permanent load.
Reconciliation. The backstop. Compare your records against the provider's over a window and resolve the differences. This is the only mechanism that catches transactions neither of the other two knew to look at, and it is the one teams most often defer. It should not be deferred: it is what lets a human establish ground truth when everything else has been exhausted.
Retry deliberately, and only what is safe#
Retry rules worth holding to:
- Never retry an unknown. Resolve it first. A retried timeout is a duplicate.
- Retry connection failures freely, a connection that was never established did nothing.
- Use exponential backoff with jitter. Synchronised retries after a provider recovers will knock it over again.
- Cap total attempts, and make the cap a business decision. "Retry forever" is a decision too, just an unconsidered one.
Circuit-break per provider#
When a provider starts failing, every additional request makes things worse for both sides and consumes capacity your other providers need. A circuit breaker scoped to the individual provider, not to the platform, keeps one bad dependency from becoming an outage.
The important detail is what happens when the breaker is open. Failing fast is good; failing fast with an accurate message is better. "This provider is currently unavailable" is actionable. A generic error is not.
Log the correlation, not the payload#
Financial payloads contain data that should not be sitting in log storage. What you actually need to debug an integration is:
- your reference and the provider reference
- provider id, operation, attempt number
- status transitions with timestamps
- outcome classification
That set answers nearly every real question without retaining account details, credentials, or customer information. Redact by default and add fields deliberately, the reverse order tends to leak.
A closing note on respect for the dependency#
It is easy to frame integration work as fighting badly-behaved third parties. A more useful frame: every provider is a production system with its own constraints, its own incidents and its own engineers making trade-offs under pressure. Designing for their bad days rather than being surprised by them is the whole discipline.
The integrations that hold up are the ones that assume the other side is doing its best and build for the times that is not enough.
Next article
Lessons From Building Production APIs
An API is a promise you have to keep after you have forgotten making it. What ten years of building integration surfaces teaches about versioning, errors, contracts and the clients you cannot upgrade.
Contact
Let’s Build Something Meaningful
I’m open to senior and lead engineering roles internationally, backend, full-stack, architecture and platform work. The fastest route is email; LinkedIn works just as well.