Appearance
Idempotency
Network calls fail. Partner mutations are safe to retry if you persist the key.
Where a key is required
| Call | idempotency_key |
|---|---|
POST /bookings/hold | required |
POST /bookings/confirm | required |
POST /partner/holds/{id}/cancel | required |
POST /bookings/{id}/cancel | required |
POST /bookings/{id}/refund | required |
POST /packages/{id}/purchase | required |
POST /waitlist | not used — naturally unique per partner on venue + resource + date + phone (Waitlist) |
A missing key on a required call is a 400, with a different error depending on the call:
| Call | Missing idempotency_key |
|---|---|
POST /bookings/hold, POST /bookings/confirm | 400 VALIDATION_ERROR — request validation; details names the idempotency_key field |
POST /partner/holds/{id}/cancel, POST /bookings/{id}/cancel, POST /bookings/{id}/refund, POST /packages/{id}/purchase | 400 IDEMPOTENCY_KEY_REQUIRED |
Either way, nothing was executed: add the key and send the request again.
Generate a UUID v4 per logical action in your system. Persist it with that action. Send the same string on every retry until you abandon the action. Then never reuse it.
Scope
Keys are scoped to your partner identity (from the API key), not globally. Two partners may use the same string without colliding. Generate unique keys anyway.
Redis caches the response for one hour. Confirm, cancel, and refund also write a durable row in the same database transaction as the state change. Hold and package purchase have a durable unique index. A retry after a cache flush still replays.
Replay does not re-check venue grants. If a grant is revoked after a mutation succeeded, replaying that mutation still returns the original success.
Confirm, cancel, refund, package purchase
| Situation | Result |
|---|---|
| First request | Executes |
| Same partner + same key + same logical operation | Original success replayed (same ids, no second side effect) |
| Same key used for a different hold (confirm), a different booking (cancel, refund), or the other operation (cancel vs refund) | 409 IDEMPOTENCY_KEY_MISMATCH |
Same key on the same hold or booking with a different body (customer name or phone, payment_reference, reason) | Original success replayed. The new body is ignored: confirm, cancel and refund keys are matched on the operation and the hold or booking id only. |
| Package purchase: same key, different package or customer | 409 (package-purchase conflict) |
| Second cancel/refund after the booking is already terminal, without a matching stored key | 409 INVALID_HOLD_STATE |
Hold creation
Since 2026-10-04 a hold key is bound to what it asked for (see the changelog).
| Situation | Result |
|---|---|
| First hold | 201, new hold_id |
| Same key, same request | 201, the original hold_id, never a second ACTIVE hold |
Same key, different venue, slot time, duration, court type or customer_phone | 409 IDEMPOTENCY_KEY_MISMATCH, nothing held |
| Same key, after that hold expired | 409 HOLD_EXPIRED |
| Same key, after that hold was cancelled | 409 INVALID_HOLD_STATE |
The court is compared by type (badminton-court), not number: a retry that names another court of the same type at the same venue and time replays the original hold. Do not reuse a hold key across requests. If you need a second hold, generate a new key.
Timeout recipe
text
1. Create UUID, save it on your local booking/hold row.
2. POST with that idempotency_key.
3. If you see 5xx or a timeout, wait briefly and POST the same body again.
4. If you see 4xx (except 409/429), stop. Inspect error.Keep the key outside your HTTP helper. If the helper generates a new UUID per attempt, you have lost idempotency.
