Customers
Customers are the verified end-users you provision international accounts for. Each customer is KYC-verified once, then issued one or more international virtual accounts (GBP / USD / EUR) or a CAD Interac routing address. Inflows and outflows attributed to a customer are queryable as a single transaction history.
Customer endpoints require the international accounts feature on your account. If it isn’t enabled, every customer endpoint returns 403 feature_not_enabled. Contact Technest support to enable it.
The customer object
{
"object": "customer",
"id": "cus_xxxxxxxxxxxx",
"customer_reference": "your-internal-user-id",
"interac_email": "jane@example.com",
"status": "verified",
"env": "live",
"created_at": "2026-01-15T10:00:00Z"
}Field notes:
idis the Swappr customer id (cus_...). Use it on every nested call (/v1/customers/{id}/...) and ascustomer_idwhen attributing a payout.customer_referenceis your identifier for this user — pass it on create and we echo it back everywhere. It also dedupes creates (see below).statusis one ofpending/verified/rejected/closed. Account provisioning and payouts requireverified.interac_emailis the routing address for CAD Interac e-Transfer. Null for customers who don’t use CAD.
POSTCreate a customer
Creates a customer record and registers them with our banking partner. This does not start verification — the customer stays pending until you upload an identity document (see Upload identity documents and the optional Submit KYC acknowledgement below). Requires an Idempotency-Key header — use a unique value (UUID) per logical create.
individual or business.
Valid email address.
ISO 3166-1 alpha-2 (e.g. NG, GB, CA) — must be a supported country.
Government ID type. Allowed values are country-specific (see below).
The ID number.
Tax identification number. Label varies by country (BVN/NIN for NG, SSN/EIN for US, SIN/BN for CA, NINO/UTR for GB) — always passed via tin.
Postcode — validated against the country’s format.
Required when type=individual. Paired with last_name.
Required when type=individual. Paired with first_name.
Required when type=business.
ID expiry (YYYY-MM-DD, future date). Required for individuals — the banking partner needs it to provision a virtual account and it cannot be added after verification.
Your internal user id. Dedupes creates (see idempotency).
CAD Interac routing address.
Contact phone. Required for individuals — the banking partner needs it to provision a virtual account. Presence is required; the format isn’t validated.
Date of birth (YYYY-MM-DD, a past date). Required for individuals — needed to provision a virtual account; can’t be added after verification.
Address component. Required for individuals — needed to provision a virtual account; can’t be added after verification.
Address component. Required for individuals — needed to provision a virtual account; can’t be added after verification.
Address component. Required for individuals — needed to provision a virtual account; can’t be added after verification.
ID issue date (YYYY-MM-DD). Optional, but not updatable after create.
id_type is country-specific and cannot be corrected after submission. Each country accepts a different set (e.g. NG individuals accept International Passport or Driver’s License, not the NIN slip). If verification is rejected on id_type / id_number / name fields, you must retry with a new customer_reference — the PATCH endpoint can’t fix those fields. Submitting the wrong id_type returns invalid_id_type with the allowed list for that country.
Idempotency & retries
A customer_reference is unique per merchant per env. Creating again with the same customer_reference does not create a duplicate:
- If the prior attempt was rejected by verification, the same
customer_referenceis refreshed with your new body and re-submitted (the customer id stays stable for audit). - If a prior attempt succeeded, you get the existing customer back.
On a transient provider error (HTTP 502, retry_safe: true in the error detail), retry with the same customer_reference + a fresh Idempotency-Key — the orphan-replay flow resumes from your existing record. On a validation rejection (422), fix the offending field and retry — you may reuse the same customer_reference (the failed record was discarded) with a fresh Idempotency-Key. A validation 422 does not return a customer_id (the half-created row was rolled back).
Errors
| Code | HTTP | Meaning |
|---|---|---|
missing_field | 400 | A required field is absent |
country_not_supported | 400 | country isn’t in the supported list |
invalid_id_type | 400 | id_type not accepted for that country/type |
invalid_postcode | 400 | zip_code doesn’t match the country format |
validation_failed | 422 | Identity rejected at create — fix the field, then retry reusing the same customer_reference with a fresh Idempotency-Key (no customer_id is returned) |
provider_error | 502 | Transient provider error — retry with the same customer_reference |
Create response
For individual customers returned as pending, the create response adds a next_action and a requirements hint pointing at the document-upload steps — so it’s clear an identity document must be uploaded before verification can start. These fields are purely additive; the base customer fields are unchanged, and they’re absent for business customers (and on list / retrieve / idempotent-replay responses).
{
"object": "customer",
"id": "cus_...",
"status": "pending",
"next_action": "upload_identity_document",
"requirements": {
"identity_document": {
"required": true,
"reason": "Verification has not started. Upload an identity document to begin.",
"steps": [
"POST /api/v1/customers/{id}/files with { file_category: \"identity\", content_type } to get an upload URL",
"PUT the file bytes to the returned upload_url, echoing required_headers verbatim (server-side; the URL is not browser-CORS-enabled)",
"POST /api/v1/customers/{id}/files/attach with { id_file: <file_id> } to attach it and start verification"
]
}
}
}curl https://api.swappr.me/api/v1/customers \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"type": "individual",
"customer_reference": "user_8821",
"email": "oliver@example.com",
"country": "GB",
"first_name": "Oliver",
"last_name": "Whitmore",
"id_type": "passport",
"id_number": "P1234567",
"id_expiry_date": "2030-01-01",
"tin": "AB123456C",
"zip_code": "SW1A 1AA"
}'GETList customers
Returns a Stripe-style list of customer objects. Use q to answer “did my user X get provisioned here?”.
1-100, default 50.
Cursor (last customer id from the previous page).
Filter: pending / verified / rejected / closed.
Search by customer_reference, interac_email, or name (case-insensitive contains).
curl 'https://api.swappr.me/api/v1/customers?status=verified' \
-H "Authorization: Bearer sk_test_..."GETRetrieve a customer
Returns the customer object.
curl https://api.swappr.me/api/v1/customers/cus_xxxxxxxxxxxx \
-H "Authorization: Bearer sk_test_..."PATCHUpdate a customer
Narrow scope — contact + address fields only. Identity fields (id_type, id_number, name) are immutable after submission; to correct those, create a new customer with a new customer_reference.
Returns the updated customer object with the latest verification status.
curl -X PATCH https://api.swappr.me/api/v1/customers/cus_xxxxxxxxxxxx \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "phone": "+2348023456789" }'POSTSubmit KYC
Optional — this endpoint does not start verification, and you can safely skip it.
For individual customers (type: individual — the customer flow this API serves), verification is document-triggered: it begins automatically the moment you attach an identity document (id_file) — see Upload identity documents below. There is no separate KYC submit for individuals. This /kyc call simply records kycSubmittedAt (and stores any supplemental KYC details you pass) as a clean audit acknowledgement — it is never required for verification to happen.
Business / KYB customers verify differently. A business is onboarded with Technest directly — beneficial owners plus business documents, reviewed in roughly 1–5 business days — a separate process not driven by this endpoint (and not self-serve via this API today). See Individual vs business.
status reflects the partner’s verification state. Verification completes asynchronously (a few minutes, up to ~2h). Watch the customer.status_changed webhook for the transition to verified / rejected, or re-read the customer — status also refreshes from the banking partner on read.
{
"object": "kyc_submission",
"status": "pending"
}POSTUpload identity documents
Two steps — request a signed upload URL, then PUT the file bytes directly:
1. Request an upload URL — POST /v1/customers/{id}/files with the document slot (file_category) and the file’s content_type (e.g. image/jpeg, application/pdf). Returns a file_upload object with a signed upload_url and the required_headers you must echo (these now include the Content-Type for your file).
2. PUT the bytes to upload_url with the required_headers verbatim, from your backend. Do not send your Bearer token — the signed URL authorizes the upload itself.
3. Attach the uploaded file — POST /v1/customers/{id}/files/attach, mapping file_ids to typed document slots (e.g. id_file).
Send the required_headers exactly as returned — they include the Content-Type for the content_type you specified. A wrong or missing Content-Type causes storage to reject the file and the attach step fails.
Run the byte-upload from your backend. The signed upload_url is not browser-CORS-enabled — a PUT from a browser or single-page app fails with a CORS Failed to fetch. Perform step 2 server-side (see Backend architecture). Steps 1 and 3 are ordinary authenticated API calls.
{
"object": "file_upload",
"file_id": "file_xxxxxxxxxxxx",
"upload_url": "https://...signed-url...",
"required_headers": { "Content-Type": "application/pdf" }
}POSTIssue an international account (VIBAN)
Issues a dedicated international virtual account for a verified customer in one of the supported currencies.
One of GBP / USD / EUR.
NGN virtual accounts are not issued for customers this way; CAD uses an Interac e-Transfer routing address (set via interac_email) rather than a virtual account. The customer must have a verified status and the merchant must hold an active wallet in that currency.
The call is idempotent per (customer, currency) — only one virtual account exists per customer per currency. List a customer’s issued accounts with GET /v1/customers/{id}/virtual_accounts.
This account also authorizes the customer’s outbound payouts. In the individual flow a GBP/USD/EUR payout sends from the sender’s own virtual account, so a payout with sender_customer_id set to a customer who has no active VBA in that currency is rejected with sender_account_not_provisioned. Issue the VBA first. (CAD sends via Interac and needs no VBA.) See Individual vs business.
A freshly-issued account starts with status: "provisioning" while the banking partner allocates the real account number. While provisioning, account_number is null — do not surface or fund it. Poll GET /v1/customers/{id}/virtual_accounts (or wait for the virtual_account.activated webhook); once status becomes "active" the real account_number is populated. provider_ref is present throughout for correlation.
{ "currency": "GBP" }GETList customer transactions
A unified, audit-grade history of everything attributed to one customer — collections (money received into their international account) and payouts (money sent on their behalf), merged into one time-ordered feed.
Requires the customer_transaction_view permission on the API key, in addition to the international-accounts feature. Grant it under Settings → API Keys. Without it the endpoint returns 403.
1-100, default 25.
Opaque cursor from the previous page’s next_cursor.
Filter: collection or payout.
Filter: successful / pending / failed / reversed.
Filter to one currency.
YYYY-MM-DD range start, inclusive of the whole day. Paired with to_date.
YYYY-MM-DD range end, inclusive of the whole day. Paired with from_date.
Field reference
| Field | Type | Notes |
|---|---|---|
type | string | collection (inflow) or payout (outflow) |
id | string | The underlying record id |
reference | string | For collections, the ledger entry id (le_...). For payouts, the payout reference (swappr_... / po_...). See the asymmetry note below |
amount_minor | string | Principal in minor units (BigInt-safe string) |
fee_minor | string | "0" for collections. For payouts, the combined fee + tax in minor units |
currency | string | ISO 4217 code |
status | string | Rolled-up status — successful / pending / failed / reversed |
provider_reference | string | null | null for collections. For payouts, the NIP session id (or the provider’s internal ref) |
sender | object | null | Present on collections only — currently null (sender capture for customer collections is roadmap) |
recipient | object | Present on payouts — account_name, account_number, bank_code, bank_name |
raw_provider_event_id | string | Collections only — the underlying provider event id |
created_at | string | ISO 8601 timestamp |
Reference shape differs by type. Collections carry the internal ledger entry id (le_...) as their reference; payouts carry the human-readable payout reference (swappr_...). Key any reconciliation off type + id rather than assuming a single reference format across both.
What collection covers: only credits that landed via the customer’s international account. A reversed payout shows up in this feed as a payout row with status: reversed — it does not re-appear as a fresh collection. For the raw ledger view of every credit movement attributed to a customer (including reversal credit-backs), use GET /v1/wallets/{id}/transactions?customer_id={id}.
{
"object": "list",
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0wNS0wNVQxMjozNDo1NloiLCJpIjoiY3VzX3h4eCJ9",
"data": [
{
"object": "customer_transaction",
"type": "collection",
"id": "le_xxxxxxxxxxxx",
"reference": "le_xxxxxxxxxxxx",
"amount_minor": "5000000",
"fee_minor": "0",
"currency": "GBP",
"status": "successful",
"provider_reference": null,
"sender": null,
"raw_provider_event_id": "evt_xxxxxxxxxxxx",
"created_at": "2026-05-05T12:34:56Z"
},
{
"object": "customer_transaction",
"type": "payout",
"id": "po_xxxxxxxxxxxx",
"reference": "swappr_xxxxxxxxxxxx",
"amount_minor": "2500000",
"fee_minor": "1500",
"currency": "GBP",
"status": "successful",
"provider_reference": "TRF_xxxxxxxxxxxx",
"recipient": {
"account_name": "JANE DOE",
"account_number": "12345678",
"bank_code": "200000",
"bank_name": "Example Bank"
},
"created_at": "2026-05-06T09:10:11Z"
}
]
}