# 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](api:listAccountCurrencies) 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:

```json
{ "amount": 1200.5 }
```

- **Major units, not minor units:** send `1200.5` for ₦1,200.50, not `120050`. (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](api:createSingleTransfer), [Validate a transfer](api:validateTransfer), [Initiate a remittance](api:createRemittance) | `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](api:refundInflow) | `amount` (optional) | **No** | Above zero, at most two decimals, no more than the inflow. |
| [Create an account](api:createAccount) (dynamic account) | `amount` (optional) | **No** | At least `0.01`, at most two decimals. NGN only. |
| [Create a receivable](api:createReceivable) | `amount` | Yes | At least `0.01`. |
| [Get a quote](api:getFxQuote) | `amount` (query) | — | Above zero. |
| [Lock a rate](api:lockFxRate) | `amount` | Yes | Above zero. Use the amount you quoted. |

Bulk payout files carry amounts as text in a file column: see [Bulk payouts](../04-products/06-bulk-payouts.md).

## 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_completed` webhook | Decimal strings (sometimes numbers on create) |
| [Get an account balance](api:getAccountBalance) | Decimal strings |
| Account objects (`availableBalance`) | 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.

<Tabs>
<Tab title="Node.js">

Store money as integer minor units (`BigInt`).

```javascript
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
```

</Tab>
<Tab title="Python">

Use `Decimal`, built from a string.

```python
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
```

</Tab>
</Tabs>

:::warning[Never use floating point for money]
Adding `0.1 + 0.2` in floating point gives `0.30000000000000004`. 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 full `amount`; `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](api:getTransferFee).

## Dates and times

Timestamps are ISO 8601 in UTC, for example `2026-10-10T09:30:00.482Z`. Date-only query parameters (`startDate`, `from`) take `YYYY-MM-DD`.
