Skip to main content

Funding and defunding

Add value to an account, and take it back out.

Before you begin​

  • An account — see creating accounts.
  • An active card on the account. Value can only be added or removed while the card is active; in any other status the call is refused with card_invalid_state.
  • An understanding of the three balances. If you have not read balances: ledger vs available, read it first; the difference determines whether a debit succeeds.
Idempotency is not optional here

These two endpoints move money. A retry without an Idempotency-Key — after a timeout, a dropped connection, a pod restart mid-request — credits the account twice.

Generate the key once, before the first attempt, and reuse it for every retry of that same logical operation. A key derived from your own primary key, such as topup-8841, survives a process restart in a way a fresh UUID does not. See idempotency.

The key is forgotten after 24 hours. For the movement itself, also send client_transaction_id — your own id for this credit or debit, unique per account, never expiring. A repeat with the same id and amount returns the account unchanged with Idempotent-Replay: true; a different amount under the same id is refused. reference is a label, not an identifier: it carries no uniqueness rule.

How it works​

Two operations, mirroring each other:

Effect on ledger_balanceEffect on available_balance
POST /creditincreasesincreases
POST /debitdecreasesdecreases

Neither touches held_balance — holds belong to authorizations, not to you.

A debit is checked against available_balance, not ledger_balance. Money reserved by an outstanding authorization cannot be swept out from under it. An account showing S$250 settled but S$60 held will refuse a S$200 debit with insufficient_funds, and that is correct: S$60 of that S$250 is already spoken for.

What this does and does not cover​

These endpoints move value on the ledger. They are the record of what a cardholder can spend.

How real money reaches the pooled account backing that ledger — a bank transfer, a payout batch, a collection from your treasury — is arranged per program and is not an API call. Talk to your account contact about the funding rail for your program before you go live; the API side is the easy half.

Steps​

1. Credit an account​

Call Credit an account for the account. The request carries the amount, an optional description and reference, and your own client_transaction_id. Send an Idempotency-Key as well, as described above.

Amounts are integer minor units in the account's currency. S$250.00 is 25000, and the currency must match the account's.

The response is the account in its new state, so you do not need to read it back.

2. Debit an account​

Call Debit an account. It takes the same request, in the opposite direction. Use it to sweep an unspent balance back, correct an over-credit, or empty an account before retiring it.

A debit is checked against available_balance, not ledger_balance — funds held by pending authorizations are not yours to take back. Too large a debit fails with insufficient_funds and a 402. The check and the write are one operation, so an authorization landing between them cannot overdraw the account.

3. Read the funding history​

Credits and debits appear as transactions of type adjustment. Call List transactions filtered by the account.

Every credit and debit is one entry, and the balance you read back is the sum of them. A duplicate request carrying the same Idempotency-Key posts once, and so does one carrying the same client_transaction_id — however long ago the first was.

Verifying​

account.funded fires on credit and account.updated on debit. There is no low-balance event: poll available_balance from your own scheduler if you auto-fund, at whatever cadence your float can tolerate.

Read the account back and check that available_balance moved by exactly the amount you sent. If it moved by a different amount, something else touched the account concurrently — an authorization clearing, most likely.

Common errors​

StatuscodeFix
402insufficient_fundsavailable_balance is too low. Note this is not 422
422currency_mismatchThe amount.currency differs from the account's currency. There is no conversion
422account_blockedUnblock the account first
400invalid_parameterUsually a float or a major-unit amount. 25000, not 250.00
409idempotency_key_reuseSame key, different body. Almost always a retry-logic bug worth fixing rather than working around
409adjustment_already_appliedSame client_transaction_id, different amount or direction. The first movement stands; the message says what it was
insufficient_funds is never retryable

Retrying it will fail identically until the balance changes. Credit the account, then retry — with a new idempotency key, since it is a new operation.