Balances
The accounts you hold with us, what each number means, how fresh it is, and why it is not the number to build a spending check on.
GET /v1/balances returns the accounts you hold with us and what is available
in each. Every credential gets your KES account under kes. A Collections
credential also gets your USD account under usd.
Response for a USDT Rails credential — 200 OK:
{
"kes": {
"available": "150000.50",
"accountNumber": "00000000123456",
"currency": "KES",
"asOf": "2026-09-08T12:00:00.000Z"
}
}Response for a Collections credential — 200 OK:
{
"kes": {
"available": "150000.50",
"accountNumber": "00000000123456",
"currency": "KES",
"asOf": "2026-09-08T12:00:00.000Z"
},
"usd": {
"available": "98.50",
"accountNumber": "00000000000000",
"currency": "USD",
"asOf": "2026-09-08T12:00:00.000Z",
"unavailableReason": null
}
}It needs the balances:read scope. See
Products and scopes.
What each field is for
available is a decimal string, in shillings and cents on kes and in
dollars and cents on usd, like every other amount on this API — never a JSON
number. Convert it to integer minor units at your boundary and do arithmetic
there. See Money.
accountNumber is the same account your inbound instructions point at, so
a credit you see arrive and the balance you read afterwards are the same
account. Receive money in Kenya is how
money gets into your KES account; a credited
collection is paid into your USD account.
asOf is the instant the figure was read. It can be up to thirty seconds
behind, because the balance is cached for that long rather than fetched on
every call. Show it wherever you show the number: a balance with no timestamp
invites a reader to treat it as live.
usd.unavailableReason is null whenever usd.available is a figure. It
is set only when your USD account cannot be read (below).
It is a reading, not a fact about now
Do not treat a balance you read as live
Reading a balance tells you what was available when it was read, up to thirty seconds before the call returned. It holds nothing and reserves nothing, and a credit can land between the read and whatever you do with the figure.
Use it for what a reading is good for: displaying a figure beside its asOf,
and reconciling at the end of a period against the credit.received and
collection.received events we sent you. Transfers and buy orders are paid from
your KES balance, and USD payouts and
conversions from your USD balance. Each is
checked against its balance by us when you create it — refused with
TRANSFER_INSUFFICIENT_BALANCE, INSUFFICIENT_KES_BALANCE or
CONVERSION_INSUFFICIENT_BALANCE when it does not cover the amount — so a
figure you read is not a spending check of your own, and it should not be the
number a decision waits on. In sandbox the same holds, against the simulated
figures read here (see below).
Simulated money in sandbox
In sandbox, both figures are simulated unless we have enabled real money for your account, and both are read the same way: either both simulated or both real, never one of each. The KES figure is the balance your buy orders, transfers and batch transfers are paid from, and a conversion pays into; it starts at zero, and we add to it on request. The USD figure is the balance a simulated collection credits, and that USD payouts and conversions draw on.
If real money is enabled for your account, both figures are the real ones. Collections stay simulated in sandbox even then, so a collection's credit goes to a simulated USD balance that this read no longer shows.
accountNumber is a real account at the bank, with a real sandbox balance beside
the simulated one. A payment to it from outside, and an M-PESA funding request,
add to the real balance, and they do not change available. A flow keeps the
mode it started in, so an order, transfer, payout or conversion created while
your balance was simulated stays simulated to its end. See
Sandbox.
When there is no account to report
KES. 503 with PARTNER_VA_NOT_ALLOCATED, PARTNER_VA_ACTIVATING or
PARTNER_VA_SUSPENDED means there is no active KES account behind the balance
yet, and nothing else is returned. None of the three is self-serve and none is
worth retrying in a tight loop — see Errors for what each one
means and what to do.
USD. A USD account that cannot be read does not fail the call. Your KES
figure is still returned, and usd says the USD figure is absent and why:
{
"kes": {
"available": "150000.50",
"accountNumber": "00000000123456",
"currency": "KES",
"asOf": "2026-09-08T12:00:00.000Z"
},
"usd": {
"available": null,
"accountNumber": null,
"currency": "USD",
"asOf": "2026-09-08T12:00:00.000Z",
"unavailableReason": "PARTNER_VA_NOT_ALLOCATED"
}
}unavailableReason | Meaning |
|---|---|
PARTNER_VA_NOT_ALLOCATED | No USD account has been opened for your business yet. |
PARTNER_VA_ACTIVATING | Your USD account exists and is still being activated. |
PARTNER_VA_SUSPENDED | Your USD account is frozen or dormant. |
An absent USD figure is never 0: 0 would mean you hold nothing, and an
absent figure means we could not read one. None of the three is self-serve;
contact us. If the bank cannot be reached at all, the call fails rather than
reporting the account as absent.
Your USDT balance
GET /v1/balances never carries a USDT figure, and a USDT Rails credential's
response carries no usd key. A USDT Rails credential reads its USDT balance
from its own endpoint, GET /v1/usdt/balance. It needs the usdt:read scope,
and a Collections credential is refused, because that product holds no USDT.
When we hold your USDT for you (custody: "onlink_managed"), available is
your USDT balance with us:
{
"custody": "onlink_managed",
"asset": "USDT",
"chain": "tron",
"available": "125.500000",
"source": "ledger",
"asOf": "2026-10-30T14:00:00.000Z",
"unavailableReason": null
}When your USDT is in your own wallet (custody: "partner_customer"),
available is what that wallet holds, read from the wallet itself:
{
"custody": "partner_customer",
"asset": "USDT",
"chain": "tron",
"available": "42.000001",
"source": "chain",
"asOf": "2026-10-30T14:00:00.000Z",
"unavailableReason": null
}available is a decimal string with six decimal places, like every other
USDT amount on this API — never a JSON number. See Money.
source says where the figure was read. ledger is your USDT balance with
us. It is what you can use now: it already leaves out USDT committed to a sell
order, or to a send that has started, and a buy order counts once it has been
delivered. A send you have created that has not started has taken nothing yet.
chain is your own wallet, which can trail a transfer that has only just
settled.
asOf is the instant the figure was read. A figure read from your wallet
can be up to thirty seconds old, because it is cached for that long. The same
rule as above applies: it is a reading, not a spending check. Orders and sends
are checked against your balance by us when they run.
When your wallet cannot be read, the call still succeeds and says so:
{
"custody": "partner_customer",
"asset": "USDT",
"chain": "tron",
"available": null,
"source": "chain",
"asOf": "2026-10-30T14:00:00.000Z",
"unavailableReason": "provider_unavailable"
}unavailableReason | Meaning |
|---|---|
holding_wallet_not_allocated | No wallet has been set up for your business yet. Contact us. |
provider_unavailable | Your wallet could not be read just now. Try again shortly; nothing is lost. |
An absent figure is never 0: 0 would mean your wallet holds nothing, and an
absent figure means we could not read one. Only a partner_customer response can
carry a reason, because a balance we hold for you does not depend on reading a
wallet.
Where your USDT is held
GET /v1/usdt/holding-wallet answers the other half: the address of the
holding wallet, the wallet that holds your USDT, and what USDT sent to it
does. It needs the usdt:read scope, takes no parameters, and a Collections
credential is refused.
When we hold your USDT for you (custody: "onlink_managed"), the holding wallet
is your own deposit address with us, and USDT sent to it is credited to your USDT
balance:
{
"custody": "onlink_managed",
"asset": "USDT",
"chain": "tron",
"address": "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb",
"receives": "USDT sent to this address is credited to your USDT balance."
}When your USDT is in your own wallet (custody: "partner_customer"), the holding
wallet is that wallet. Your sells and sends draw from it. There is no balance with
us to credit:
{
"custody": "partner_customer",
"asset": "USDT",
"chain": "tron",
"address": "T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb",
"receives": "This is your own wallet: your sells and sends draw from it."
}address is returned in full: compare the whole string when you verify it, since a
lookalike that differs only in the middle is an irrecoverable loss. receives is one
sentence saying what a deposit to this address does for your custody, so you do not
have to work it out from custody. It is the same address GET /v1/funding
returns for USDT, whose instructions say the same thing in the same terms.
The holding wallet is not one of the withdrawal wallets you register under
/v1/wallets. Those are external destinations you send USDT to, each with its own
confirmation. The holding wallet is where your USDT already is.
If no address has been allocated for your account yet, the call fails with 503
and errorCode HOLDING_WALLET_NOT_ALLOCATED. It is not self-serve; contact us.
The response never carries an empty address.