curl --request POST \
--url https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "NGN",
"accountData": {
"bvn": "22211122233"
}
}
'import requests
url = "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts"
payload = {
"currency": "NGN",
"accountData": { "bvn": "22211122233" }
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({currency: 'NGN', accountData: {bvn: '22211122233'}})
};
fetch('https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'currency' => 'NGN',
'accountData' => [
'bvn' => '22211122233'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts"
payload := strings.NewReader("{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Client already onboarded",
"success": true,
"data": {
"status": "already_exists",
"message": "An account already exists for this reference; returning it.",
"bankAccount": {
"id": "c81f0a44-2d6e-4a5b-9c31-77b0e2d1f904",
"currency": "USD",
"bankName": "Lead Bank",
"accountNumber": "8823410077",
"accountName": "Beta Logistics LLC",
"routingNumber": "021000021",
"swiftCode": "LEADUS33",
"bankAddress": null,
"type": "static",
"status": "active",
"reference": "store-amazon-uk",
"label": "Amazon UK",
"expiresAt": null,
"createdAt": "2026-06-14T09:22:11.000Z"
}
}
}{
"status": 201,
"message": "Bank account issued successfully",
"success": true,
"data": {
"status": "issued",
"bankAccount": {
"id": "7f3a2b10-91c4-4f3b-b1d2-0a8e44c10a55",
"currency": "NGN",
"bankName": "Guaranty Trust Bank",
"accountNumber": "1238726395",
"accountName": "Beta Logistics LLC",
"routingNumber": null,
"swiftCode": null,
"bankAddress": null,
"type": "static",
"status": "active",
"reference": null,
"label": null,
"expiresAt": null,
"createdAt": "2026-06-13T01:50:00.000Z"
}
}
}{
"status": 202,
"message": "Bank account request accepted",
"success": true,
"data": {
"status": "submitted_for_review",
"requestId": "185b0aaf-ba79-4f9b-9c6d-5e0587d14314",
"provider": "rolla",
"message": "Account details were submitted for review. The deposit account becomes available once approved; retry this endpoint to collect it."
}
}{
"status": 400,
"message": "Missing required details: bvn. Provide them in accountData or complete the application.",
"code": "MISSING_ACCOUNT_DETAILS",
"provider": "rolla",
"missingFields": [
"bvn"
]
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 409,
"message": "This deposit-account request was declined: Business registration document could not be verified.",
"code": "REQUEST_DECLINED"
}Issue Bank Account
Issues an NGN or USD deposit account for an approved account. The provider is chosen automatically based on currency and account type. NGN accounts are issued immediately (201). USD accounts require review: the first call records the request and returns 202; call again after approval to collect the deposit account details (201). A repeat call for an account that already exists returns it with 200. Pass reference to open an additional USD deposit account under the same client.
curl --request POST \
--url https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "NGN",
"accountData": {
"bvn": "22211122233"
}
}
'import requests
url = "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts"
payload = {
"currency": "NGN",
"accountData": { "bvn": "22211122233" }
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({currency: 'NGN', accountData: {bvn: '22211122233'}})
};
fetch('https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'currency' => 'NGN',
'accountData' => [
'bvn' => '22211122233'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts"
payload := strings.NewReader("{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rolla.xyz/api/v1/external/accounts/{accountId}/bank-accounts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"currency\": \"NGN\",\n \"accountData\": {\n \"bvn\": \"22211122233\"\n }\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Client already onboarded",
"success": true,
"data": {
"status": "already_exists",
"message": "An account already exists for this reference; returning it.",
"bankAccount": {
"id": "c81f0a44-2d6e-4a5b-9c31-77b0e2d1f904",
"currency": "USD",
"bankName": "Lead Bank",
"accountNumber": "8823410077",
"accountName": "Beta Logistics LLC",
"routingNumber": "021000021",
"swiftCode": "LEADUS33",
"bankAddress": null,
"type": "static",
"status": "active",
"reference": "store-amazon-uk",
"label": "Amazon UK",
"expiresAt": null,
"createdAt": "2026-06-14T09:22:11.000Z"
}
}
}{
"status": 201,
"message": "Bank account issued successfully",
"success": true,
"data": {
"status": "issued",
"bankAccount": {
"id": "7f3a2b10-91c4-4f3b-b1d2-0a8e44c10a55",
"currency": "NGN",
"bankName": "Guaranty Trust Bank",
"accountNumber": "1238726395",
"accountName": "Beta Logistics LLC",
"routingNumber": null,
"swiftCode": null,
"bankAddress": null,
"type": "static",
"status": "active",
"reference": null,
"label": null,
"expiresAt": null,
"createdAt": "2026-06-13T01:50:00.000Z"
}
}
}{
"status": 202,
"message": "Bank account request accepted",
"success": true,
"data": {
"status": "submitted_for_review",
"requestId": "185b0aaf-ba79-4f9b-9c6d-5e0587d14314",
"provider": "rolla",
"message": "Account details were submitted for review. The deposit account becomes available once approved; retry this endpoint to collect it."
}
}{
"status": 400,
"message": "Missing required details: bvn. Provide them in accountData or complete the application.",
"code": "MISSING_ACCOUNT_DETAILS",
"provider": "rolla",
"missingFields": [
"bvn"
]
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 400,
"message": "Validation failed",
"code": "ACCOUNT_RESTRICTED",
"errors": [
{
"path": [
"amount"
],
"message": "amount must be a whole number of smallest currency units (e.g. cents)"
}
]
}{
"status": 409,
"message": "This deposit-account request was declined: Business registration document could not be verified.",
"code": "REQUEST_DECLINED"
}accountData.
NGN accounts
NGN accounts are issued immediately.curl -X POST "https://api.rolla.xyz/api/v1/external/accounts/eec3cbed-79d8-4370-87a0-b6be9e287337/bank-accounts" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "NGN",
"accountData": {
"bvn": "22211122233"
}
}'
{
"status": 201,
"message": "Bank account issued successfully",
"success": true,
"data": {
"status": "issued",
"bankAccount": {
"id": "7f3a2b10-91c4-4f3b-b1d2-0a8e44c10a55",
"currency": "NGN",
"bankName": "Guaranty Trust Bank",
"accountNumber": "1238726395",
"accountName": "Beta Logistics LLC",
"routingNumber": null,
"swiftCode": null,
"bankAddress": null,
"type": "static",
"status": "active",
"reference": null,
"label": null,
"expiresAt": null,
"createdAt": "2026-06-13T01:50:00.000Z"
}
}
}
accountData, the call returns 400 with code MISSING_ACCOUNT_DETAILS listing them (see Errors).
USD accounts
account.virtual_account.created. That is an internal name, not a product distinction.The only real distinction is primary vs additional, and it is visible on every account as the reference field: null on the primary, set on the ones you named. See Multiple USD deposit accounts below.202 Accepted; the deposit account is set up once the request is approved:
curl -X POST "https://api.rolla.xyz/api/v1/external/accounts/eec3cbed-79d8-4370-87a0-b6be9e287337/bank-accounts" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"accountData": {
"onboardingDetails": {
"businessInfo": { "tradeName": "Beta Logistics", "website": "https://betalogistics.com" }
},
"representatives": [{
"relatedPersonId": "a3d4f21e-f499-488f-8508-43228dfea485",
"taxNumber": "123-45-6789",
"idDocument": {
"number": "P123456789",
"type": "passport",
"issueDate": "2020-01-01",
"expirationDate": "2030-01-01"
}
}]
}
}'
{
"status": 202,
"message": "Bank account request accepted",
"success": true,
"data": {
"status": "submitted_for_review",
"requestId": "185b0aaf-ba79-4f9b-9c6d-5e0587d14314",
"provider": "rolla",
"message": "Account details were submitted for review. The deposit account becomes available once approved; retry this endpoint to collect it."
}
}
requestId identifies this request — quote it when asking us about a request that is taking longer than you expect.
Call the endpoint again once the account is approved — the response then includes the deposit account with accountNumber, routingNumber and swiftCode. While review is in progress, repeat calls keep returning 202 with the same requestId, so retrying is safe and never creates a second request.
Two other 202 bodies can come back while you wait. If the review asks you to correct something, data.status is changes_requested and data.message carries the note; re-submit with corrected accountData. Once the request is approved on our side but the banking partner is still finishing its own checks, data.status is pending_provider_review (with the provider state and no requestId); keep retrying until the account is returned. If the request was declined, the call returns 409 with code REQUEST_DECLINED (see Errors).
account.virtual_account.created — it fires as soon as the deposit account exists, with the account details in the payload.Individual USD accounts
Individual accounts have no representatives. When the account was onboarded with its identity details via Update Individual Details (idDocument, address.line2, and a tax number from taxId / nin / bvn) —
plus the passport/ID from the completed Sumsub KYC — no extra payload is needed:
{ "currency": "USD" }
accountData.onboardingDetails to override a field that wasn’t
set on the account, e.g.:
{
"currency": "USD",
"accountData": {
"onboardingDetails": {
"idDocument": { "type": "passport", "number": "P123456789", "issueDate": "2020-01-01", "expirationDate": "2030-01-01" }
}
}
}
Multiple USD deposit accounts
A client can hold more than one USD deposit account, each with its own account number. Pass areference — your own stable id for whatever you need to reconcile separately, such as a storefront, a branch, a marketplace or a channel:
curl -X POST "https://api.rolla.xyz/api/v1/external/accounts/eec3cbed-79d8-4370-87a0-b6be9e287337/bank-accounts" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"reference": "store-amazon-uk",
"label": "Amazon UK"
}'
{
"status": 201,
"message": "Bank account issued successfully",
"success": true,
"data": {
"status": "created",
"message": "Additional deposit account created.",
"bankAccount": {
"id": "c81f0a44-2d6e-4a5b-9c31-77b0e2d1f904",
"currency": "USD",
"bankName": "Lead Bank",
"accountNumber": "8823410077",
"accountName": "Beta Logistics LLC",
"routingNumber": "021000021",
"swiftCode": "LEADUS33",
"bankAddress": null,
"type": "static",
"status": "active",
"reference": "store-amazon-uk",
"label": "Amazon UK",
"expiresAt": null,
"createdAt": "2026-06-14T09:22:11.000Z"
}
}
}
reference and label are echoed back on List Bank Accounts and Get Funding Instructions, so you can hand the right details to the right source without keeping your own mapping.
The client’s primary account is the one issued without a reference, and it reports reference: null.
That field is the reliable way to tell the two apart, and it is returned on List Bank Accounts and Get Funding Instructions as well as at creation — so you can classify an account at any time rather than having to remember the order you created things in. A client can only ever hold one account with reference: null: a second call without a reference returns the existing account rather than opening another.
status: already_exists, so a retry, a duplicated job or a replayed request can never mint a second account for one source. There is no cap on how many references a client can have.reference) returns the existing account with 200 instead of 201:
{
"status": 200,
"message": "Client already onboarded",
"success": true,
"data": {
"status": "already_exists",
"message": "An account already exists for this reference; returning it.",
"bankAccount": {
"id": "c81f0a44-2d6e-4a5b-9c31-77b0e2d1f904",
"currency": "USD",
"bankName": "Lead Bank",
"accountNumber": "8823410077",
"accountName": "Beta Logistics LLC",
"routingNumber": "021000021",
"swiftCode": "LEADUS33",
"bankAddress": null,
"type": "static",
"status": "active",
"reference": "store-amazon-uk",
"label": "Amazon UK",
"expiresAt": null,
"createdAt": "2026-06-14T09:22:11.000Z"
}
}
}
reference first, or the call is rejected.Additional accounts are USD only. A reference with currency: "NGN" returns 400; for per-customer NGN accounts use the customer virtual account endpoint instead. Not every USD provider can issue more than one account per client — where it can’t, the call returns 400 explaining so.Key Behaviours
accountData.representatives are merged into the prefilled representative with the same relatedPersonId, so you only send the fields you’re adding — names, addresses and ownership come from the application. Individual accounts use accountData.onboardingDetails instead (see above).errors (USD) or missingFields (NGN).reference returns the client’s existing account with status: already_exists (200) rather than creating another — safe to retry. A USD call with a reference behaves the same way per reference. NGN is one account per provider and returns 409 Conflict once one exists.Errors
Error bodies carrystatus and message; the ones below also carry a code.
400 with code MISSING_ACCOUNT_DETAILS when an NGN provider still needs fields you have not supplied:
{
"status": 400,
"message": "Missing required details: bvn. Provide them in accountData or complete the application.",
"code": "MISSING_ACCOUNT_DETAILS",
"provider": "rolla",
"missingFields": ["bvn"]
}
400 with code APPLICATION_NOT_APPROVED when the account’s application is not yet approved:
{
"status": 400,
"message": "The account application must be approved before bank accounts can be issued",
"code": "APPLICATION_NOT_APPROVED"
}
409 with code REQUEST_DECLINED when a USD request was declined during review. The message includes the decision note when one was left:
{
"status": 409,
"message": "This deposit-account request was declined: Business registration document could not be verified.",
"code": "REQUEST_DECLINED"
}
400 with an errors array; a duplicate NGN issuance returns 409 with a plain message; malformed request bodies return 400 with "message": "Validation failed" and an errors array.Authorizations
Your Rolla API key
Path Parameters
Identifier of an account owned by the same user as your API key's business
"eec3cbed-79d8-4370-87a0-b6be9e287337"
Body
NGN, USD "NGN"
Optional overrides merged over the details prefilled from the account's application. The shape depends on the provider: call GET /accounts/{accountId}/bank-accounts/requirements to see the prefilled payload and what is missing. For USD business accounts, entries in representatives merge into the prefilled person with the same relatedPersonId; USD individual accounts use onboardingDetails only.
{ "bvn": "22211122233" }
Your own stable id for whatever you need to reconcile separately (a storefront, a branch, a marketplace, a channel). Supplying it issues an additional USD deposit account under the same client, with its own account number, so incoming funds are attributable to that one reference. Omit it for the client's primary account.
Idempotent: the same reference always returns the same account, so a retry cannot create a duplicate. USD only: sending it with currency: "NGN" returns 400. The client must already hold its primary USD account.
1 - 100"store-amazon-uk"
Human-readable name stored alongside the account and echoed back on reads, e.g. "Amazon UK". Only meaningful together with reference.
1 - 120"Amazon UK"