Core Concepts
Idempotency
Every money-moving POST requires an Idempotency-Key header so a retry returns the original result instead of creating a duplicate
Every endpoint that creates/moves money (a money-moving POST) must send a header to prevent duplication. If the network times out and the merchant retries, the system returns the original result instead of creating a duplicate record.
1. Header
| Header | Direction | Meaning |
|---|---|---|
Idempotency-Key | request (merchant sends) | A merchant-defined key bound to a single transaction attempt |
Idempotent-Replay | response (system returns) | If set to true, this response is a replay from cache — no new record was created |
2. Endpoints that require it (money-moving POST on the S2S side)
| Method + Path | Description |
|---|---|
POST /v1/deposits | Create a deposit |
POST /v1/withdrawals | Create a payout/withdrawal |
If you call these endpoints without sending the
Idempotency-Key header, the system responds with HTTP 400 and code IDEMPOTENCY_KEY_REQUIRED immediately — it is a mandatory header.GET / cancel / read-back endpoints (e.g. GET /v1/deposits/:id, GET /v1/withdrawals, POST /v1/deposits/:id/cancel) do not need to send Idempotency-Key.Pending confirmation — confirm the full list of S2S money-moving POSTs, in case there are additional S2S endpoints that create money
3. TTL and replay behavior
- TTL = 24 hours — within 24 h, a retry with the same key returns the original result. After that the old key is considered expired (purged) and is treated as a new record.
- The scope of the key is separated by the (mode, entity) of the caller that the system derives from the credential — i.e. the same key in live and test will not collide (a test replay will not return a live result, and vice versa). The merchant does not have to manage scope.
Behavior across 3 cases:
| Situation | Result |
|---|---|
| Same key + same body (path/method/body all match) | The system replays the original response (same status + body) with header Idempotent-Replay: true — no new record is created |
| Same key + different body | The system rejects with HTTP 422 code IDEMPOTENCY_KEY_MISMATCH — "Idempotency-Key was reused with a different request" (prevents reusing a key for a different transaction) |
| New key | Processed as a new record as usual |
The system binds the key to the method + path + the entire body (a hash), so even a tiny change to the body with the same key is rejected as
IDEMPOTENCY_KEY_MISMATCH — when retrying you must send the exact same body.If a prior record failed with a server-level error (HTTP 5xx), the system rolls everything back — a retry with the same key can re-run cleanly (it does not get stuck on a dangling in-progress).4. Recommended key format
- The system does not enforce a format or length for the key — it accepts any string that is non-empty but must be unique per record.
- Use a UUID v4 (random, collision chance near zero), e.g.
9f1c2e7a-3b4d-4f8a-9c10-2b6d5e7f8a90. - Generate 1 key per transaction intent and keep that key so you can retry with the same key when the network fails.
Example request (deposit) with idempotency:
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 per the canonical string>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9f1c2e7a-3b4d-4f8a-9c10-2b6d5e7f8a90" \
-d '{
"amount": "100.50",
"currency": "THB"
}'
For how to compute
X-Signature, see Authentication or use a ready-made client from the Code Samples that signs automatically.Pending confirmation — confirm the Authorization / HMAC header form of S2S in the Authentication section; this example uses only fields that exist (amount, currency) — add other CreateReq fields per the real spec
