curl --request POST \
--url https://api.rolla.xyz/api/v1/external/beneficiaries \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "NGN",
"label": "Office rent",
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014",
"email": "john@example.com"
}
'import requests
url = "https://api.rolla.xyz/api/v1/external/beneficiaries"
payload = {
"currency": "NGN",
"label": "Office rent",
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014",
"email": "john@example.com"
}
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',
label: 'Office rent',
account_name: 'JOHN DOE',
account_number: '0123456789',
bank_name: 'Access Bank',
bank_code: '000014',
email: 'john@example.com'
})
};
fetch('https://api.rolla.xyz/api/v1/external/beneficiaries', 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/beneficiaries",
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',
'label' => 'Office rent',
'account_name' => 'JOHN DOE',
'account_number' => '0123456789',
'bank_name' => 'Access Bank',
'bank_code' => '000014',
'email' => 'john@example.com'
]),
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/beneficiaries"
payload := strings.NewReader("{\n \"currency\": \"NGN\",\n \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\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/beneficiaries")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"NGN\",\n \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rolla.xyz/api/v1/external/beneficiaries")
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 \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Beneficiary already saved",
"success": true,
"data": {
"id": "e5f6a7b8-c9d0-1234-efab-567890123456",
"account_name": "Acme Corp",
"currency": "USD",
"withdrawal_method": "rolla_transfer",
"label": "Partner company"
}
}{
"status": 201,
"message": "Beneficiary created successfully",
"success": true,
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"account_name": "JOHN DOE",
"currency": "NGN",
"email": "john@example.com",
"withdrawal_method": null,
"label": "Office rent",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014"
}
}{
"status": 400,
"message": "Validation failed",
"errors": [
{
"code": "custom",
"message": "Routing number: routing_number is required for ACH and domestic wire beneficiaries",
"path": [
"routing_number"
]
}
]
}{
"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)"
}
]
}Create Beneficiary
Creates a new saved beneficiary for the business. The required fields depend on withdrawal_method and currency. Returns 201 for a new record; saving a rolla_transfer recipient that is already saved returns the existing record with 200.
curl --request POST \
--url https://api.rolla.xyz/api/v1/external/beneficiaries \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"currency": "NGN",
"label": "Office rent",
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014",
"email": "john@example.com"
}
'import requests
url = "https://api.rolla.xyz/api/v1/external/beneficiaries"
payload = {
"currency": "NGN",
"label": "Office rent",
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014",
"email": "john@example.com"
}
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',
label: 'Office rent',
account_name: 'JOHN DOE',
account_number: '0123456789',
bank_name: 'Access Bank',
bank_code: '000014',
email: 'john@example.com'
})
};
fetch('https://api.rolla.xyz/api/v1/external/beneficiaries', 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/beneficiaries",
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',
'label' => 'Office rent',
'account_name' => 'JOHN DOE',
'account_number' => '0123456789',
'bank_name' => 'Access Bank',
'bank_code' => '000014',
'email' => 'john@example.com'
]),
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/beneficiaries"
payload := strings.NewReader("{\n \"currency\": \"NGN\",\n \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\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/beneficiaries")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"currency\": \"NGN\",\n \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.rolla.xyz/api/v1/external/beneficiaries")
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 \"label\": \"Office rent\",\n \"account_name\": \"JOHN DOE\",\n \"account_number\": \"0123456789\",\n \"bank_name\": \"Access Bank\",\n \"bank_code\": \"000014\",\n \"email\": \"john@example.com\"\n}"
response = http.request(request)
puts response.read_body{
"status": 200,
"message": "Beneficiary already saved",
"success": true,
"data": {
"id": "e5f6a7b8-c9d0-1234-efab-567890123456",
"account_name": "Acme Corp",
"currency": "USD",
"withdrawal_method": "rolla_transfer",
"label": "Partner company"
}
}{
"status": 201,
"message": "Beneficiary created successfully",
"success": true,
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"account_name": "JOHN DOE",
"currency": "NGN",
"email": "john@example.com",
"withdrawal_method": null,
"label": "Office rent",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014"
}
}{
"status": 400,
"message": "Validation failed",
"errors": [
{
"code": "custom",
"message": "Routing number: routing_number is required for ACH and domestic wire beneficiaries",
"path": [
"routing_number"
]
}
]
}{
"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)"
}
]
}withdrawal_method and currency.
account_owner_type, account_category). Every bank-account
beneficiary except NGN must now declare account_owner_type (individual or business), and
every USD beneficiary must also declare account_category (checking or savings).
During a 60-day grace period, requests that omit these fields still succeed but return a
deprecation_warning in the response; after the enforcement date they are rejected with a
400. See Beneficiary Account Fields
for the full migration guide.Example Request (NGN Bank Transfer)
For Nigerian Naira transfers, omitwithdrawal_method (or set it to null). bank_code is always required for NGN.
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "NGN",
"label": "Office rent",
"account_name": "JOHN DOE",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014",
"email": "john@example.com"
}'
Example Request (Domestic Wire — USD)
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"withdrawal_method": "domestic_wire",
"label": "US Supplier",
"account_name": "John Doe",
"account_number": "123456789",
"bank_name": "Chase Bank",
"routing_number": "021000021",
"account_owner_type": "business",
"account_category": "checking",
"beneficiary_address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
},
"bank_address": {
"street": "270 Park Ave",
"city": "New York",
"state": "NY",
"postalCode": "10017",
"country": "US"
}
}'
Example Request (ACH — USD)
ACH is the low-cost domestic US rail. It reaches the same US bank accounts as a domestic wire but settles in business days rather than same-day, and is usually priced lower — so quote the fee per rail rather than assuming they match. Useaccount_category to say whether the destination is a checking or savings account.
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"withdrawal_method": "ach",
"label": "US Payroll",
"account_name": "John Doe",
"account_number": "123456789",
"bank_name": "Chase Bank",
"routing_number": "021000021",
"account_owner_type": "individual",
"account_category": "checking",
"beneficiary_address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
},
"bank_address": {
"street": "270 Park Ave",
"city": "New York",
"state": "NY",
"postalCode": "10017",
"country": "US"
}
}'
routing_number is the beneficiary bank’s 9-digit ABA number and is required for both ach and domestic_wire — those rails are addressed by routing number, so a beneficiary saved without one can never be paid and is rejected with a 400. international_wire uses swift_code instead and is unaffected.domestic_wire requirement is newly enforced. It has always been listed as required below, but the server previously accepted domestic wire beneficiaries without a routing_number. If you have saved any, backfill them via PATCH /beneficiaries/{beneficiaryId} — otherwise the next update to that beneficiary will be rejected.Example Request (International Wire — USD)
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"withdrawal_method": "international_wire",
"label": "US Supplier",
"account_name": "John Doe",
"account_number": "123456789",
"bank_name": "Barclays Bank",
"swift_code": "BARCGB22",
"account_owner_type": "business",
"account_category": "checking",
"beneficiary_address": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
},
"bank_address": {
"street": "1 Churchill Place",
"city": "London",
"state": "England",
"postalCode": "E14 5HP",
"country": "GB"
}
}'
Example Request (Crypto USDC)
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"withdrawal_method": "crypto_usdc",
"label": "Base USDC wallet",
"account_name": "My USDC Wallet",
"wallet_address": "0x1234567890abcdef1234567890abcdef12345678",
"wallet_chain": "base"
}'
Example Request (Rolla Transfer)
For sending funds to another Rolla business. Userecipient_business_id to identify the target business. You can send in any supported currency.
curl -X POST "https://api.rolla.xyz/api/v1/external/beneficiaries" \
-H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"withdrawal_method": "rolla_transfer",
"label": "Partner company",
"account_name": "Acme Corp",
"recipient_business_id": "d4e5f6a7-b8c9-0123-defa-456789012345"
}'
recipient_business_id is the Rolla-assigned UUID of the destination business. A Rolla transfer beneficiary is not scoped by currency: one record per recipient serves every currency you pay them in. Saving the same recipient_business_id again returns the existing record with 200 and the message Beneficiary already saved (a new label, if sent, is applied).Example Response
A new beneficiary returns201. The beneficiary is returned directly under data; only populated fields are included, except withdrawal_method, which is always present and is null when no rail was set.
{
"status": 201,
"message": "Beneficiary created successfully",
"success": true,
"data": {
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"account_name": "JOHN DOE",
"currency": "NGN",
"email": "john@example.com",
"withdrawal_method": null,
"label": "Office rent",
"account_number": "0123456789",
"bank_name": "Access Bank",
"bank_code": "000014"
}
}
Validation error
Requests that fail schema validation return400 with one entry per offending field:
{
"status": 400,
"message": "Validation failed",
"errors": [
{
"code": "custom",
"message": "Routing number: routing_number is required for ACH and domestic wire beneficiaries",
"path": ["routing_number"]
}
]
}
400 with a descriptive message and no errors array.
Account Classification Fields
For compliant payout routing, bank-account beneficiaries carry two classification fields:| Field | Values | Required when |
|---|---|---|
account_owner_type | individual, business | Every bank-account beneficiary except NGN — any fiat method that is not crypto, mobile_money or rolla_transfer (ach, domestic_wire, international_wire). Accepted but not required for NGN. Not applicable to crypto, mobile_money or rolla_transfer. |
account_category | checking, savings | Only when currency is USD. If you are not sure, use checking. |
deprecation_warning object in the
response (see below). After the enforcement date, the same request is rejected with a 400.
Send the fields now to avoid disruption. Full details in the
Beneficiary Account Fields migration guide.Deprecation warning (during grace period)
{
"status": 201,
"message": "Beneficiary created successfully",
"success": true,
"data": { "...": "..." },
"deprecation_warning": {
"code": "BENEFICIARY_FIELDS_REQUIRED_SOON",
"message": "The field(s) account_owner_type will soon be required for beneficiaries. account_owner_type (individual|business) is required for bank-account beneficiaries except NGN; account_category (checking|savings) is required for USD beneficiaries.",
"missing_fields": ["account_owner_type"],
"enforcement_date": "2026-12-01T00:00:00.000Z"
}
}
enforcement_date is null until the date has been set.
Required Fields by Withdrawal Method
| Method | Required Fields |
|---|---|
NGN bank transfer (no withdrawal_method) | currency, account_name, account_number, bank_name, bank_code |
ach (USD) | currency, account_name, account_number, bank_name, routing_number, beneficiary_address, bank_address, account_owner_type, account_category |
domestic_wire (USD) | currency, account_name, account_number, bank_name, routing_number, beneficiary_address, bank_address, account_owner_type, account_category |
international_wire (USD) | currency, account_name, account_number, bank_name, swift_code, beneficiary_address, bank_address, account_owner_type, account_category |
crypto_usdc / crypto_usdt | currency, account_name, wallet_address, wallet_chain |
rolla_transfer | currency, account_name, recipient_business_id |
mobile_money | currency, mobile_money_provider, phone_number |
beneficiary_address and bank_address are objects with the following fields:
| Field | Required | Notes |
|---|---|---|
street | Yes | Street address |
city | Yes | City |
state | Yes | State or province |
postalCode | Yes | Zip/postal code |
country | Yes | 2-letter ISO 3166-1 country code (e.g. US, GB); a full country name is rejected |
ach, domestic_wire and international_wire are rejected with a 400 validation error naming each part (for example bank_address.city).
/beneficiaries/beneficiary-lookup endpoint first to validate the account number and retrieve the correct account name before saving.Authorizations
Your Rolla API key
Body
Which other fields are required depends on withdrawal_method and currency; see the "Required Fields by Withdrawal Method" table on Create Beneficiary. The same schema is validated as a whole on PATCH and PUT.
Currency code (e.g., NGN, USD). Must be a supported wallet or FX corridor currency.
10"NGN"
Friendly label for the beneficiary
"Office rent"
Account holder name. Required on every rail except mobile_money (for crypto and rolla_transfer it is the nickname).
100"JOHN DOE"
Bank account number. Required for ach, domestic_wire and international_wire; surrounding whitespace is trimmed.
50"0123456789"
Bank name. Required for ach, domestic_wire and international_wire.
100"Access Bank"
Nigerian bank code from List Nigerian Banks. Required whenever currency is NGN and account_number is sent.
20"000014"
Postal address. On ach, domestic_wire and international_wire every part is required and country must be a two-letter ISO 3166-1 code; a missing part is rejected with a 400 naming the field (e.g. bank_address.city). Optional on every other rail.
Show child attributes
Show child attributes
SWIFT/BIC code (required for international wire)
20Beneficiary email
100"john@example.com"
Contact person name
100Postal address. On ach, domestic_wire and international_wire every part is required and country must be a two-letter ISO 3166-1 code; a missing part is rejected with a 400 naming the field (e.g. bank_address.city). Optional on every other rail.
Show child attributes
Show child attributes
Payout rail. Omit (or send null) for an NGN bank transfer. On update, omit to keep the current rail; null or "" clears it.
domestic_wire, international_wire, ach, local_transfer, crypto_usdt, crypto_usdc, rolla_transfer, mobile_money 9-digit ABA routing number. Required for ach and domestic_wire.
20Crypto wallet address (required for crypto_usdt / crypto_usdc). 26 to 64 alphanumeric characters; validated against the network.
26 - 64Blockchain network slug (required for crypto_usdt / crypto_usdc), e.g. base, tron, ethereum. Stored as the canonical slug.
50Intermediary bank name
255Intermediary bank routing number
50IBAN, for banks that use one instead of an account number
50BIC, where it differs from swift_code
20UK sort code
10Required for rolla_transfer. The UUID of the destination Rolla business (not your own).
"d4e5f6a7-b8c9-0123-defa-456789012345"
Whether the account is held by an individual or a business. Required for every bank-account beneficiary except NGN (ach, domestic_wire, international_wire). Accepted but not required for NGN. Not applicable to crypto_usdt, crypto_usdc, mobile_money or rolla_transfer. During the deprecation grace period, requests that omit this field still succeed but return a deprecation_warning object; after the enforcement date the request is rejected with a 400. See the Beneficiary Account Fields migration guide.
individual, business "business"
Whether a USD account is checking or savings. Required only when currency is USD. If you are not sure, use "checking". Subject to the same grace-period behaviour as account_owner_type.
checking, savings "checking"
Mobile money operator slug (required for mobile_money)
100Mobile money wallet number (required for mobile_money)
30Response
Beneficiary already saved - a rolla_transfer recipient_business_id you had already saved; the existing record is returned
200
"Beneficiary already saved"
true
A saved beneficiary as returned to API-key callers. Only populated fields are included: a fiat beneficiary never carries wallet fields and a crypto beneficiary never carries bank fields, and empty values are dropped. withdrawal_method is always present and is null when no rail was set. Timestamps and internal ids (business_id, recipient_business_id) are not returned.
Show child attributes
Show child attributes