A mobile app-to-bank transfer solution that lets users send money from US-issued bank cards to Nigerian bank accounts, with the debited amount arriving in the destination account instantly.
Sergey Privalov, Co-Founder, Itexus
There is a moment in every consumer banking product built for migrant workers when the most-requested feature stops being a feature and becomes the whole point: I need to send money home. The account, the card, the early access to wages – those are what keeps the customer. Sending money to family is what brought them.
We have built both halves of that sentence. A US consumer banking app for people who had recently arrived to work, running on a sponsor bank's charter through a banking-as-a-service provider. And, separately, a cross-border transfer app on a US-to-Africa corridor, where we integrated a backup payout gateway because relying on a single provider in that corridor was not survivable. Two products, two teams, one obvious adjacency between them.
So when a banking app adds international transfers, the payment API integration looks easy. A remittance partner has an API, sometimes an SDK, occasionally a drop-in interface. You wire it up and ship.
And then you look at your own acceptance criteria and notice that every single one of them begins with the same clause: Given I am authorised in the partner's system.
The shape of the mistake
Here is what that produces, and it is worth reading slowly because it looks reasonable at every individual step.
The customer opens your app. They have already proved who they are – a document check, a government identifier, a selfie, a device fingerprint, and a decision your compliance function is accountable for. They have an account and a card in their hand.
They tap Send money abroad. They are asked to create an account: email, verification code, password, security question. Then to log in to it. Then to add a beneficiary – first name, last name, phone, address, destination country, destination currency, reception method, and if the reception method is a bank credit, the account number and the account holder's name. Then, finally, to send.
They now have two identities inside one app, two passwords, two password-reset flows, two transaction histories that never appear on the same screen, and two support queues that cannot see each other. If they abandon halfway through the second registration – and a meaningful share of them do – nobody can tell you whether that was a conversion problem, a literacy problem, a trust problem or a bug, because the drop-off happens inside a system you do not instrument.
This is not incompetence. It is the default outcome of accepting the boundary your partner hands you. The partner's API is shaped around the partner's user, because from the partner's point of view that is exactly what your customer is. Exposing that model is the fastest path to a working transfer, and on the day of the demo it works.
What it actually costs
You ask a customer to prove identity twice. Your onboarding already ran document verification, screening and device checks; the partner runs its own, because it is accountable for its own compliance. From the outside it reads as the same app asking the same questions again – precisely the experience that erodes trust with a customer base that has good historical reasons to be careful about handing over documents.
You lose your own data. Corridor mix, ticket size, frequency, seasonality, the share of customers who send within 48 hours of payday – these are the most commercially valuable facts about your customer base, and in this design they live on the partner's side. You cannot model retention against a behaviour you cannot see.
You inherit a reconciliation problem you did not design: two transaction records with different status vocabularies, and no single ledger in which a transfer and the account debit that funded it appear as one event.
And you inherit the partner's failure modes without the machinery to survive them. This is the expensive one, and it deserves its own section.
The failure mode nobody demos
A payout instruction does not have two outcomes. It has three: accepted, conclusively rejected, and unknown. Unknown is not exotic – it is what a timeout looks like, what a dropped callback looks like, what a partner having a bad afternoon looks like.
Teams that treat unknown as failure do one of two things. They refund the customer, and sometimes the payout was in fact in flight, so the recipient is paid and the sender is refunded. Or they retry – often into a second partner, because the industry advice is to always contract two payout partners for automatic failover – and the recipient is paid twice, in a corridor where the money cannot be recalled once it lands.
That advice is correct and incomplete. Contracting a second partner is necessary. What makes failover safe is the ability to tell a rejection from silence, and that is a property of the partner's interface, not of your code. Automatic reroute is safe only when the second partner is contracted and funded, the first partner's response is a conclusive rejection mapped by an explicit rule rather than by a default branch, and no instruction to the first partner is outstanding. An unknown outcome fails the second condition and must therefore stop and wait for a human or a statement.
If you have surfaced the partner as a second app, you have no place to put that logic. The state machine lives on their side.
What the boundary should be instead
The correction is small to describe and structural to implement: model a transfer as an instruction against a rail, not as a product.
Your movement domain already knows how to send money – internally between customers, and domestically over whatever rails your banking provider supports. Each rail has its own timing, its own reversibility and its own required fields. A cross-border corridor is another rail. Adding one is a rail implementation and a field set, not a second product with a second identity.
One transfer instruction, three rails. The destination decides which fields become mandatory:
| Internal | Domestic | Cross-border corridor | |
|---|---|---|---|
| What it is | Between customers of your own platform | Over the rails your banking provider supports | Another rail – a partner adapter and a field set |
| Timing | Immediate | Set by the domestic rail | Set by the corridor and payout method |
| Reversibility | Reversible by compensating entry | Rail-dependent | Effectively none once the payout lands |
| Required fields | Minimal – both parties are yours | The rail's standard fields | Set by the corridor registry: payout method, per-method beneficiary fields, thresholds |
| Who defines the rules | You | Your banking provider | Destination regulator + partner – versioned as data, not code |
Four consequences follow, and each of them is what the earlier design gave away:
One customer, one identity. The partner account becomes a credential your platform holds on the customer's behalf, provisioned silently when they first send. The customer never sees a second registration. Whether you can reuse your identity evidence rather than re-collecting it is a question for the receive side and for your partner's obligations, not for your UX – but even when the partner must re-verify, that verification can be driven from data you already hold instead of from a form the customer fills in again.
Corridor rules are versioned data. Look again at the requirement from the real project: reception method is account credit or cash pickup; if account credit, then two more fields become mandatory; destination country and destination currency are captured per beneficiary. That is not screen logic. That is a corridor registry – payout methods, required beneficiary fields per method and per threshold, limits, cut-offs, disclosure obligations, payout currency constraints – pinned by version to every transfer created under it, so a rule change never retroactively rewrites a completed transfer.
Quoting and payout are separate contracts, even when one partner implements both. Bind the rate to the payout adapter and changing partners changes the price your customer sees, and you can never compute your real margin per corridor. Store the provider rate, the customer rate, each markup component, the quote identifier, its expiry and the acceptance timestamp – and a disputed transfer becomes a lookup instead of an investigation.
One ledger. The account debit that funds the transfer and the transfer itself are the same business event and belong in the same double-entry record. Balances derived from postings, corrections as compensating entries, and a daily reconciliation against the partner's statement – which, incidentally, is also the mechanism that resolves those unknown outcomes without anyone having to telephone the partner.
What has changed since we first hit this
The project that produced the pattern above is several years old, and three things have moved underneath it.
The receive side became more explicit, and more binding. Depending on the corridor, the beneficiary may be payable only in local currency; the transfer may be required to be free of charge to both sender and beneficiary above a stated amount; the applied exchange rate may have to be disclosed to the beneficiary, not only to the sender; and the local counterparty may be required to contract with the principal rather than with an agent. Each of those changes either your revenue model or your data model in that corridor, and none of them is discoverable from your partner's API documentation.
Structured addresses stopped being optional – even though the deadline moved. Under the original plan, from 14 November 2026 CBPR+ would no longer have accepted fully unstructured postal addresses: at minimum, town and country had to be supplied in designated fields. In August, Swift agreed to extend the migration timeline, deferring the payments changes planned for Standards Release 2026 and promising an updated timetable by December. The direction has not changed. Earlier in 2026, Swift reported that roughly 65% of payment messages still contained unstructured addresses, illustrating the scale of the readiness gap. Separately, under FATF Recommendation 16, jurisdictions may set a de minimis threshold of no more than USD/EUR 1,000 for cross-border transfers; above the applicable threshold, payment messages must carry more detailed originator and beneficiary information, including structured location or identification data. The architectural implication is the same: capture address components as structured data at origination. It is cheap to design in before launch, difficult to retrofit later, and useful regardless of the final Swift cutover date.
And there is no template
This is the part I would underline for anyone planning this work, and it is the reason the four decisions above have to be made deliberately rather than discovered.
We did not find a first-party AWS or Azure reference architecture that covers the complete consumer remittance problem end to end – customer identity, payout-state handling, ledgering, reconciliation and corridor rules in one design. What exists is bank-to-bank ISO 20022 messaging architecture, which assumes you are a bank, and card-acceptance guidance, which is a different problem. One provider's own core-banking guidance still prescribes a purpose-built ledger database that has since been withdrawn from sale.
So if this has felt harder than the integration guide implied, that is not a failure of your team. You are not assembling a documented pattern. You are designing a system in a space where the obvious authorities have not published – which means the design decisions, and their costs, are yours to make explicitly rather than to inherit.
The short version
If you are adding cross-border transfers to a consumer banking product, check four things before the payment API integration starts:
- Does your customer register twice? If yes, you have accepted the partner's boundary rather than defining your own.
- Where does a transfer appear in your ledger? If the answer is "in the partner's dashboard", you have no reconciliation.
- What does your code do when the payout partner answers neither yes nor no? If the answer is "retry", you have built a duplicate-payment machine.
- Are destination rules in code or in data? If a new corridor needs a release, your corridor launches will always be late.
None of these are exotic. All four are cheap before the first line of integration code and expensive afterwards – which is, unhelpfully, exactly the order in which they are usually discovered.
If you are at the point of answering them and want a second reading, the useful version is narrow: tell us which corridors you are launching and which partner you have been handed. We can map the four decisions above to your corridor and provider – where identity should live, how payout states should behave, what belongs in the ledger, and which corridor rules need to become configuration before integration starts.