Rate limits & idempotency
Per-key limits with X-RateLimit headers and Retry-After, plus Idempotency-Key semantics: replays, conflicts, the 24-hour window.
Two mechanisms keep your integration reliable under load and under retries: per-key rate limits, and idempotent invoice creation.
Rate limits
Limits are per API key, in a fixed 60-second window:
| Scope | Endpoints | Live key | Sandbox key (pk_test_…) |
|---|---|---|---|
| Invoice creation | POST /v1/invoices | 30 / min | 10 / min |
| Reads | GET /v1/invoices…, /v1/events, /v1/payouts | 300 / min | 90 / min |
Every authenticated response carries the counters, so you never have to guess:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
X-RateLimit-Reset: 1694007937
X-RateLimit-Reset is a Unix timestamp of the window end. When you exceed the limit the API answers 429 rate_limited with an extra Retry-After: <seconds> header — wait it out, don't tight-loop. If your checkout legitimately bursts higher, batch order creation or contact us about a raised limit.
Polling guidance: GET /v1/invoices/{id} once every few seconds per open checkout is far below the read budget. You rarely need to poll at all — the webhook is the intended push channel; poll as a fallback.
Idempotent invoice creation
POST /v1/invoices accepts an Idempotency-Key header (up to 120 characters). Send it on every creation — it turns network-timeout retries from "accidental double invoice" into "same invoice returned again":
- Same key + same body within 24 hours → the original invoice is returned again with
201and an extraIdempotent-Replay: trueheader. Not a duplicate, not an error. - Same key + different body →
409 idempotency_conflict. The key is bound to the exact request body (canonically hashed); change the payload — change the key. - Keys are scoped per merchant and per mode: the same key used with a
pk_test_…and later apk_live_…key never collide.
curl -X POST https://api.zerokyc-payments.com/v1/invoices \
-H "Authorization: Bearer pk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-create" \
-d '{"amount": "49.00", "order_id": "order-1042", "payment_currency": "any"}'
A good key is deterministic — derive it from your order id and intent (order-1042-create), so a retry after a crash or a timeout reuses it automatically. Random keys defeat the purpose.
Putting both together
- Create with a deterministic
Idempotency-Key; treat both201(with or withoutIdempotent-Replay) as success and store the invoice id against your order. - On
429, sleep forRetry-Afterseconds and retry the same request — idempotency makes it safe. - On
5xx/timeouts, retry with exponential backoff, same key, same body. - Fulfil on the
payment.confirmedwebhook; use reads for reconciliation, not as the primary signal.