Deposit Overview & State Machine
This section explains how a merchant takes money in from a customer through UnknownPay Server-to-Server (S2S): from "create a deposit" → "let the customer transfer" → "the system matches and credits the balance" through "track status" and "cancel".
"100.50" — not a JSON number and not satang. Fields such as amount, expected_amount, matched_amount must always be sent/read as strings.Related endpoints
| Operation | Method + Path | Auth |
|---|---|---|
| Create a deposit | POST /v1/deposits | S2S (HMAC) + Idempotency-Key |
| Get / retrieve a deposit | GET /v1/deposits/:id | S2S (HMAC) |
| Cancel a deposit | POST /v1/deposits/:id/cancel | S2S (HMAC) |
All three endpoints sit on the gateway surface /v1 and must be HMAC-signed on every call (see Authentication). The system derives merchant and mode (live/test) from the signing API key itself — you never send a merchant id in the body.
/v1 prefix, so the full path is /v1/deposits and so on.State machine
┌───────────────┐
│ PENDING │ (initial status after creation)
└───────┬───────┘
┌────────────────┼─────────────────────┐
customer transfers past match_window merchant/system cancels
exact expected_amount │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌─────────────┐
│ CREDITED │ │ EXPIRED │ │ CANCELLED │
└───────────┘ └───────────┘ └─────────────┘
(terminal) (terminal) (terminal)
| Status | Meaning |
|---|---|
PENDING | Created; waiting for the customer to transfer the exact expected_amount within the time window. |
CREDITED | Inbound money matched — credited to the merchant balance (matched_amount = the amount actually received). terminal |
EXPIRED | The match_window elapsed with no matching inbound amount → expired. terminal |
CANCELLED | Cancelled while still PENDING. terminal |
A deposit is single-phase (no intermediate state): from PENDING it ends in exactly 1 of the 3 terminals, and once terminal it cannot change back.
Exact-amount matching
The system matches inbound money with a simple rule: the amount arriving in the account must equal 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". This is the deposit's "signature amount".
expected_amount (not amount) for the system to match and credit it. For PROMPTPAY_QR the amount is embedded in qr_payload automatically; for BANK_TRANSFER you must show expected_amount clearly and remind the customer to enter it down to the satang. If the customer transfers the wrong amount, the deposit may not match and will expire as EXPIRED.Timing (TTL + grace)
display_expires_at= the QR / payment-page display time = creation time + 5 minutes (display TTL).match_window_until=display_expires_at+ 2 minutes (grace) = the last moment the system will still match an inbound amount.- Once
match_window_untilpasses with no matching inbound amount, the deposit is swept toEXPIRED.
So the total window in which the customer should finish transferring is within ~5 minutes (plus a 2-minute grace for a slow bank feed, ~7 minutes total).
Outcome arrives via webhook
deposit.success on CREDITED and deposit.expired on EXPIRED. Never treat a 201PENDING as final — wait for the terminal webhook. See payload structure and verification under Webhooks.Points merchants must watch
- Every money field is a baht string (
"500.37") — don't send/parse it as a number or satang. - The customer must transfer the exact
expected_amount(notamount) for matching — useqr_payloadto make the amount correct automatically. - Send an
Idempotency-Keyon every create to prevent duplicate deposits from retries. - Send the full payer for both methods — used for anti-fraud checks.
- Deposits are short-lived (~5–7 minutes) — show the QR and let the customer transfer immediately; if it expires you must create a new one.
Error envelope & codes
Every error uses one JSON envelope — branch on error.code, never the message — plus the consolidated cross-cutting error reference
Create a Deposit
POST /v1/deposits — create a deposit with HMAC + Idempotency-Key; returns expected_amount and pay_to / qr_payload with status PENDING
