Rate Limits

Flint rate limits every public API request so one integration's traffic spike cannot degrade the platform for everyone else. The limits are generous enough that most integrations never notice them. 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.

This guide covers the default limits, how to read a 429, how to retry safely, and the design patterns that keep well-built integrations under the limits without thinking about them.

How Limits Work#

Every request to /v1 passes through two checks:

  1. An IP-level limit on all traffic, applied before authentication.
  2. Class-based budgets for the authenticated caller.

Each authenticated endpoint belongs to a route class (read, write, 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.

Because test and live counters are independent, load testing against a sandbox never consumes your live budget, and a sandbox stress test tells you exactly how your integration will behave against the same limits in live mode.

The numbers on this page 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 classApplies toPer API keyPer merchant
readAll GET endpoints60/sec, 3,000/min200/sec, 12,000/min
writeMost create and update endpoints20/sec, 600/min60/sec, 2,400/min
sensitive_writeCredentials, settings, and account structure10/sec, 120/min30/sec, 600/min
external_provider_actionActions that move money or leave Flint5/sec, 120/min20/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 inside Flint: orders, products, customers, coupons, checkout sessions, payment links, invoices. Most POST, PATCH, PUT, and DELETE endpoints are in this class.

Examples: POST /v1/orders, POST /v1/checkout-sessions, PATCH /v1/customers/{customer_id}.

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#

The tightest class: actions that move money or trigger side effects outside Flint, such as charging a card, issuing a refund, sending a payout, or emailing an invoice. These operations are deliberate, one-at-a-time events in a healthy integration, so a burst of them is almost always a bug or a batch job that needs pacing.

Examples: POST /v1/payment-intents/{payment_intent_id}/confirm, POST /v1/refunds, POST /v1/payouts, POST /v1/invoices/{invoice_id}/send.

A few endpoints deviate from the class their HTTP verb suggests. For example, POST /v1/orders/{order_id}/pay charges the buyer, so it is an external provider action even though other order writes are plain writes. When you are sizing a batch job around a money-moving endpoint, assume the tighter class.

IP Limit#

Before authentication, all /v1 traffic is limited to 50 requests per second and 3,000 per minute for each client IP. This protects the API from unauthenticated floods. Legitimate integrations under their class budgets stay well below it; if you operate many workers behind one egress IP, count their combined throughput against this ceiling too.

The 429 Response#

A rate-limited request fails with HTTP 429 and the standard error envelope:

JSON
{
  "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:

HeaderMeaning
Retry-AfterWhole seconds to wait before retrying. Present whenever Flint knows the wait; always at least 1.
Flint-Rate-Limited-ReasonWhich limit the request hit. Use it to pick the right fix.

Flint-Rate-Limited-Reason takes one of these values:

ValueWhat it meansWhat to do
api-keyThis key's own budget is exhausted.Slow this caller down, or split unrelated workloads onto their own keys.
merchantThe shared per-merchant budget is exhausted.Reduce combined throughput across all keys and workers. More keys will not help.
global-ipThe pre-authentication IP limit.Reduce total traffic from this IP, including unauthenticated requests.
setup-flowAn onboarding throttle. See Setup and Onboarding Throttles.Complete setup once and store the key instead of re-running the flow.
demo-sessionA demo sandbox limit. See Demo Sandbox Limits.Create your own sandbox and key for sustained work.
resourceA limit specific to one resource or operation.Honor Retry-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:

  1. Honor Retry-After when it is present. It is the server telling you the exact wait; sleeping longer wastes time and sleeping less guarantees another 429.
  2. Fall back to exponential backoff when it is absent, and add jitter so parallel workers do not retry in lockstep.
  3. Cap total attempts and surface the failure to your own monitoring instead of retrying forever.
  4. Send an Idempotency-Key on writes so a retry can never create a duplicate order, refund, or charge. Replays return the original result and are marked with an Idempotency-Replayed: true response header.
JavaScript
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 actually 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:

EndpointPer IPPer email
POST /v1/onboarding/start3/sec, 10/min3 per 30 sec
POST /v1/onboarding/verify-email6/sec, 30/min6 per 10 sec
POST /v1/onboarding/api-key2/sec, 12/minnone

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 classPer API keyPer merchant
read10/sec, 120/min20/sec, 240/min
write2/sec, 30/min4/sec, 60/min
sensitive_write1/sec, 10/min2/sec, 20/min
external_provider_action1/sec, 10/min2/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#

The defaults absorb typical production traffic with substantial headroom, and most integrations that hit them are fixed by the patterns above rather than by a bigger number. If you have a legitimate workload that genuinely needs more, such as a migration backfill, a flash sale, or a high-volume platform, reach out before you launch it.

Contact support@withflintpay.com 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. Honor Retry-After and 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-Reason says merchant, 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 actually 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.
Rate this doc