Skip to content
UnknownPay
ไทย
Deposits

Deposit Overview & State Machine

How a merchant takes money in via UnknownPay S2S — from creating a deposit to exact-amount matching, crediting, and the PENDING → CREDITED / EXPIRED / CANCELLED 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".

Money reminder (very important): every money field on the wire is a baht string with 2 decimals, e.g. "100.50" — not a JSON number and not satang. Fields such as amount, expected_amount, matched_amount must always be sent/read as strings.
Diagram coming soon — sequence diagram — Merchant → POST /v1/deposits → returns expected_amount + QR → customer transfers the exact amount → bank feed → matcher → deposit CREDITED → merchant balance increases
OperationMethod + PathAuth
Create a depositPOST /v1/depositsS2S (HMAC) + Idempotency-Key
Get / retrieve a depositGET /v1/deposits/:idS2S (HMAC)
Cancel a depositPOST /v1/deposits/:id/cancelS2S (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.

These paths are also the paths the HMAC signature matches against. The system is mounted under the /v1 prefix, so the full path is /v1/deposits and so on.
Pending confirmation — confirm the gateway router prefix is /v1 and matches production ingress

State machine

                 ┌───────────────┐
                 │    PENDING    │  (initial status after creation)
                 └───────┬───────┘
        ┌────────────────┼─────────────────────┐
   customer transfers     past match_window      merchant/system cancels
   exact expected_amount         │                       │
        ▼                       ▼                       ▼
   ┌───────────┐          ┌───────────┐          ┌─────────────┐
   │ CREDITED  │          │  EXPIRED  │          │  CANCELLED  │
   └───────────┘          └───────────┘          └─────────────┘
     (terminal)             (terminal)             (terminal)
StatusMeaning
PENDINGCreated; waiting for the customer to transfer the exact expected_amount within the time window.
CREDITEDInbound money matched — credited to the merchant balance (matched_amount = the amount actually received). terminal
EXPIREDThe match_window elapsed with no matching inbound amount → expired. terminal
CANCELLEDCancelled 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".

The customer must transfer the exact 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_until passes with no matching inbound amount, the deposit is swept to EXPIRED.

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).

Pending confirmation — confirm the display TTL (5 minutes) and grace (2 minutes) match current production config

Outcome arrives via webhook

The final deposit result arrives via Webhook. The system fires 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

  1. Every money field is a baht string ("500.37") — don't send/parse it as a number or satang.
  2. The customer must transfer the exact expected_amount (not amount) for matching — use qr_payload to make the amount correct automatically.
  3. Send an Idempotency-Key on every create to prevent duplicate deposits from retries.
  4. Send the full payer for both methods — used for anti-fraud checks.
  5. 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.