Rate Limits
Every Genius Checkout API endpoint sits in one of three tiers. Limits are per-API-key (per-merchant for authenticated traffic, per-IP for unauthenticated traffic) and refresh on a rolling one-minute window.
Tiers
| Tier | Limit | What it covers |
|---|---|---|
payment-mutations | 60 / minute | Money-moving writes — create payment, refund, capture, void, tokenize, charge-token, create subscription, cancel subscription, deactivate token |
writes | 120 / minute | Reads + non-money writes — list/get any resource, create/pause/archive payment link, pause/resume subscription, Zapier subscribe/unsubscribe |
public-reads | 300 / minute | Unauthenticated read paths (currently used only by public payment-link rendering) |
Endpoint → tier reference
| Endpoint | Tier |
|---|---|
POST /payments, POST /payments/{id}/capture, POST /payments/{id}/refund, POST /payments/{id}/void | payment-mutations |
GET /payments/{id} | writes |
POST /checkout-sessions | writes |
GET /checkout-sessions/{id} | writes |
POST /tokenize, POST /charge-token | payment-mutations |
POST /subscriptions, POST /subscriptions/{id}/cancel | payment-mutations |
GET /subscriptions, GET /subscriptions/{id}, POST /subscriptions/{id}/pause, POST /subscriptions/{id}/resume, PUT /subscriptions/{id}/payment-method | writes |
GET /transactions, GET /customers, GET /customers/{id}/payment-tokens | writes |
POST /payment-tokens/{id}/deactivate | payment-mutations |
GET /payment-links, POST /payment-links, GET /payment-links/{id}, POST /payment-links/{id}/* | writes |
POST /zapier/subscriptions, DELETE /zapier/subscriptions/{id}, GET /zapier/sample-payments | writes |
Response headers
Every authenticated response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Maximum requests in the current window for this tier |
X-RateLimit-Remaining | Requests left before the next 429 |
When you breach the limit, the 429 response additionally includes:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the window resets |
429 body
json
{
"error": "Too many requests. Please try again later.",
"retry_after": 17
}retry_after matches the Retry-After header in seconds.
Backoff guidance
- Always honor
Retry-After— don't poll harder. - Use exponential backoff with jitter if you can't read the header (e.g., a third-party HTTP client that swallows it):
min(60s, base * 2^n + random(0, 1s))starting atbase = 1s. - For batch jobs (mass refunds, bulk re-charges), stay below 50/min on payment-mutations to leave headroom for any in-flight customer-driven traffic on the same key.
- Use Idempotency-Key on every retry so you can safely re-fire a request without double-charging.
js
async function callWithBackoff(url, init, attempt = 0) {
const res = await fetch(url, init)
if (res.status !== 429) return res
const wait = Number(res.headers.get('Retry-After')) || 2 ** attempt
await new Promise(r => setTimeout(r, wait * 1000))
return callWithBackoff(url, init, attempt + 1)
}python
# Python — same pattern with `requests`
import requests, time
def call_with_backoff(method, url, **kwargs):
for attempt in range(6):
r = requests.request(method, url, **kwargs)
if r.status_code != 429:
return r
wait = int(r.headers.get('Retry-After', 2 ** attempt))
time.sleep(wait)
return rBurst headroom
Limits are evaluated per-key. Splitting traffic across multiple API keys is not a sanctioned way to multiply your effective limit — if you need higher throughput on a single merchant account, contact support and we can raise the tier on your key.
Next
- Error Handling — full status-code table
- Idempotency — safe retries on 429 and 5xx
