Rate limits
Flint rate limits every public API request so one integration's traffic spike cannot degrade the platform for everyone else. When you do exceed one, the request fails fast with 429 Too Many Requests and a Retry-After header that tells you exactly when to try again.
How limits work#
Every request to /v1 passes through two checks:
- An emergency IP limit on all traffic, applied before authentication.
- Class-based budgets for authenticated callers. Public unauthenticated routes and failed authentication attempts also consume a tighter IP budget.
Each authenticated endpoint belongs to a route class (read, write, payment, sensitive_write, or external_provider_action), and each class defines four budgets that are checked on every request:
- requests per second and per minute, per API key
- requests per second and per minute, per merchant
Exceeding any one of the four returns 429. The merchant budgets are shared across every key that belongs to the same merchant, so creating more keys does not create more capacity.
Test and live traffic have identical limits but are counted separately, and each sandbox has its own counters within test mode.
Sandbox traffic does not consume live class budgets. Traffic from the same IP still shares the IP limits across modes.
These rate limits are the current defaults. They may be tuned over time as usage patterns change; if your workload needs more, see Raising Your Limits.
Default limits#
| Route class | Applies to | Per API key | Per merchant |
|---|---|---|---|
read | All GET endpoints | 60/sec, 3,000/min | 200/sec, 12,000/min |
write | General resource creation and updates | 20/sec, 600/min | 60/sec, 2,400/min |
payment | Checkout sessions, order preparation, and payment actions | 20/sec, 600/min | 60/sec, 2,400/min |
sensitive_write | Credentials, settings, and account structure | 10/sec, 120/min | 30/sec, 600/min |
external_provider_action | Refunds, payouts, invoice reminders, and other external actions | 5/sec, 120/min | 20/sec, 300/min |
Read#
Every GET endpoint: fetching a single resource, listing a collection, or downloading an invoice PDF. This is the most generous class, sized for dashboards and reconciliation jobs.
Examples: GET /v1/orders, GET /v1/customers/{customer_id}, GET /v1/payment-intents.
Write#
Creating and updating resources such as products, customers, promotions, payment links, and invoices uses the write budget. Checkout and payment operations use a separate budget.
Examples: POST /v1/products, POST /v1/customers, PATCH /v1/customers/{customer_id}.
Payment#
Creating and updating checkout sessions, preparing orders, and creating, confirming, capturing, or canceling payments share the payment budget. Invoice checkout-session creation also uses this budget. Refunds, payouts, invoice reminders, and receipt resends use a separate budget, so those operations cannot consume the payment allowance.
Examples: POST /v1/orders, POST /v1/checkout-sessions, POST /v1/orders/{order_id}/pay, POST /v1/payment-intents/{payment_intent_id}/confirm.
Sensitive write#
Operations that change credentials, configuration, or account structure. These are rare in steady-state traffic, so the class is tighter as an abuse guard.
Examples: POST /v1/api-keys, PATCH /v1/api-keys/{api_key_id}, POST /v1/webhook-endpoints/{webhook_endpoint_id}/rotate-secret, PATCH /v1/settings.
External provider action#
Refunds, payouts, invoice issuance and reminders, receipt resends, and other external actions share the external_provider_action budget. Pace batches against this budget independently of checkout and payment traffic.
Examples: POST /v1/refunds, POST /v1/payouts, POST /v1/invoices/{invoice_id}/issue.
IP limits#
All public API traffic is subject to an emergency ceiling of 1,000 requests per second and 60,000 per minute for each client IP. This check runs before authentication to bound incoming floods.
Public unauthenticated routes and failed authentication attempts also share a tighter budget of 50 requests per second and 3,000 per minute per IP. Sending an invalid credential still consumes this budget. Authenticated requests use their class budgets without consuming the tighter IP allowance.
Workers behind one outbound IP share the emergency ceiling. IP counters include traffic across merchants, live mode, and sandboxes, even though authenticated class budgets are counted separately.
The 429 response#
A rate-limited request fails with HTTP 429 and the standard error envelope:
{
"error": {
"type": "rate_limit_error",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded.",
"request_id": "3e7d1c9a-5b2f-4e8c-9a41-d6f0b3a8c527"
}
}
Two response headers tell you how to react:
| Header | Meaning |
|---|---|
Retry-After | Whole seconds to wait before retrying. Present whenever Flint knows the wait; always at least 1. |
Flint-Rate-Limited-Reason | Which limit the request hit. Use it to pick the right fix. |
Flint-Rate-Limited-Reason takes one of these values:
api-keymerchantglobal-ipsetup-flowdemo-sessionresourceRetry-After and pace calls to that endpoint.Flint does not currently return remaining-quota headers such as X-RateLimit-Remaining. Treat 429 plus Retry-After as the complete signal, and design for backpressure rather than budget accounting.
RATE_LIMIT_EXCEEDED is always retryable; it is catalogued with every other code in the error catalog. As with any error, log the request_id so support can trace the exact request.
Handling a 429#
A 429 is backpressure, not failure. The recipe:
- Honor
Retry-Afterwhen it is present. It is the server telling you the exact wait; sleeping longer wastes time and sleeping less guarantees another429. - Fall back to exponential backoff when it is absent, and add jitter so parallel workers do not retry in lockstep.
- Cap total attempts and surface the failure to your own monitoring instead of retrying forever.
- Send an
Idempotency-Keyon writes so a retry can never create a duplicate order, refund, or charge. Replays return the original result and are marked with anIdempotency-Replayed: trueresponse header.
async function withRateLimitRetry(fn, maxAttempts = 5) {
let backoffMs = 500;
for (let attempt = 1; ; attempt++) {
const res = await fn();
if (res.status !== 429 || attempt === maxAttempts) return res;
const retryAfter = Number(res.headers.get("Retry-After"));
const waitMs = retryAfter > 0 ? retryAfter * 1000 : backoffMs;
await new Promise((r) => setTimeout(r, waitMs + Math.random() * 250));
backoffMs = Math.min(backoffMs * 2, 30_000);
}
}
// The Idempotency-Key makes the retry replay the original create
// instead of producing a second order.
const res = await withRateLimitRetry(() =>
fetch("https://api.withflintpay.com/v1/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FLINT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-create-7f3d2a91",
},
body: JSON.stringify({ currency: "USD" }),
}),
);
Never retry a 429 in a tight loop. Immediate retries count against the same budgets, so a hot loop keeps the budget pinned at zero and starves the requests you care about. If retries themselves are getting rate limited, the fix is less concurrency at the caller, not more retries.
Staying under the limits#
The limits are sized so that an integration built on these patterns rarely sees a 429 at all.
Subscribe to webhooks instead of polling. This is the single biggest lever. Payment, refund, payout, and dispute outcomes arrive as webhook events the moment they happen. A loop that polls GET /v1/orders/{order_id} every second per open order burns read budget linearly with volume and still learns the outcome later than the webhook would have delivered it.
Fetch lists, not loops of GETs. One GET /v1/orders?page_size=100 costs one request against the read budget; fetching the same 100 orders individually costs 100. Use list endpoints with pagination for reconciliation and sync jobs.
Pace batch jobs and spread scheduled work. A backfill or nightly sync should run behind a client-side queue with a fixed concurrency, not a Promise.all over thousands of items. If several cron jobs fire at the top of the hour, add jitter to their start times so they do not spend the same one-second budget.
Design for the merchant budget, not just your key. Every worker and service acting for one merchant draws from the same merchant-level budget. Giving each service its own key is still worth doing, because per-key budgets isolate a runaway service and Flint-Rate-Limited-Reason tells you which side you hit, but the merchant ceiling only moves if Flint raises it.
Watch your own traffic. Request logs in Debugging Requests show every call your keys make, including 429s, so you can spot a hot loop before it matters.
Setup and onboarding throttles#
The zero-to-first-key setup flow has its own anti-abuse throttles, separate from and much tighter than the class budgets. They apply per client IP and per email address:
| Endpoint | Per IP | Per email |
|---|---|---|
POST /v1/onboarding/start | 3/sec, 10/min | 3 per 30 sec |
POST /v1/onboarding/verify-email | 6/sec, 30/min | 6 per 10 sec |
POST /v1/onboarding/api-key | 2/sec, 12/min | none |
POST /v1/onboarding/api-key is also subject to the sensitive_write budgets once the onboarding session exists. GET /v1/onboarding/state and POST /v1/onboarding/advance follow their normal route classes (read and external_provider_action).
These 429s carry Flint-Rate-Limited-Reason: setup-flow. If you are building an agent that onboards itself, run the setup flow once, store the key, and reuse it; a pipeline that re-onboards on every run will hit these throttles almost immediately.
Demo sandbox limits#
Keys issued through the interactive demo flow on this site run with much lower limits, sized for exploration rather than sustained traffic:
| Route class | Per API key | Per merchant |
|---|---|---|
read | 10/sec, 120/min | 20/sec, 240/min |
write | 2/sec, 30/min | 4/sec, 60/min |
payment | 1/sec, 10/min | 2/sec, 20/min |
sensitive_write | 1/sec, 10/min | 2/sec, 20/min |
external_provider_action | 1/sec, 10/min | 2/sec, 20/min |
These 429s carry Flint-Rate-Limited-Reason: demo-session. Creating demo sandboxes is itself limited per IP, and there is a daily cap that returns 429 with the distinct code DAILY_LIMIT_EXCEEDED rather than RATE_LIMIT_EXCEEDED.
Demo limits exist so throwaway sandboxes stay throwaway. The moment you are testing anything sustained, create your own sandbox and use its key: you get the full default limits and your data persists.
Raising your limits#
If your expected steady-state or peak traffic exceeds these limits, contact Flint before launching that workload. Include any batches that run alongside checkout traffic.
Ask for a higher limit at Flint Help with the endpoints involved, your expected steady-state and peak request rates, and the merchant or merchants the traffic runs under.
Common mistakes#
- Retrying in a tight loop. Immediate retries drain the same budget the original request exhausted, guaranteeing more
429s. HonorRetry-Afterand back off. - Polling for outcomes webhooks already deliver. Payment status transitions are pushed to your webhook endpoint; polling for them is the most common way integrations waste their read budget.
- Treating per-key limits as the whole picture. Ten workers with ten keys still share one merchant budget. When
Flint-Rate-Limited-Reasonsaysmerchant, adding keys changes nothing. - Retrying writes without an
Idempotency-Key. A request can be rate limited on your side but a retry without a key of a request that succeeded creates a duplicate. Always pair retries with idempotency keys. - Surfacing raw 429s to buyers. A rate-limited call inside a checkout flow should be absorbed and retried behind the scenes. Buyers should see a brief delay, never a rate limit error.
Next steps#
- Error handling: the full error envelope and a retry strategy for every error type.
- Idempotency: make write retries safe by construction.
- Webhooks: event-driven updates that replace polling.
- Debugging Requests: inspect your traffic and find what is eating your budget.
- Error catalog: every error code, including
RATE_LIMIT_EXCEEDED. - Going live: the pre-launch checklist, including retry and idempotency review.
