Idempotency
A request that timed out may still have succeeded. If your POS simply sends it again, the customer could get their stamps twice. An Idempotency-Key makes the retry safe: ServeU remembers the first answer and gives it back instead of doing the work again.
Sending a key
- Send
Idempotency-Keyon every write: 1–100 visible ASCII characters (no spaces). Use something that is unique per action, such as your receipt number (receipt-10422) or a UUID. - It is required on earn, redeem, void and order actions: without it they answer
400 idempotency_key_required. Joining a customer and menu writes take one optionally. - Keys are per API key: two systems can't collide.
What happens on a retry
- Same key and same request within 24 hours: you get the stored answer, with the header
Idempotent-Replayed: true. Error answers are stored and replayed too. - So a stored error is the answer for that key for 24 hours. When you fix its cause (the order is now paid, you send fewer stamps, the owner set up a loyalty program), send the request with a new key: retrying with the old one answers the same
payment_required,order_not_actionable,card_cap_reachedorno_programagain. A key that includes the state you acted on (an order'supdated_at, the stamps you send) changes by itself. - "Same request" means the same method, path and body, byte for byte: when you retry, send the same JSON text, not a rebuilt one with fields in another order.
- Same key with a different request:
409 idempotency_conflict. - The first request is still running:
409 idempotency_in_progresswithRetry-After: 1. Wait a second and retry with the same key. - Not stored, so retrying with the same key is safe:
5xxanswers,409 conflict,409 idempotency_in_progressand429 rate_limited. Refusals about the key or the business aren't stored either, even when they happen mid-request (401 unauthorized,403 insufficient_scope,api_disabled,business_suspended), nor is a body that fails validation before the request runs (400 invalid_requestwith field details): once the owner has fixed the key or you've fixed the body, the same key runs the request. - A replayed error keeps the original
request_idin its body; theX-Request-Idheader is the retry's own. - A request runs for at most 30 seconds, so set your client timeout above that, or retry with the same key and handle
idempotency_in_progress.
A retry loop
import json, time, requests
BASE = "https://serveu-psi.vercel.app/api/v1"
key = f"receipt-{receipt_no}"
# Build the body once, so every retry sends exactly the same bytes.
body = json.dumps({"stamps": 1, "reference": str(receipt_no)})
headers = {
"Authorization": f"Bearer {SERVEU_API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": key,
}
for attempt in range(5):
try:
r = requests.post(f"{BASE}/cards/{card_id}/earn", headers=headers, data=body, timeout=35)
except requests.RequestException:
time.sleep(2 ** attempt)
continue
if r.status_code >= 500 or r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
continue
if r.status_code == 409 and r.json()["error"]["code"] in ("idempotency_in_progress", "conflict"):
time.sleep(1)
continue
break # done: a success, or an answer to show (e.g. card_cap_reached)
else:
raise RuntimeError(f"ServeU API: no answer for {key} after 5 attempts")Earns and redeems are also protected inside the ledger: even after the 24 hours, the same API key and
Idempotency-Key can never earn twice. Voids are protected by the event itself: retrying a void that already happened answers 409 already_voided, which means it is done.