GuidesRemittance flow

End-to-end remittance flow

For Remittances API users (IMTOs, MSBs, EMIs). End-to-end walk-through covering: register an end-user → upload KYC → provision a VIBAN → receive an inflow → send an outbound payout.

⚠️

Gated: requires the Remittances feature flag on your merchant account. Contact support@the-technest.com to enable.

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,
    country: 'CA',
    id_type: 'passport',
    id_number: pouchUser.passportNumber,
    dob: pouchUser.dob,
    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: 'id_document',
      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 flowpayout_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
  • Remittance 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.

What’s next