Skip to content
UnknownPay
ไทย
Withdrawals

Create a Withdrawal

POST /v1/withdrawals — create a payout request; gross (amount + fee) is debited immediately and the request starts in PENDING awaiting manual approval

POST /v1/withdrawals

Creates a payout request out of your wallet. The request is HMAC-signed (S2S) and requires an Idempotency-Key. On success it returns 201 Created with status PENDING — the money is not out yet; it awaits manual approval.

Never trust 201 PENDING as a completed payout. Creation only reserves the funds and queues the request for approval — wait for the terminal withdrawal.success webhook before treating the payout as done. See Withdrawal Overview.

Headers

HeaderRequiredDescription
Idempotency-KeyYesA merchant-generated key (one per request) to prevent duplicate creation — see Idempotency. If omitted you get 400 IDEMPOTENCY_KEY_REQUIRED.
Content-TypeYesapplication/json
X-Api-Key / X-Signature / X-TimestampYesS2S authentication (HMAC) headers — see Authentication.

Request body

FieldTypeRequiredDescription
amountstring (baht)YesThe amount the destination will "actually receive" (net), as a baht string, e.g. "500.00". The fee is added on top of this amount and debited from your wallet.
currencystringNo (default "THB")Currency. Currently supports "THB". If omitted or empty, defaults to "THB".
receiver_bank_providerstringYesDestination bank code (bank code alias), e.g. "SCB", "KBANK", "BBL", "KTB"not the 3-digit number. Valid codes come from GET /v1/banks (field bank_code). Lowercase / leading-trailing whitespace is normalized. Unknown code → 422 INVALID_BANK.
receiver_bank_account_namestringYesDestination account name. If blank → 422 (destination must have bank/account_no/name complete).
receiver_bank_account_numberstringYesDestination account number. If blank → 422.
kindstringNo (default "customer")Must be "customer" only (see the constraint below). Blank = customer; any other value → 422 INVALID_KIND.
additionalobjectNoExtra optional data.
additional.descriptionstringNoMerchant description.
additional.reference_user_idstringNoThe merchant-side reference id of the "destination customer" — stored and echoed back in the response/list as reference_user_id.
kind=customer constraint (important): a merchant API key can only create customer payouts (paying a destination account the merchant specifies). Sending any other kind (e.g. settlement, merchant, …) returns 422 INVALID_KINDsettlement is an internal system route only and cannot be called by a merchant over S2S.
Destination structure: internally the destination is stored as an object {bank, account_no, name}, but in the request the merchant sends three separate fields (receiver_bank_provider, receiver_bank_account_number, receiver_bank_account_name). The response returns destination back as that object (full plaintext, not masked).

Amount / balance rules

  • amount must be positive and within the system's configured min/max (out of range → 422 INVALID_AMOUNT).
  • The system debits funds immediately at create (debit-at-request): the amount taken from the wallet is gross = amount + fee. If the gross exceeds your "available" balance (which already excludes prior PENDING/approved requests) → 422 INSUFFICIENT_BALANCE (no row is created).
Pending confirmation — the real withdrawal min/max in production (the code default is unlimited until configured)

Example request

curl -X POST "https://api.unkpay.co/v1/withdrawals" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f1c2b7a-3e44-4d18-9b21-7c0e6a1d2f55" \
  -d '{
    "amount": "500.00",
    "currency": "THB",
    "receiver_bank_provider": "SCB",
    "receiver_bank_account_name": "สมชาย ใจดี",
    "receiver_bank_account_number": "1234567890",
    "kind": "customer",
    "additional": {
      "description": "payout order #A-1042",
      "reference_user_id": "cust-7"
    }
  }'
Pending confirmation — add the S2S authentication headers (API key + HMAC signature)

Response body — 201 Created

FieldTypeDescription
idstring (uuid)The withdrawal id — used with GET /v1/withdrawals/:id and referenced in the webhook.
amountstring (baht)The amount the destination will receive.
feestring (baht)The fee debited on top of the wallet amount (frozen at creation).
net_payoutstring (baht)The net amount paid to the destination (for customer = equal to amount).
currencystring"THB"
receiver_bank_providerstringDestination bank code.
destinationobject{ "bank": "...", "account_no": "...", "name": "..." } — destination in full plaintext.
kindstring"customer"
statusstringInitial status = "PENDING" (awaiting approval — not paid out yet).
This response does not include gross_debit (the real gross debited = amount + fee) — to check the amount taken from the wallet, compute amount + fee or check the balance via the balance endpoint.
{
  "id": "3b9c1e54-2a77-4f0d-bb1e-8a6d4c2e91aa",
  "amount": "500.00",
  "fee": "9.00",
  "net_payout": "500.00",
  "currency": "THB",
  "receiver_bank_provider": "SCB",
  "destination": {
    "bank": "SCB",
    "account_no": "1234567890",
    "name": "สมชาย ใจดี"
  },
  "kind": "customer",
  "status": "PENDING"
}

::placeholder-note{value="the fee value "9.00" in the example is illustrative (based on a bps fee structure) — check the merchant's real withdrawal fee rate in the system"} ::

Idempotency-Key

POST /v1/withdrawals is a money-creating endpoint, so the Idempotency-Key header is required (shared mechanism: Idempotency).

  • The value is a unique string per request — the system enforces no format, only that it is non-empty. UUID v4 recommended, e.g. 9f1c2e7a-3b4d-4f8a-9c10-2b6d5e7f8a90.
  • If omitted → 400 IDEMPOTENCY_KEY_REQUIRED.
  • If you retry with the same key + same body → the system returns the original response (no duplicate request created) with the header Idempotent-Replay: true.
  • If you use the same key but a different body422 IDEMPOTENCY_KEY_MISMATCH.
  • Key TTL = 24 hours.
  • Keys are namespaced by mode (live/test) + merchant — live and test keys never collide.
Set your merchant-side retry policy to reuse the same Idempotency-Key when retrying the same request (e.g. on a timeout / 5xx) to prevent a double payout.

Error codes

HTTPcodeCause
400IDEMPOTENCY_KEY_REQUIREDIdempotency-Key header not sent
422IDEMPOTENCY_KEY_MISMATCHSame Idempotency-Key used with a different body
422INVALID_AMOUNTamount malformed / not positive / out of min-max range
422INVALID_CURRENCYcurrency not supported
422INVALID_BANKreceiver_bank_provider is not a known bank code
422INVALID_KINDkind is not customer
422INSUFFICIENT_BALANCEGross (amount+fee) exceeds the available wallet balance
422VALIDATIONdestination incomplete (must have bank / account_no / name)

See the Error Envelope & Codes for the cross-cutting error format.