Skip to content
UnknownPay
ไทย
Deposits

Create a Deposit

POST /v1/deposits — create a deposit with HMAC + Idempotency-Key; returns expected_amount and pay_to / qr_payload with status PENDING

POST /v1/deposits

Create a deposit so a customer can transfer money in. The response returns an expected_amount (the exact amount the customer must transfer) plus a pay_to destination / qr_payload, with status PENDING. This call must be HMAC-signed (see Authentication) and requires an Idempotency-Key header.

Every money field on the wire is a baht string with 2 decimals, e.g. "500.00" — never a number, never satang. Sending 50000 for "500.00" is the 100× bug.

Idempotency-Key (mandatory when creating)

POST /v1/deposits requires the Idempotency-Key header (if omitted → 400 IDEMPOTENCY_KEY_REQUIRED). See the mechanism and replay behavior under Idempotency.

  • Any unique value — the system does not enforce a format/length, only that it is non-empty. UUID v4 recommended, e.g. 9f1c2e7a-3b4d-4f8a-9c10-2b6d5e7f8a90 (or map it to your order id, as long as it is unique per record).
  • The system caches the key's result for 24 hours — retrying with the same key + same body returns the same deposit (no duplicate, no double amount-slot reservation).
  • The idempotency namespace is separated by mode (live/test) — the same key in live and test do not collide.
On a network-timeout retry you must use the exact same key + same body to prevent a duplicate deposit (which wastes an amount slot). Same key but a different body → 422IDEMPOTENCY_KEY_MISMATCH.

Request body

FieldTypeRequiredMeaning
amountstring (baht)requiredThe amount you want the customer to pay, e.g. "500.00" (baht string, max 2 decimals). Too small/large vs the limit is rejected with 422 INVALID_AMOUNT.
currencystringoptionalCurrency — only "THB" is supported; empty = default THB; any other value → 422 INVALID_CURRENCY.
payment_method_typestringoptionalChannel: "PROMPTPAY_QR" or "BANK_TRANSFER"; empty = default PROMPTPAY_QR; any other value → 422 INVALID_PAYMENT_METHOD.
payer_bank_providerstringrequiredThe customer's source bank (bank code/alias, e.g. "KBANK", "SCB") — used by the matcher for anti-fraud checks; an unknown code → 422 INVALID_BANK.
payer_bank_account_namestringrequiredThe paying customer's account name (declared by the merchant).
payer_bank_account_numberstringrequiredThe paying customer's account number.
additional_dataobjectoptionalExtra metadata with one field: description (string), e.g. an invoice number.
user_refstringoptionalMerchant-side reference (e.g. order id / customer id) — usable to search later.
callback_metaobject (JSON)optionalJSON data you want attached to the record (stored with the deposit).
payer is required for both methods:payer_bank_provider, payer_bank_account_name, payer_bank_account_number must all be sent for both PROMPTPAY_QR and BANK_TRANSFER. If any is missing → 422PAYER_REQUIRED. The system uses this to verify that "whoever actually transferred the money" matches the customer the merchant declared (anti-fraud) — the same for both methods.
Where mode (live/test) comes from: you don't send it in the body — the system derives it from the signing API key (unk_live_... = live, unk_test_... = sandbox).

Response body

After creation the status is always PENDING.

FieldTypeMeaning
idstring (uuid)deposit id — keep it to check status / cancel.
amountstring (baht)The amount the merchant requested (echoed back).
expected_amountstring (baht)The exact amount the customer must transfer (see below) — usually differs from amount by a small satang remainder.
matched_amountstring (baht)The amount actually received — null/absent until CREDITED (not present at creation).
currencystring"THB".
statusstringStatus — at creation, "PENDING".
payment_method_typestring"PROMPTPAY_QR" or "BANK_TRANSFER".
pay_toobjectThe destination account/channel the customer transfers into (see sub-table).
payerobjectEcho of the payer the merchant declared (full: bank, account_no, name).
display_expires_atstring (RFC3339 timestamp)When the QR / payment page display expires.
match_window_untilstring (RFC3339 timestamp)The last moment the system will still match the amount (grace included).

pay_to (object):

FieldConditionMeaning
bankalwaysDestination bank.
account_holderalwaysDestination account name.
account_noBANK_TRANSFER onlyThe full destination account number to transfer into (PROMPTPAY_QR does not include this field).
qr_payloadPROMPTPAY_QR onlyThe EMVCo string used to render the PromptPay QR with a fixed amount = expected_amount (scanning fills the exact amount automatically).
PROMPTPAY_QR: use qr_payload to render the QR for the customer to scan — it pays into the system's PromptPay proxy (not a raw account number) and already embeds expected_amount, so the customer transfers the correct amount automatically. BANK_TRANSFER: no QR — show pay_to.bank + pay_to.account_no + pay_to.account_holder for the customer to transfer manually, and remind them to enter exactly expected_amount.
Diagram coming soon — example payment page showing a PromptPay QR + the expected_amount for the customer to scan

Why expected_amount has a satang remainder

The system matches inbound money by comparing the amount arriving in the account to expected_amount exactly, so it can tell which inbound payment belongs to which deposit (many customers may transfer into the same pool account at nearly the same time).

So when a merchant requests amount = "500.00", the system adds a small satang remainder to make a value unique among other outstanding deposits, e.g. expected_amount = "500.37" (a remainder of 1–99 satang, and if truly necessary the system may nudge it up by a small whole baht). This value is the deposit's "signature amount".

The customer must transfer the exact expected_amount (not amount) for the system to match and credit it.
  • PROMPTPAY_QR: with qr_payload the amount is embedded automatically — the customer doesn't type it (recommended).
  • BANK_TRANSFER: show expected_amount clearly and remind the customer to enter it down to the satang.
If the customer transfers the wrong amount (e.g. "500.00" instead of "500.37"), the system may fail to match and the deposit will expire as EXPIRED.

"One customer, one active deposit" rule

The same customer (identified by payer_bank_provider + payer_bank_account_number) can have only one PENDING deposit at a time. Creating another while one is still outstanding returns:

{ "code": "DEPOSIT_ALREADY_ACTIVE", "message": "this customer already has an active deposit — cancel it or wait for it to expire", "details": { "deposit_id": "<existing id>" } }

Use the returned deposit_id with GET /v1/deposits/:id to re-fetch the existing payment instructions, or cancel the existing one first.

Error codes

Common errors for POST /v1/deposits. Branch on the stable code — see the full envelope under Error Envelope & Codes.

HTTPcodeCause
400IDEMPOTENCY_KEY_REQUIREDThe Idempotency-Key header was not sent.
422INVALID_AMOUNTMalformed amount / more than 2 decimals / out of the min–max range.
422INVALID_CURRENCYcurrency is not THB.
422INVALID_PAYMENT_METHODmethod is not PROMPTPAY_QR/BANK_TRANSFER.
422PAYER_REQUIREDpayer data was incomplete.
422INVALID_BANKpayer_bank_provider is unknown.
422IDEMPOTENCY_KEY_MISMATCHSame Idempotency-Key reused with a different body.
403MERCHANT_SUSPENDEDThe merchant (or parent entity) is suspended.
409DEPOSIT_ALREADY_ACTIVEThis customer already has an outstanding deposit.
409DEPOSIT_AMOUNT_POOL_EXHAUSTEDThe amount slot pool is temporarily full (retry).
503NO_QR_ACCOUNTNo account supports PromptPay QR (try BANK_TRANSFER).
503NO_ALLOWED_ACCOUNTNo usable destination account.

Example request

cURL
# X-Signature = hex( HMAC-SHA256( secret, "POST\n/v1/deposits\n{TS}\n{SHA256_HEX(body)}" ) )
curl -X POST 'https://api.unkpay.co/v1/deposits' \
  -H 'X-Api-Key: unk_live_xxxxxxxxxxxx' \
  -H 'X-Timestamp: 1718790000' \
  -H 'X-Signature: <hex hmac-sha256>' \
  -H 'Idempotency-Key: order-2026-0001' \
  -H 'Content-Type: application/json' \
  -d '{
    "amount": "500.00",
    "currency": "THB",
    "payment_method_type": "PROMPTPAY_QR",
    "payer_bank_provider": "KBANK",
    "payer_bank_account_name": "Somchai Jaidee",
    "payer_bank_account_number": "9876543210",
    "additional_data": { "description": "inv #42" },
    "user_ref": "ord-1"
  }'

Example response

Example response (201 Created) for PROMPTPAY_QR:

{
  "id": "8f2b1c4e-7a90-4d2f-9b3a-1c2d3e4f5a6b",
  "amount": "500.00",
  "expected_amount": "500.37",
  "currency": "THB",
  "status": "PENDING",
  "payment_method_type": "PROMPTPAY_QR",
  "pay_to": {
    "bank": "SCB",
    "account_holder": "ACME Holder",
    "qr_payload": "00020101021229370016A0000006770101110213..."
  },
  "payer": {
    "bank": "KBANK",
    "account_no": "9876543210",
    "name": "Somchai Jaidee"
  },
  "display_expires_at": "2026-06-19T10:05:00Z",
  "match_window_until": "2026-06-19T10:07:00Z"
}

Example response for BANK_TRANSFER (has account_no, no qr_payload):

{
  "id": "1a2b3c4d-...",
  "amount": "500.00",
  "expected_amount": "500.37",
  "currency": "THB",
  "status": "PENDING",
  "payment_method_type": "BANK_TRANSFER",
  "pay_to": {
    "bank": "SCB",
    "account_no": "1234567890",
    "account_holder": "ACME Holder"
  },
  "payer": {
    "bank": "KBANK",
    "account_no": "9876543210",
    "name": "Somchai Jaidee"
  },
  "display_expires_at": "2026-06-19T10:05:00Z",
  "match_window_until": "2026-06-19T10:07:00Z"
}
test mode (sandbox): with a unk_test_... key the system returns a placeholder pay_to (e.g. account_holder = "SANDBOX TEST" and qr_payload starting with "SANDBOX-TEST-QR-", which cannot be scanned for real) so you can test the flow without a real transfer. To simulate an inbound transfer in test mode see Simulate Transfer.
Pending confirmation — reference the sandbox simulate-transfer endpoint for simulating a transfer in test mode