Skip to content
UnknownPay
ไทย
Withdrawals

Withdrawal Overview

How payouts work — gross debited immediately at create, manual approval, and the PENDING→SUCCESS/FAILED/REJECTED state machine

This section explains how your merchant system creates a withdrawal request (payout) out of your wallet through UnknownPay, paying a destination (e.g. your end customer) into a bank account over a Server-to-Server (S2S) connection.

A payout does not move money out instantly. Every withdrawal request must pass manual approval by the UnknownPay team before the system sends the real pay-out instruction to the bank. Your system must therefore wait for the withdrawal.success webhook to confirm the payout succeeded — never treat the request as successful just because creation returned 201 Created.
Every money field on the wire is always a baht string with 2 decimals, e.g. "100.50" (not a number, not satang) — in both request and response. Fields such as amount, fee, and net_payout must always be sent/read as baht strings.
Diagram coming soon — sequence diagram — merchant → POST /v1/withdrawals → UnknownPay (PENDING) → team approve → bank pays out → withdrawal.success webhook back to merchant

Endpoints

OperationMethodPath
Create a withdrawalPOST/v1/withdrawals
Get one withdrawal (single)GET/v1/withdrawals/:id
List your own withdrawalsGET/v1/withdrawals

Every endpoint lives on the /v1 gateway surface and requires S2S authentication (API key + HMAC) just like the other merchant endpoints — see Authentication.

State machine

The live state flow:

PENDING ──(team approves as a batch)──▶ PROCESSING ──▶ [IN_PROGRESS] ──▶ SUCCESS
   │                                          │                              │
   │                                          └──────────────┬───────────────┘
   │                                                         ▼
   └──(team rejects)──▶ REJECTED                          FAILED
                                              (bank/bot reports failure → refund)
StatusMeaningWhat the merchant should do
PENDINGRequest created, gross already debited from the wallet, awaiting team approvalWait — not paid out yet
APPROVEDApproved (mainly a sandbox/test resting state — live skips it)Wait
PROCESSINGTeam approved + the pay instruction was sent to the payment system/bank (money is going out)Wait — can no longer be cancelled
IN_PROGRESSIn progress at the bank (intermediate state, may be skipped)Wait
SUCCESSPayout succeeded (terminal)✅ Treat as success — withdrawal.success webhook
FAILEDPayout failed (terminal) — system refunds the gross back to the walletwithdrawal.failed + withdrawal.refunded webhooks
REJECTEDTeam rejected the request (terminal) — system refunds the gross back to the walletwithdrawal.refunded webhook

Technical notes that match the code:

  • The live approval path goes straight from PENDINGPROCESSING (approval happens as a "batch") — APPROVED is not a resting state in live (it is used mainly in sandbox/test mode).
  • IN_PROGRESS is optional — the bank may report SUCCESS directly from PROCESSING. Do not design your system to require IN_PROGRESS as a mandatory condition.
  • SUCCESS, FAILED, and REJECTED are terminal (they never change again).
The statuses the merchant should "wait for the webhook" on: treat a payout as truly finished only when you receive a terminal-status webhook, namely:
  • withdrawal.success (paid out successfully — money has really left)
  • withdrawal.failed (payout failed — money refunded to the wallet)
  • withdrawal.refunded (money refunded to the wallet; fires for both REJECTED and FAILED)
withdrawal.rejected (team rejected) always comes paired with withdrawal.refunded.
Pending confirmation — confirm the exact event names emitted by the live webhook side (code confirms: withdrawal.success / withdrawal.failed / withdrawal.refunded / withdrawal.rejected) and document the real webhook payload
Diagram coming soon — withdrawal state diagram (PENDING → PROCESSING → IN_PROGRESS → SUCCESS / FAILED / REJECTED)

The exact payload of each event and how to verify the signature are covered in the Webhooks section.

Debit at create + refund (matters for reconciliation)

  • Debit at create (debit-at-request): when the request is created successfully (201, status PENDING), the system immediately debits gross = amount + fee from your wallet — that amount is "reserved" from this moment, not at approval.
  • Rejected → refund: if the team rejects the request (while still PENDING or APPROVED), the system refunds the full gross back to the wallet and sends the withdrawal.refunded webhook (status REJECTED).
  • Failed → refund: if the payout was attempted and failed (FAILED), the system likewise refunds the gross back to the wallet (withdrawal.failed + withdrawal.refunded webhooks).
  • Once PROCESSING, it cannot be cancelled: once approved and dispatched (PROCESSING), the team can no longer reject it (it returns 409) — from this point only SUCCESS or FAILED will settle the money.
Effect on your merchant-side logic: do not deduct your own internal balance again. The debit happens on the UnknownPay side. Rely on the balance endpoint for the wallet balance, and rely on the terminal webhook as the deciding factor for the final outcome.

Next steps