Balances API
Last updated: 2026-09-26
Balance operations use a secret key (sk_*). Every *Minor value is a signed
int64 encoded as a JSON string. Use minorUnit to convert it for display.
List balances
GET /merchant/api/v2/balances
Auth: Secret key
The response contains authorityState and a balances array. Each array entry
is one balance, identified by its currency, country and scope.
curl "https://merchants-api.tcpay.io/merchant/api/v2/balances" \
-H "Authorization: Bearer sk_test_..."
Example response:
{
"balances": [
{
"currencyCode": "XOF",
"countryCode": "CI",
"scopeType": "MERCHANT_BALANCE_SCOPE_TYPE_COUNTRY",
"minorUnit": 0,
"updatedAt": "2026-09-25T10:00:00Z",
"availableMinor": "125000",
"pendingMinor": "18000",
"pendingInMinor": "15000",
"pendingOutMinor": "3000",
"holdMinor": "2000",
"currentMinor": "145000",
"payoutAvailableMinor": "100000",
"payoutControlsRevision": 7,
"version": 42,
"authorityState": "MERCHANT_BALANCE_AUTHORITY_STATE_AUTHORITATIVE"
}
],
"authorityState": "MERCHANT_BALANCE_AUTHORITY_STATE_AUTHORITATIVE"
}
All fields shown on a returned balance item are present even when their value is
zero or an empty string. balances is always present and is an empty array when
the merchant has no positions; the top-level authorityState is always present.
Authority state
Read the top-level authorityState before using balances or history in a
funding comparison. The same value is repeated on each returned balance item.
MERCHANT_BALANCE_AUTHORITY_STATE_AUTHORITATIVE: the returned balance information is confirmed for this account.MERCHANT_BALANCE_AUTHORITY_STATE_UNAVAILABLE: confirmed balance information is not yet available for this account. This state does not by itself disable payment processing; the payout response decides acceptance.MERCHANT_BALANCE_AUTHORITY_STATE_BACKFILLING: balance information is being prepared and is not yet confirmed.MERCHANT_BALANCE_AUTHORITY_STATE_CATCHING_UP: recent changes are being included; the information is not yet confirmed.
Treat MERCHANT_BALANCE_AUTHORITY_STATE_UNSPECIFIED and any unknown future
value as non-authoritative. Do not infer authority from non-zero balances or
from a successful HTTP response.
For a non-authoritative response, skip the balance-based comparison. Continue handling the payout API response: it decides acceptance and can return the account-level refusal.
Balance identity and country scope
Use currencyCode, countryCode, and scopeType together to identify a
balance. Do not combine rows only because they share a currency.
MERCHANT_BALANCE_SCOPE_TYPE_COUNTRY:countryCodeis an ISO 3166-1 alpha-2 code. Match both currency and the payment market country.MERCHANT_BALANCE_SCOPE_TYPE_GLOBAL:countryCodeis empty. Use this scope only when the API explicitly returns it.MERCHANT_BALANCE_SCOPE_TYPE_LEGACY_UNSCOPED:countryCodeis empty. Keep this balance separate; never guess a country from the currency.
Treat MERCHANT_BALANCE_SCOPE_TYPE_UNSPECIFIED, an invalid country/currency,
or duplicate entries with the same three identifiers as unusable for a funding
comparison. Check the response with support rather than guessing which entry
to use.
Shared currencies can have multiple country-scoped rows: for example, XOF in
CI and XOF in SN are distinct balances.
Position fields
| Response field | JSON type | Detailed meaning |
|---|---|---|
balances[].currencyCode | string | Currency code; use it together with country and scope to identify the balance |
balances[].countryCode | string | Country for a country-specific balance; empty for global or historically unassigned balances |
balances[].scopeType | enum string | Balance scope: country, global, or historically unassigned; see the values above |
balances[].minorUnit | integer | Currency exponent for every *Minor field; divide by 10^minorUnit only for display |
balances[].updatedAt | string (date-time) | Timestamp of this position revision |
balances[].availableMinor | string (int64) | Value in the available bucket; preserve negative values if returned |
balances[].pendingMinor | string (int64) | pendingInMinor + pendingOutMinor |
balances[].pendingInMinor | string (int64) | Value currently pending inbound |
balances[].pendingOutMinor | string (int64) | Value currently pending outbound |
balances[].holdMinor | string (int64) | Value currently in the hold bucket |
balances[].currentMinor | string (int64) | availableMinor + pendingMinor + holdMinor |
balances[].payoutAvailableMinor | string (int64) | Current amount for an advisory funding check; compare the payout plus any applicable merchant payout fee |
balances[].payoutControlsRevision | integer (int32) | Revision of the effective payout controls; 0 means default controls |
balances[].version | integer (int32) | Revision of this position only |
balances[].authorityState | enum string | Same authority decision as the top-level response field |
version and payoutControlsRevision change independently. A controls-only
change can change payoutAvailableMinor without changing the position
version; do not use either field as a timestamp or calculate it client-side.
These revision fields are not a complete change detector for payout availability.
Use payoutAvailableMinor, not availableMinor or currentMinor, for an
advisory comparison of the full debit: payout amount plus any applicable payout
fee. If the fee is unknown, comparing only the payout amount is insufficient.
A read does not reserve funds; use a fresh read for a new decision and handle
the payout response even when the comparison passes.
Get balance history
GET /merchant/api/v2/balances/history
Auth: Secret key
curl -G "https://merchants-api.tcpay.io/merchant/api/v2/balances/history" \
-H "Authorization: Bearer sk_test_..." \
--data-urlencode "currencyCode=XOF" \
--data-urlencode "countryCode=CI" \
--data-urlencode "scopeType=MERCHANT_BALANCE_SCOPE_TYPE_COUNTRY" \
--data-urlencode "limit=50" \
--data-urlencode "offset=0"
| Query parameter | Type | Presence | Meaning |
|---|---|---|---|
limit | integer | Optional | Page size; defaults to 50 and is capped at 200 |
offset | integer | Optional | Zero-based result offset from 0 through 2147483647; larger values return HTTP 400 |
currencyCode | string | Optional | Filter by currency |
countryCode | string | Optional | Filter by market country |
scopeType | enum string | Optional | Filter by balance scope: country, global, or historically unassigned; omitted/unspecified means all scopes |
sourceType | enum string | Optional | Filter by a returned category: payment, adjustment, hold, settlement, settlement_reversal, or other |
sourceId | string (UUID) | Optional, deprecated | Payment-only correlation filter; requires sourceType=payment, and other combinations return HTTP 400 |
Filters are combined. sourceType is case-insensitive, but values outside the
six published categories return HTTP 400. Use currencyCode, countryCode,
and scopeType together when reconciling one balance. Results are ordered
newest first by createdAt. Rows with the same timestamp have a stable order,
but the API does not define a public field for reproducing that order. Preserve
the returned order; use id for identity and deduplication.
The response always includes authorityState, movements, and page.
movements is an empty array when there are no matches; page.limit,
page.offset, and page.totalPages remain present even when they are zero.
Apply the same authority rules as the balance snapshot.
Each returned movement has the following stable fields:
| Field | JSON type | Meaning |
|---|---|---|
id | string | Opaque public movement identifier |
operationId | string | Deprecated opaque grouping handle; use it only to group movements returned by this API |
currencyCode | string | Currency code for this movement |
countryCode | string | Country for country scope; empty for global or legacy-unscoped scope |
scopeType | enum string | Same balance-scope values as the balance response |
minorUnit | integer | Currency exponent for amountMinor and balance values |
bucket | string | Changed bucket: available, pending_in, pending_out, or hold |
direction | string | CREDIT or DEBIT |
amountMinor | string (int64) | Positive movement magnitude in minor units |
balanceBeforeMinor | string (int64), nullable | Recorded bucket value before the movement when available |
balanceAfterMinor | string (int64), nullable | Recorded bucket value after the movement when available |
sourceType | enum string | Category: payment, adjustment, hold, settlement, settlement_reversal, or other |
sourceId | string | Deprecated verified public payment ID for payment; otherwise empty |
sourceReference | string | Merchant-owned payment reference preserved exactly for payment; otherwise empty |
description | string | Description for display; use sourceType for program logic |
createdAt | string (date-time) | Time the movement was recorded; an earlier settlement transfer can have a different date |
balanceBeforeMinor and balanceAfterMinor can be omitted or null. When
present, they describe the balance around that individual movement. Adjacent
rows for the same balance and bucket do not guarantee a continuous running
balance. A gap alone does not prove a missing payment or a duplicate charge.
Use the current confirmed balance snapshot for the current position and
reconcile individual payments through status reads and verified webhooks.
For settlement, an available-balance DEBIT records a transfer paid to you;
a CREDIT records funding received from you. settlement_reversal reverses the
recorded entry with the opposite direction. It does not represent a new
external transfer or a refund. These movements have empty sourceId and
sourceReference values.
Example history response:
{
"movements": [
{
"id": "movement-example-1",
"operationId": "group-example-1",
"currencyCode": "XOF",
"countryCode": "CI",
"scopeType": "MERCHANT_BALANCE_SCOPE_TYPE_COUNTRY",
"minorUnit": 0,
"bucket": "available",
"direction": "DEBIT",
"amountMinor": "2500",
"balanceBeforeMinor": "127500",
"balanceAfterMinor": "125000",
"sourceType": "payment",
"sourceId": "00000000-0000-4000-8000-000000000001",
"sourceReference": "PO-3002",
"description": "Payment balance movement",
"createdAt": "2026-09-25T10:00:00Z"
}
],
"page": { "limit": 50, "offset": 0, "totalPages": 1 },
"authorityState": "MERCHANT_BALANCE_AUTHORITY_STATE_AUTHORITATIVE"
}
History pagination
offset counts movements, not pages. Start at 0 and use the returned effective
page.limit for the next offset. With limit: 50 and totalPages: 3, request
offsets 0, 50, and 100. A requested limit above 200 is capped to 200;
use that returned value when advancing.
An empty result still includes the effective limit and the requested offset. For an initial request with no matching movements, the response can be:
{
"movements": [],
"page": { "limit": 50, "offset": 0, "totalPages": 0 },
"authorityState": "MERCHANT_BALANCE_AUTHORITY_STATE_AUTHORITATIVE"
}
totalPages describes the total matching result, not pages remaining after the
current offset. New movements can shift later pages. Deduplicate by id and
recheck a range when completeness matters; separate reads are not one frozen
snapshot.
Invalid history filters
An unsupported sourceType or scopeType, an offset above 2147483647, or an
invalid sourceId combination returns HTTP 400 / INVALID_ARGUMENT.
For example, sourceType=settlement&sourceId=<payment_id> is invalid: sourceId
is a payment-only filter and requires sourceType=payment plus a valid payment
UUID. Correct the request before retrying. See Validation errors.
Related guide: Balances.