Skip to main content
Disburse funds from a client wallet to a bank account. All endpoints require the standard x-api-key + x-timestamp headers — and the Quote and Create endpoints additionally require the x-signature request signature. See Authentication for both. Typical flow: list banks → quote → create → poll get. Status updates after create (SUCCESS/FAILED) also arrive via Disbursement Webhooks.

1. List available banks

GET /api/v3/wallets/{wallet_id}/banks Call this first to obtain valid bank_code values for the wallet’s payout vendor.
Depending on the wallet’s payout vendor, the list can include e-wallets (e.g. dana, gopay, ovo, linkaja, shopeepay) alongside banks. E-wallets are disbursed through the same endpoints — pick the e-wallet’s code as bank_code and use the recipient’s registered phone number as account_no (see Account number for e-wallets).

2. Quote a disbursement (non-binding fee preview)

POST /api/v3/disbursements/quote
Required header: x-signature — the HMAC-SHA256 request signature (t=<ms>,v1=<hex>). Requests without a valid signature are rejected with 401. See Signing a request; validate your implementation with the signature-check endpoint before going live.

Request

Account number for e-wallets

account_no is forwarded to the payout vendor as-is (no reformatting on our side) and verified in real time via the vendor’s account inquiry — both Quote and Create run this inquiry, and the verified account_holder_name is echoed back so you can confirm the recipient before money moves.
  • Bank codes: the recipient’s bank account number, digits only.
  • E-wallet codes (dana, gopay, ovo, …): the phone number the e-wallet account is registered with, digits only in local Indonesian format — e.g. 081234567890 (leading 0, no +62, no spaces or dashes).
If the account or phone number can’t be resolved you get 422"bank account verification failed" for an unknown account, "bank account is inactive" for a known-but-inactive one. Because Quote runs the same inquiry with no side effects, it’s the safe way to validate a recipient (and their name) before creating the disbursement.

Response

Quote has no side effects (no funds locked, nothing persisted).

3. Create a disbursement

POST /api/v3/disbursements
Required headers:
  • Idempotency-Key (max 64 chars). Missing → 400. Retrying with the same key (and the same parameters) returns the original outcome rather than creating a duplicate — see Idempotency & replay semantics.
  • x-signature — the HMAC-SHA256 request signature (t=<ms>,v1=<hex>), computed over the exact body string you send. Missing or invalid → 401. See Signing a request.
When x-signature is present, x-timestamp is ignored — the t inside the signature is what’s checked against the ±5-minute window.

Request

Response

Status codes:
  • 201 Created — fresh disbursement created.
  • 200 OK with header Idempotent-Replayed: true — the Idempotency-Key was already used and the disbursement is SUCCESS or REQUESTED; the stored result is returned. Replays of unsuccessful disbursements answer with an error status instead — see below.

Idempotency & replay semantics

A replay is faithful: retrying with the same Idempotency-Key and the same parameters answers exactly as the original attempt did — same HTTP status, same body — with the Idempotent-Replayed: true response header as the only addition. Do not treat every replay as a success; key your success handling off the HTTP status: Parameter fingerprint. Reusing a key with different parameters returns 422 and performs nothing — this protects you from believing a new payload was disbursed when only the old one was. The compared fields are client_wallet_id, amount (scale-insensitive: "100000" equals "100000.00"), bank_code (case-insensitive), account_no (whitespace-trimmed), and transfer_type. memo is not compared. On a 422, either resend the original payload with that key, or use a new key for the new payload. Concurrency. Two simultaneous creates with the same key can race; the loser may get 409 “concurrent request with the same Idempotency-Key; retry” — safe to retry with the same key. Other error codes on Create:

4. Get a disbursement

GET /api/v3/disbursements/{id} Returns the disbursement if it belongs to your client; otherwise 404. x-signature is optional here (and on List available banks) — sign it if you want, unsigned requests with a valid x-timestamp work too.

5. Disbursement status values

PENDING_VERIFICATIONVERIFIEDREQUESTEDSUCCESS, with terminal failure states FAILED, EXPIRED, and CANCELLED.

6. FAQ — prerequisites & common rejections

Why did I get 402 “Insufficient balance”?

Your wallet’s available balance must cover the full amount of the disbursement (the gross amount — the platform fee is deducted from it, not added on top). Available balance is:
Two things commonly surprise integrators:
  • Incoming payments only count after settlement. A QR payment that has been paid but not yet settled does not increase your available balance. Settlement typically lands the next business day (T+1). To top up: receive QR payments into the wallet, then wait for settlement before disbursing.
  • In-flight disbursements lock their full amount. Every disbursement that has not reached a terminal state (SUCCESS/FAILED/EXPIRED/CANCELLED) holds a lock on its amount, reducing what’s available for new ones. Locks are released when the disbursement finishes or expires.
Check your current balance via the Wallets endpoints before creating large disbursements.

Why did I get 422 “bank account verification failed” or “bank account is inactive”?

account_no is verified against the receiving bank in real time (a bank inquiry) before the disbursement is created — an invalid or inactive account is rejected up front, and nothing is charged. Common causes: Tip: the Quote endpoint runs the same verification without creating anything — use it to validate the account (and preview the account holder’s name) before the real create.

What are the minimum and maximum disbursement amounts?

Minimum. The minimum applies to the net amount (your amount minus the platform fee) and depends on transfer_type: Because the minimum is on the net amount, the minimum amount you can send is slightly higher and depends on your fee configuration. You don’t need to compute it yourself:
  • A too-small create (or quote) fails with 422 "withdrawal amount is below minimum: <X>" — where <X> is the exact minimum gross amount for your account and that transfer type.
  • The Quote endpoint returns your fee and net amount for any candidate amount, so you can verify before creating.
Maximum. The platform does not enforce a maximum of its own — the effective ceiling per transaction comes from the payout network and the receiving bank:
  • BIFAST transfers are subject to the BI-FAST network’s per-transaction cap set by Bank Indonesia.
  • RTOL (online transfer) caps are lower and vary per receiving bank.
A disbursement above the applicable cap is rejected by the payout vendor and answers 503 "Vendor payout request failed" — no money moves and your balance is unaffected (the balance lock is released when the disbursement fails). For amounts near or above network caps, split the payout into multiple disbursements — each with its own Idempotency-Key — or contact support to confirm the current limits for your route. Your practical ceiling is also bounded by your available balance (see the 402 question above) — the full amount must be covered at create time.