Virtual accounts
Funding accounts merchants receive money into. Read-only on the public API, provisioning new merchant-level VAs is admin-only.
If your business model requires per-end-user VIBANs (one virtual IBAN per app user, common in remittance + e-wallet apps), additional capabilities can be enabled on your account. Contact support@the-technest.com for details.
The virtual_account object
{
"object": "virtual_account",
"id": "ckva_xxxxxxxxxxxx",
"currency": "NGN",
"env": "live",
"status": "active",
"account_number": "8001234567",
"account_name": "TECHNEST/MERCHANT NAME",
"bank_name": "Wema Bank",
"bank_code": "035",
"provider": "<provider_slug>",
"provider_ref": "<provider_ref>",
"is_visible_to_merchant": true,
"created_at": "2026-01-15T10:00:00Z"
}Status values
| Status | Description |
|---|---|
provisioning | Created at the provider; awaiting activation webhook |
active | Ready to receive funds |
failed | Provisioning failed |
closed | Deactivated |
GETList virtual accounts
Returns merchant-level VAs. Per-customer VIBANs are excluded, query /v1/customers/{id}/virtual_accounts instead.
1-100, default 50.
Cursor.
Filter to one currency.
Filter: provisioning | active | failed | closed.
curl 'https://api.swappr.me/api/v1/virtual_accounts?currency=NGN&status=active&limit=50' \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"has_more": false,
"data": [
{ "object": "virtual_account", "id": "...", ... },
...
]
}GETRetrieve a virtual account
Returns the same virtual_account shape as the list endpoint.
curl https://api.swappr.me/api/v1/virtual_accounts/ckva_xxx \
-H "Authorization: Bearer sk_live_..."{
"object": "virtual_account",
"id": "ckva_xxxxxxxxxxxx",
"currency": "NGN",
"env": "live",
"status": "active",
"account_number": "8001234567",
"account_name": "TECHNEST/MERCHANT NAME",
"bank_name": "Wema Bank",
"bank_code": "035",
"provider": "<provider_slug>",
"provider_ref": "<provider_ref>",
"is_visible_to_merchant": true,
"created_at": "2026-01-15T10:00:00Z"
}Sub-merchant collections
Not sure this is the right flow? See Which flow am I in?. For a one-off payment link instead of a standing account, use Collections.
Issue a dedicated account for each of your sub-merchants, so money their customers pay lands with you, tagged to the right sub-merchant. Your sub-merchants never sign up on Swappr.
These accounts are issued in NGN, as a NUBAN. Swappr also collects other currencies for customers you onboard through international accounts. Every account settles into your wallet in the account’s currency. You do not hold a separate balance per sub-merchant. You attribute each inflow using the customer_reference you set, and you run the per-sub-merchant accounting in your own system.
Before you can create these accounts: Technest grants your account access, you accept the sub-merchant collections terms in your dashboard, and the API key carries the Create virtual accounts capability. If any of the three is missing the request returns 403.
POSTCreate a sub-merchant account
Creates an NGN account for one of your sub-merchants and returns it. Requires an Idempotency-Key header. Retrying with the same key returns the same account rather than creating a second one.
The sub-merchant’s business name. Appears on the account and on your reconciliation.
The sub-merchant’s Bank Verification Number. Exactly 11 digits. Validated before the account is created; a malformed BVN is rejected with invalid_bvn and no account is provisioned. The BVN is stored encrypted and is never returned.
Must be NGN. A value other than NGN returns unsupported_currency. Other currencies are available for customers you onboard through international accounts.
Your own stable identifier for this sub-merchant. It is echoed back as customer_reference and carried on every wallet_funded webhook, so it is how you attribute inflows. Reusing the same reference returns the same sub-merchant’s account. If omitted, we generate one. A reference already used for a non-sub-merchant customer is rejected with reference_conflict.
curl https://api.swappr.me/api/v1/virtual_accounts \
-X POST \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Stores",
"bvn": "22222222222",
"currency": "NGN",
"reference": "submerchant_8842"
}'{
"object": "virtual_account",
"id": "ckva_xxxxxxxxxxxx",
"currency": "NGN",
"env": "live",
"status": "active",
"account_number": "8001234567",
"account_name": "ADA STORES",
"bank_name": "<bank_name>",
"bank_code": "035",
"customer_reference": "submerchant_8842",
"created_at": "2026-01-15T10:00:00Z"
}A retry with the same Idempotency-Key returns the same account with 200.
Receiving funds
When a sub-merchant’s customer pays into the account, we credit your wallet in the account’s currency and fire a wallet_funded webhook. Its customer.customer_reference is the reference you set, so you know which sub-merchant the money is for. The net amount is what lands in your wallet. Sub-merchant accounts hold no balance of their own, so there is nothing to sweep. Reconcile and pay your sub-merchants from your own records.
The wallet_funded webhook is your single signal for money coming in, across every currency and account type: sub-merchant accounts, international customer accounts, and your own funding accounts. Build one handler keyed on customer.customer_reference and it covers them all.
Sub-merchant accounts do not appear in GET /v1/virtual_accounts; that list is your own funding accounts. Attribute and reconcile sub-merchant inflows from the wallet_funded webhook.
Simulate an inbound payment (sandbox)
Testing your wallet_funded handling should not require real money moving. In the sandbox environment you can fire a simulated inbound payment at one of your own sub-merchant accounts. It runs through the same path a real inbound payment takes: your test wallet is credited, the inflow is attributed to the sub-merchant, and a wallet_funded webhook is queued, so you can exercise your webhook endpoint and your reconciliation end to end.
This endpoint is sandbox-only. Authenticate with a sk_test_ key. A sk_live_ key returns 400 sandbox_only, and the account number is only ever matched against your own sandbox accounts.
POSTSimulate a payment
Credits one of your sandbox sub-merchant accounts as if a customer paid into it.
The account number of one of your own sandbox sub-merchant accounts.
The amount to credit, in the currency minor unit (kobo for NGN). Must be a positive integer. For example, 500000 credits ₦5,000.00.
The payer name. Shown on the transaction and used for attribution and screening.
Optional. The payer’s account number.
Optional. The payer’s bank code.
curl https://api.swappr.me/api/v1/sandbox/simulate-payment \
-X POST \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"account_number": "8001234567",
"amount_minor": 500000,
"sender_name": "Ada Obi"
}'{
"object": "simulated_payment",
"virtual_account_number": "8001234567",
"amount_minor": 500000,
"currency": "NGN",
"credited": true,
"transaction_reference": "SIM_8f2c1e90-..."
}Your sandbox wallet is credited and a wallet_funded webhook is queued, exactly as a real inbound payment would.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | sandbox_only | The request used a live key or a live environment. Use a sk_test_ key. |
400 | missing_field | account_number or sender_name is missing. |
400 | invalid_field | amount_minor is not a positive integer, or the body is not valid JSON. |
404 | virtual_account_not_found | No sandbox account with that number belongs to your account. |