End-to-end flow
This is the end-to-end walkthrough. For the reference on each call it uses, see the API reference.
For platforms that issue international accounts to their customers (wallets, marketplaces, diaspora apps, and licensed money-services operators). End-to-end walk-through covering: register an end-user → upload KYC → provision an account → receive an inflow → send an outbound payout.
Requires FX capabilities enabled on your account. Sending in foreign currencies and issuing international accounts both use the same access. Contact Technest to enable it.
Register the end-user as a customer
When a Pouch user (or your equivalent) signs up, your backend creates a Swappr customer record:
const customer = await fetch('https://api.swappr.me/api/v1/customers', {
method: 'POST',
headers: { /* auth + idempotency */ },
body: JSON.stringify({
type: 'individual',
first_name: pouchUser.firstName,
last_name: pouchUser.lastName,
email: pouchUser.email,
phone: pouchUser.phone,
country: 'CA',
id_type: 'passport',
id_number: pouchUser.passportNumber,
id_expiry_date: pouchUser.passportExpiry, // YYYY-MM-DD, must not be in the past
dob: pouchUser.dob, // YYYY-MM-DD
// dob + street/city/state + id_expiry_date CANNOT be added later, a
// customer created without them can never be issued a virtual account.
street: pouchUser.street,
city: pouchUser.city,
state: pouchUser.province,
zip_code: pouchUser.postalCode,
tin: pouchUser.sin,
customer_reference: pouchUser.id, // YOUR opaque user ID
interac_email: pouchUser.interacEmail,
}),
}).then((r) => r.json());
// Persist the Swappr customer.id alongside your user record
await db.users.update({
where: { id: pouchUser.id },
data: { swapprCustomerId: customer.id },
});Status starts at pending, KYC docs come next.
Upload KYC documents
For each required document (ID front, ID back, proof of address, etc.):
// 1. Request upload URL
const intent = await fetch(
`https://api.swappr.me/api/v1/customers/${customer.id}/files`,
{
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({
file_category: 'identity',
filename: 'passport.jpg',
content_type: 'image/jpeg',
}),
},
).then((r) => r.json());
// 2. PUT bytes to the upload URL
await fetch(intent.upload_url, {
method: 'PUT',
headers: { 'Content-Type': 'image/jpeg' },
body: passportFileBytes,
});
// 3. Track the file_id for the attach step
fileIds.push({ slot: 'id_document', file_id: intent.file_id });Attach files + submit KYC
Once all docs are uploaded, attach them in named slots + submit:
await fetch(
`https://api.swappr.me/api/v1/customers/${customer.id}/files/attach`,
{
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({
id_file: fileIds.find((f) => f.slot === 'id_document').file_id,
proof_of_address_file: fileIds.find((f) => f.slot === 'proof_of_address').file_id,
}),
},
);
// Submit for verification review
await fetch(`https://api.swappr.me/api/v1/customers/${customer.id}/kyc`, {
method: 'POST',
headers: { /* ... */ },
body: '{}',
});Listen for verification
Subscribe to customer_verified + customer_rejected webhooks:
async function handleCustomerVerified(data: any) {
await db.users.update({
where: { swapprCustomerId: data.id },
data: {
kycStatus: 'verified',
kycVerifiedAt: new Date(data.verified_at),
},
});
// Now provision a VIBAN if you want to receive money
await provisionVibanForUser(data.id);
}
async function handleCustomerRejected(data: any) {
await db.users.update({
where: { swapprCustomerId: data.id },
data: {
kycStatus: 'rejected',
kycRejectionReason: data.rejection_reason,
},
});
// Notify user, prompt them to retry with a NEW customer_reference
}Typical verification time: a few hours to 1 business day.
Provision a VIBAN
After verification, create a per-customer VIBAN to receive money in GBP / USD / EUR:
const viban = await fetch(
`https://api.swappr.me/api/v1/customers/${customerId}/virtual_accounts`,
{
method: 'POST',
headers: { /* ... */ },
body: JSON.stringify({ currency: 'GBP' }),
},
).then((r) => r.json());
// Display in your UI
console.log({
account_number: viban.account_number,
bank_name: viban.bank_name,
// For GBP: also include sort_code in the bank_code field
});If account_number is empty, the VIBAN is provisioning, wait for the virtual_account_activated webhook before showing it to the user.
For CAD, don’t create a VIBAN, set interac_email at customer-create time and the upstream FX rail routes inbound Interac transfers automatically.
Receive an inflow
When money lands at the customer’s VIBAN, Swappr fires wallet_funded with a customer block identifying which end-user the inflow belongs to. Sample sender values below are fictional, real deliveries carry the actual sender’s bank-of-record name.
async function handleWalletFunded(event: any) {
// Match the inflow to your end-user via the customer block
const user = await db.users.findUnique({
where: { swapprCustomerId: event.customer.id },
});
// Credit the user's app-side balance + log the transaction.
// Example sender values: "EMILY HARRIET ASHWORTH" / "87654321" /
// "203000", your handler sees the actual remitter's details on
// each delivery.
await db.transactions.create({
data: {
userId: user.id,
type: 'inflow',
currency: event.currency,
amountMinor: BigInt(event.amountMinor),
senderName: event.sender?.name,
senderAccountNumber: event.sender?.accountNumber,
senderBankName: event.sender?.bankName,
narration: event.narration,
swapprLedgerEntryId: event.ledgerEntryId,
receivedAt: new Date(event.receivedAt),
},
});
// Notify the user, push, email, SMS
await sendUserNotification(
user,
`You received ${formatMoney(event.amountMinor, event.currency)}`,
);
}The merchant wallet (your business wallet) is credited at Swappr; per-user accounting on your app side is your responsibility.
Send an outbound payout
When the user wants to send money to a recipient, submit a payout WITH the sender_customer_id so Swappr attaches the sender identity per-transaction (CBN IMTO compliance):
await fetch('https://api.swappr.me/api/v1/payouts', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SWAPPR_API_KEY}`,
'Idempotency-Key': randomUUID(),
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount_minor: '500000',
currency: 'NGN',
recipient: {
account_number: '0690000032',
bank_code: '044',
},
sender_customer_id: user.swapprCustomerId, // Auto-resolves sender info
merchant_reference: `POUCH_OUT_${user.id}_${transactionId}`,
}),
});Sender info is auto-resolved from the customer’s KYC fields (first/last name, ID type, ID number, DOB, ID expiry), no need to duplicate them in every payout call.
Track outbound completion
Same as the Universal API webhooks flow: payout_paid / payout_failed events fire on completion. Update the user’s transaction record + notify them.
Architecture diagram
┌─────────────┐ ┌────────────────────┐ ┌──────────┐
│ Your app │────────▶│ Your backend │────────▶│ Swappr │
│ (end-user) │ │ (your servers) │ │ API │
└─────────────┘ └────────────────────┘ └──────────┘
│
▼
┌──────────┐
│ Upstream │
│ FX + │
│ KYC │
│ rails │
└──────────┘
◀────── webhooks ─────────────┘
(customer_verified, wallet_funded,
payout_paid, payout_failed)Your app never speaks to upstream rails directly. Your backend talks to Swappr; Swappr proxies to the underlying rails + persists the customer mapping internally. End-user PII is encrypted at rest in Swappr’s DB; only the regulated entity (Technest) has decrypt access.
Compliance notes
- Each end-user payout carries sender identity: required by CBN’s IMTO regulations + most international AML regimes
- KYC is performed by a regulated partner as a sub-processor under Technest’s IMTO licence
- Dual-control on remittance corridors: your team can require maker-checker on payouts above thresholds via dashboard settings
- Payout reports available via
GET /v1/reports/payouts, filter by date + currency for regulator submissions
For more on regulator filings + audit trails, contact compliance@the-technest.com.