> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Withdrawal & Balance APIs

> The five signed endpoints that let you withdraw settlement funds and read balances from your own system: the consult → create → inquiry sequence, the withdrawRequestId idempotency key, which accounts a balance query returns, and the time window and fields of an account-history query.

Withdrawals and balances are available as API, not only in the Portal. Portal screens, fees,
statements and automatic-withdrawal contracts are in [Withdrawal & Batch Payout FAQ](/docs/en/support/withdrawal-and-batch-payout-faq),
what each balance means is in [Balances & Account History](/docs/en/support/balance-and-account-history-faq),
and signing is covered in [Authentication & Signature FAQ](/docs/en/support/auth-signature-faq).

## Which endpoints exist, and what do I need to call them?

All five are `POST`, JSON in and out, signed exactly like `/api/v1/order/create`.

| Endpoint | Purpose |
| - | - |
| `/api/v1/withdraw/consult` | Dry run: limits, fee, expected received amount, beneficiary account. Moves no money. |
| `/api/v1/withdraw/create` | Creates the withdrawal order. |
| `/api/v1/withdraw/inquiry` | Reads one withdrawal order by either id. |
| `/api/v1/account/balance/inquiry` | Settlement-account balances per currency. |
| `/api/v1/account/history/inquiry` | Ledger rows for your settlement and reserve accounts. |

Two access rules catch most first calls. **API keys are scoped**: a key issued for payin only cannot
call these endpoints and fails with `A0004` "Permission denied" before the signature is even checked —
if a request that signs correctly elsewhere returns `A0004`, ask Waffo for a key whose scope includes
withdrawal. And **`merchantId` in the body must match the MID your key resolves to**, otherwise
`A0003`. `/api/v1/withdraw/inquiry` is the exception: it carries no `merchantId` — the MID comes from
the key, and an order belonging to another MID is reported as not found.

## How do the three withdrawal calls fit together?

1. **`consult`** — send `currency`, optionally `amount`. Returns the fee, expected received amount,
   your agreement's limits and the beneficiary account, so you can validate before committing.
2. **`create`** — send the amount you settled on plus a `withdrawRequestId` of your own.
3. **`inquiry`** — poll until the status is final, or wait for the callback on your `notifyUrl`.

`create` is deliberately not a completed withdrawal: the approval record and the balance deduction
are raised asynchronously, so a successful `create` normally returns `IN_PROGRESS` — treat it as
"accepted", never as "paid".

## What does consult tell me, and what if I leave the amount out?

Omit `amount` and consult answers "how much could I withdraw in one go right now": your
**withdrawable balance** — the settlement account's available balance, reduced by the reserve
shortfall when the reserve balance is negative — capped at the agreement's maximum.

| Field | Notes |
| - | - |
| `amount`, `currency` | The amount consulted, or the computed maximum |
| `beforeAvailableBalance`, `afterAvailableBalance` | Available balance before / after this withdrawal |
| `receiveCurrency`, `expectedReceiveAmount` | Same currency: `amount` − `fee`. Cross-currency: `expectedReceiveAmount` is **not returned at all**, because the rate is not fixed yet |
| `feeCurrency`, `fee` | Charged **inclusively**; a fee that would swallow the whole amount is rejected. Cross-currency withdrawals carry a fixed fee in `feeCurrency` |
| `minWithdrawAmount`, `maxWithdrawAmount` | From your withdrawal agreement; `maxWithdrawAmount` absent means no upper limit |
| `merchantBankAccount` | `bankName`, `bankAccountNo`, `bankAccountName` |

The realised `fxRate` and `actualReceiveAmount` appear on `inquiry` once the order completes.

## What does create require, and how does idempotency work?

| Field | Required | Constraint |
| - | - | - |
| `withdrawRequestId` | yes | ≤ 64 chars, unique per MID, the idempotency key |
| `currency`, `amount` | yes | Amount as a string, at most 2 decimals |
| `description` | yes | ≤ 128 chars |
| `merchantInfo.merchantId` | yes | Must equal your key's MID; `subMerchantId` optional |
| `withdrawRequestedAt` | yes | `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` |
| `notifyUrl` | yes | `http`/`https`, ≤ 256 chars |
| `bankAccountId` | no | Which bank account under the agreement receives the funds |
| `accountNo` | no | Targets one specific balance account |
| `extendInfo` | no | Valid JSON, ≤ 128 chars, echoed back on inquiry |

Leave `bankAccountId` out and the agreement's single bound account is used automatically; if the
agreement has **more than one**, omitting it fails with a parameter error asking for it. `accountNo`
matters only if you hold more than one balance account in the same currency — which happens after
re-contracting with a different Waffo entity.

`withdrawRequestId` is the idempotency key, scoped to your MID. Replay it with the same `amount` and
`currency` and you get the **original** order's result back — no second withdrawal. Replay it with a
different `amount` or `currency` and the call fails with `A0011` "Idempotent param mismatch error". So
a timeout is safe to retry with an identical body; a genuinely new withdrawal needs a new id.

## How do I poll status, and what do the statuses mean?

`inquiry` needs at least one of `withdrawRequestId` or `withdrawOrderId`; neither is a parameter error.
`withdrawOrderId` wins when both are sent, and if the two disagree the call fails rather than
returning the wrong order.

| API status | Meaning |
| - | - |
| `IN_PROGRESS` | Not yet final — covers not-submitted, under review, approved and channel processing |
| `SUCCESS` | Completed |
| `FAILURE` | Rejected at review, failed, closed, or confirmed returned by the bank |
| `BANK_DISPUTE_NOTIFIED` | The receiving bank raised a return; the outcome is still open |

The Portal shows a longer, finer-grained status list for the same order — the mapping is in
[Withdrawal & Batch Payout FAQ](/docs/en/support/withdrawal-and-batch-payout-faq). On success `inquiry`
backfills `actualReceiveAmount`, `fxRate` (always `1.00` for same-currency), `bankRefId` and
`withdrawCompleteTime`; on failure it carries `failureCode` and `failureReason`.

## Which accounts does a balance query return?

Your **settlement accounts** only — one row per settlement currency, merged per contracting entity.
Pass `currency` to narrow it, or omit it for all currencies. The pending-settlement balance is not
exposed here.

| Field | Meaning |
| - | - |
| `accountNo`, `currency` | Account number is globally unique; more than one per currency is possible |
| `totalBalance` / `availableBalance` / `frozenBalance` | Account balance, what is usable, and what is held against chargeback activity (`available = total − frozen`) |
| `reserveBalance` | Reserve balance for that currency and entity; when negative, subtract its absolute value from `availableBalance` to get what you can actually withdraw |

## How far back can an account-history query go, and what is in a row?

**All times on this endpoint are UTC** — both the `startTime` / `endTime` you send (ISO-8601 local
date-time, e.g. `2026-08-01T00:00:00`) and the `transactionTime` returned (`...Z`). The Portal
presents Account History in UTC+8 instead.

* Omit both bounds and you get **today only**; send both for anything else. The window may not exceed
  **180 days**, and a query resolving to more than **60,000 rows** is refused.
* Unlike the Portal's Account History, this endpoint covers **both** the settlement and the reserve
  ledger. Filter with `accountType` = `MERCHANT_SETTLEMENT` or `MERCHANT_RESERVE`; omit it and both are
  returned. `currency` and `accountNo` narrow it further, and a filter naming an account you do not
  hold returns an account-not-found error rather than an empty list.

Each row carries `accountNo`, `accountType`, `currency`, `amount`, `accountDirection`
(`CREDIT` / `DEBIT`), `newBalance` (the balance right after the movement), `transactionTime`,
`businessId` (the related business order) and `businessType` — codes such as `SETTLEMENT`,
`WITHDRAWAL`, `RESERVE_WITHHOLDING`, `RESERVE_RELEASING`, `RESERVE_ADJUSTMENT` and `ADJUSTMENT`, best
matched case-insensitively. Their meanings are in [Balances & Account History](/docs/en/support/balance-and-account-history-faq).

## Which errors should I handle explicitly?

| Code | When it happens |
| - | - |
| `A0003` / `A0004` | Parameter validation failed (including `merchantId` not matching your key) / your key's scope does not cover this endpoint |
| `A0007` / `A0008` | Withdrawal currency not supported for you / amount outside your agreement's range |
| `A0010` / `A0012` | Not allowed under your contract / insufficient balance |
| `A0011` | Same `withdrawRequestId` replayed with different parameters |
| `E0001` | Unknown status — keep polling `inquiry` until it is final; do not create a second order |

***

Still stuck? [Contact support](mailto:support@waffo.com) with the `withdrawRequestId` or
`withdrawOrderId` and the timestamp of the call.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.