Concepts
Amounts and currencies
How to send amounts, how they come back, and how to handle them without rounding errors.
Currencies and countries
- Currencies use ISO 4217 three-letter codes in upper case:
NGN,USD,GHS. - Countries use ISO 3166-1 alpha-3 codes in upper case:
NGA,GHA,USA,GBR. A few fields in bulk files also accept alpha-2 codes; the field descriptions say so.
Which currencies you can hold, collect and pay out in depends on the partner banks configured for your business. List currencies you hold shows the currencies of your active accounts.
Sending amounts
Send amounts in major units (naira, dollars), as JSON numbers, with at most two decimal places:
{ "amount": 1200.5 }
- Major units, not minor units: send
1200.5for ₦1,200.50, not120050. (Minor units are the smallest unit of a currency, such as kobo or cents.) - Greater than zero. Zero or a negative amount is refused with
400. - Numbers, not strings. Some endpoints also accept a numeric string such as
"1200.50", but not all of them (see the table). A JSON number always works. - At most two decimal places. Round in your own code before you send.
| Endpoint | Field | String accepted? | Rules |
|---|---|---|---|
| Create a transfer, Validate a transfer, Initiate a remittance | amount |
Yes | Above zero, otherwise "Invalid amount". Stored with two decimals: a third decimal is rounded, so the stored amount may not match what you recorded. |
| Refund an inflow | amount (optional) |
No | Above zero, at most two decimals, no more than the inflow. |
| Create an account (dynamic account) | amount (optional) |
No | At least 0.01, at most two decimals. NGN only. |
| Create a receivable | amount |
Yes | At least 0.01. |
| Get a quote | amount (query) |
— | Above zero. |
| Lock a rate | amount |
Yes | Above zero. Use the amount you quoted. |
Bulk payout files carry amounts as text in a file column: see Bulk payouts.
Amounts in responses
Responses aren't consistent yet. Depending on the endpoint and the path through it, an amount can arrive as a number (250000) or a decimal string ("250000.00"):
| Where | Type |
|---|---|
| Transfers (create, find) | Usually decimal strings; numbers for split transfers |
| Transfer webhooks | Numbers |
| Inflow webhooks | Decimal strings, with no fixed number of decimals ("150000", "1500.5") |
Conversions and the trade_ webhook |
Decimal strings (sometimes numbers on create) |
| Get an account balance | Decimal strings |
Account objects (available) |
Numbers |
Refund response (amount) |
A number for a partial refund; can be a decimal string for a full refund |
| Receivables | Numbers |
Write one parser that accepts both and converts to an exact type immediately. Never use the value as a float.
Store money as integer minor units (BigInt).
const DECIMALS = 2;
const AMOUNT_PATTERN = /^-?\d+(\.\d+)?$/;
// "250000.00", 250000 or 1200.5 -> integer minor units (25000000n, 120050n)
export function toMinorUnits(value) {
const text = String(value).trim();
if (!AMOUNT_PATTERN.test(text)) throw new Error(`Not a plain decimal amount: ${text}`);
const negative = text.startsWith('-');
const [whole, fraction = ''] = text.replace('-', '').split('.');
if (fraction.replace(/0+$/, '').length > DECIMALS) {
throw new Error(`More than ${DECIMALS} decimal places: ${text}`); // don't round money silently
}
const units = BigInt(whole) * 10n ** BigInt(DECIMALS) + BigInt(fraction.padEnd(DECIMALS, '0').slice(0, DECIMALS));
return negative ? -units : units;
}
// Integer minor units -> the JSON number to send (120050n -> 1200.5)
export function toRequestAmount(minorUnits) {
return Number(minorUnits) / 10 ** DECIMALS;
}
toMinorUnits(250000); // 25000000n
toMinorUnits('250000.00'); // 25000000n
toMinorUnits('1500.5'); // 150050n
toRequestAmount(120050n); // 1200.5
Use Decimal, built from a string.
from decimal import Decimal, InvalidOperation
CENT = Decimal("0.01")
def to_decimal(value):
# str() first, so 0.1 arriving as a JSON float doesn't become 0.1000000000000000055...
try:
amount = Decimal(str(value))
except InvalidOperation:
raise ValueError(f"Not an amount: {value!r}")
if amount != amount.quantize(CENT):
raise ValueError(f"More than 2 decimal places: {value!r}") # don't round money silently
return amount.quantize(CENT)
def to_request_amount(amount):
# Decimal -> the JSON number to send; json.dumps can't write a Decimal
return float(amount.quantize(CENT))
to_decimal(250000) # Decimal('250000.00')
to_decimal("250000.00") # Decimal('250000.00')
to_decimal("1500.5") # Decimal('1500.50')
to_request_amount(Decimal("1200.50")) # 1200.5
Adding 0.1 + 0.2 in floating point gives 0.. Over thousands of payments this drifts and your books stop matching your statements. Keep integer minor units or a decimal type in your database and code, and convert to a JSON number only when you build the request.
Fees
- Payouts: the fee (
charge) is charged to the source account on top of the amount. The beneficiary receives the fullamount;totalAmount=amount+charge. - Conversions: fees and margin are built into the rate. There is no separate fee field.
- Refunds: no fee is charged.
Quote payout fees in advance with Get a transfer fee.
Dates and times
Timestamps are ISO 8601 in UTC, for example 2026-10-10. Date-only query parameters (startDate, from) take YYYY-MM-DD.