Core Concepts
Error envelope & codes
Every error uses one JSON envelope — branch on error.code, never the message — plus the consolidated cross-cutting error reference
Every error the system returns uses the same JSON structure — a merchant can branch logic on the error.code field (machine-readable, stable), with no need to parse the message text.
1. JSON structure
{
"error": {
"code": "INVALID_AMOUNT",
"message": "amount must have at most 2 decimal places",
"request_id": "0H1K2L3M4N5P6Q7R8S9T"
}
}
Fields in the envelope:
| field | type | description |
|---|---|---|
error.code | string | A stable error code for branch logic |
error.message | string | A safe human-readable message (no internal/secret data) |
error.request_id | string | A request id to quote when reporting an issue to the team |
error.details | object | (present in some cases) extra safe key-value data — dropped if absent |
For server-level errors (HTTP 5xx) the system will not reveal internal detail —
message will be a generic string (e.g. "internal error") while the real cause is logged on the system side. Use the request_id so the team can trace it.2. HTTP status + error code table the merchant must handle
| HTTP | code | Occurs when | What the merchant should do |
|---|---|---|---|
400 | IDEMPOTENCY_KEY_REQUIRED | A money POST is called without an Idempotency-Key | Add the header and resend |
400 | BAD_REQUEST | A generally malformed request | Fix the request |
401 | UNAUTHORIZED | Credential invalid/expired/bad signature | Check the API key + signing |
403 | FORBIDDEN | No permission / out of allowed scope (e.g. IP not in the allowlist) | Check permission/allowlist |
404 | NOT_FOUND | Resource not found, or not the caller's (existence not revealed) | Check the referenced id |
405 | METHOD_NOT_ALLOWED | Wrong HTTP method for the path | Check the method |
409 | IDEMPOTENCY_IN_PROGRESS | A record with the same key is currently processing | Wait and retry |
413 | PAYLOAD_TOO_LARGE | Body exceeds the limit | Reduce the body size |
422 | INVALID_AMOUNT | Malformed money field (negative/>2dp/scientific/out-of-range/empty) | Fix the amount per the rules in Money Format |
422 | INVALID_CURRENCY | Currency is not THB | Send "THB" only |
422 | VALIDATION | Request fails general validation (e.g. body is not valid JSON) | Fix the request |
422 | IDEMPOTENCY_KEY_MISMATCH | Reused the same Idempotency-Key with a different body | Use a new key, or send the same body |
500 | INTERNAL | A system-side error | Retry later + keep the request_id to report to the team |
Pending confirmation — domain-level error codes (e.g. pool/limit/state) exist per endpoint — collect the deposit/withdrawal-specific codes into each endpoint's docs; this table only covers cross-cutting codes (foundation + money + idempotency + HTTP mapping) confirmed from the code
Business-specific codes (e.g. pool out of funds, over a limit, a state that does not allow cancel) are additional codes defined per endpoint — a merchant should branch on
error.code in a default-safe way: treat any unknown code as an error and log the request_id.3. HTTP & Content-Type conventions
- Content-Type: a request with a body must be
application/json, and every response (including errors and idempotency replays) isapplication/json. - Request ID: every error response has
error.request_idfor referencing the issue (keep it every time you hit an error). - 2xx = success, 4xx = merchant-side request error (fix then resend), 5xx = system-side (safe to retry if you use
Idempotency-Keyon a money POST). - Always use HTTPS.
Pending confirmation — confirm whether an X-Request-Id response header exists — from the code, request_id appears mainly in the error body; if we communicate an echo header, confirm the requestid middleware first
Diagram coming soon — sequence diagram showing the flow of retrying a money POST with the same Idempotency-Key → the system returns the original response with header Idempotent-Replay: true
Idempotency
Every money-moving POST requires an Idempotency-Key header so a retry returns the original result instead of creating a duplicate
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
