Idempotency
To prevent retries from causing duplicate card opens / duplicate charges, write endpoints require an Idempotency-Key header.
Which Endpoints Require It
| Endpoint | Required? |
|---|---|
/api/v1/openapi/card/apply | ✅ Yes |
/api/v1/openapi/card/recharge | ✅ Yes |
All /list and /info endpoints | ❌ No |
Header
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000| Constraint | Value |
|---|---|
| Length | ≤ 128 chars |
| Charset | URL-safe (UUID v4 / ULID recommended) |
| Dedup window | 24 hours |
| Missing | 400 openapi_idempotency_key_required |
| Too long | 400 openapi_idempotency_key_too_long |
| Bad characters | 400 openapi_idempotency_key_invalid_chars |
| Same key, different body | 409 openapi_idempotency_key_conflict |
Server Behavior
Same key + same body
Returns a copy of the previous response (the original status code and data). If the first call is still processing, the call waits and returns the final result. Does not trigger a second card open.
The replayed response carries an extra response header so you can tell it apart from a fresh call:
X-Idempotent-Replay: trueDetecting replays
Treat the X-Idempotent-Replay: true header as informational. Either way the response body is the authoritative source of truth — but the header is useful for metrics and debugging ("this attempt actually hit the server but I retried after a network blip").
Only 2xx responses are cached
The server only caches the response body when the original call returned 2xx. If the first call returned 4xx (e.g. insufficient_balance) or 5xx, the same key + same body will be re-attempted on the next request — there's nothing to replay.
In other words, idempotency protects you against losing a successful response, not against repeating a failed business call.
Captured response size
The server caches at most 256 KB of the response body. The current OpenAPI responses are well under that ceiling, so this should never be a concern in practice — flagged only for completeness.
Same key + different body
Returns 409 openapi_idempotency_key_conflict. The server refuses to process. Client should switch keys or fix the body.
Different keys + same body
Treated as two independent requests — opens two cards / charges twice. By design — idempotency is keyed by the header, not the body.
Don't use a body hash as the key
Using "request body hash" as the Idempotency-Key would deduplicate "user clicks recharge twice for the same amount" into a single charge. The key represents one business intent; generate a new key for each new business intent.
Recommended Patterns
Client SDK pattern
import uuid
def apply_card(header_id, package_id):
key = str(uuid.uuid4()) # new key per business call
return call("/api/v1/openapi/card/apply",
{"header_id": header_id, "package_id": package_id},
idempotency_key=key)Async-task pattern
If your "apply card" task is retried by a job queue on network failure, bind the key to the task record:
def open_card_task(task_id, header_id, package_id):
# All retries of the same task use the same key
key = f"openapi:apply:{task_id}"
return call("/api/v1/openapi/card/apply", {...}, idempotency_key=key)That way, even if the task is retried N times, Coinepay opens at most one card.
Relationship with Signatures
Idempotency-Key is not part of the signing input (the signature covers method/path/query/timestamp/nonce/body sha256). Each request still needs a fresh nonce. They are two independent mechanisms:
| Mechanism | Protects against |
|---|---|
| Nonce | An attacker capturing a signature and replaying the request |
| Idempotency-Key | You safely retrying after a network failure |
Coordinating both: when the client retries the same business call, use the same Idempotency-Key + new Nonce + new Timestamp + new Signature.
Error Responses
Missing (400)
{
"code": 400,
"message": "Missing Idempotency-Key header",
"message_key": "openapi_idempotency_key_required",
"data": null
}Conflict (409)
{
"code": 409,
"message": "Idempotency-Key conflict",
"message_key": "openapi_idempotency_key_conflict",
"data": null
}