Skip to main content

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: countryCode is an ISO 3166-1 alpha-2 code. Match both currency and the payment market country.
  • MERCHANT_BALANCE_SCOPE_TYPE_GLOBAL: countryCode is empty. Use this scope only when the API explicitly returns it.
  • MERCHANT_BALANCE_SCOPE_TYPE_LEGACY_UNSCOPED: countryCode is 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 fieldJSON typeDetailed meaning
balances[].currencyCodestringCurrency code; use it together with country and scope to identify the balance
balances[].countryCodestringCountry for a country-specific balance; empty for global or historically unassigned balances
balances[].scopeTypeenum stringBalance scope: country, global, or historically unassigned; see the values above
balances[].minorUnitintegerCurrency exponent for every *Minor field; divide by 10^minorUnit only for display
balances[].updatedAtstring (date-time)Timestamp of this position revision
balances[].availableMinorstring (int64)Value in the available bucket; preserve negative values if returned
balances[].pendingMinorstring (int64)pendingInMinor + pendingOutMinor
balances[].pendingInMinorstring (int64)Value currently pending inbound
balances[].pendingOutMinorstring (int64)Value currently pending outbound
balances[].holdMinorstring (int64)Value currently in the hold bucket
balances[].currentMinorstring (int64)availableMinor + pendingMinor + holdMinor
balances[].payoutAvailableMinorstring (int64)Current amount for an advisory funding check; compare the payout plus any applicable merchant payout fee
balances[].payoutControlsRevisioninteger (int32)Revision of the effective payout controls; 0 means default controls
balances[].versioninteger (int32)Revision of this position only
balances[].authorityStateenum stringSame 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 parameterTypePresenceMeaning
limitintegerOptionalPage size; defaults to 50 and is capped at 200
offsetintegerOptionalZero-based result offset from 0 through 2147483647; larger values return HTTP 400
currencyCodestringOptionalFilter by currency
countryCodestringOptionalFilter by market country
scopeTypeenum stringOptionalFilter by balance scope: country, global, or historically unassigned; omitted/unspecified means all scopes
sourceTypeenum stringOptionalFilter by a returned category: payment, adjustment, hold, settlement, settlement_reversal, or other
sourceIdstring (UUID)Optional, deprecatedPayment-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:

FieldJSON typeMeaning
idstringOpaque public movement identifier
operationIdstringDeprecated opaque grouping handle; use it only to group movements returned by this API
currencyCodestringCurrency code for this movement
countryCodestringCountry for country scope; empty for global or legacy-unscoped scope
scopeTypeenum stringSame balance-scope values as the balance response
minorUnitintegerCurrency exponent for amountMinor and balance values
bucketstringChanged bucket: available, pending_in, pending_out, or hold
directionstringCREDIT or DEBIT
amountMinorstring (int64)Positive movement magnitude in minor units
balanceBeforeMinorstring (int64), nullableRecorded bucket value before the movement when available
balanceAfterMinorstring (int64), nullableRecorded bucket value after the movement when available
sourceTypeenum stringCategory: payment, adjustment, hold, settlement, settlement_reversal, or other
sourceIdstringDeprecated verified public payment ID for payment; otherwise empty
sourceReferencestringMerchant-owned payment reference preserved exactly for payment; otherwise empty
descriptionstringDescription for display; use sourceType for program logic
createdAtstring (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.