Rate Limits

Repull applies three request limits at once. They protect different things and they clear at different times, so the first job when you see a 429 is to work out which one you hit — the answer changes what you should do next.

The three limits

LimitWindowProtectsError
Per-key burst60 seconds, per API keyOur service, from a traffic spikerate_limit_exceeded / per_api_key
Daily requestsUTC calendar dayYou, from your own runaway loopdaily_limit_exceeded
Monthly requestsCalendar monthYour plan allowancerate_limit_exceeded / monthly
Daily AI requestsUTC calendar day, AI operations onlyYour plan's AI allowancerate_limit_exceeded / daily_ai

All four can fire on the same request. Whichever you hit first is the one you are told about.

Limits by plan

PlanPer-key burstDaily requestsMonthly requestsDaily AI
Free600 / min2,0001,00010
Starter600 / min25,000100,0001,000
Custom600 / minUnlimitedUnlimitedUnlimited

On the free plan the monthly allowance is the smaller number, so it is the one you will meet first; the daily cap is a backstop. Any of these can be raised for an individual account — ask support. Unverified accounts are held to 100 requests/minute on sk_test_ keys until the email on the account is confirmed.

Why there is a daily cap

A per-minute limit and a monthly quota between them leave a real gap, and it is the gap most integration bugs fall into.

A loop is not a burst. A client that retries forever without backoff settles into a steady rate that the per-minute limiter is perfectly happy with — the limiter paces the loop rather than stopping it. The monthly quota does eventually stop it, but only after hours, and it then holds your integration down until the next billing month.

The daily cap is the middle. It notices within the hour, and it forgives at the next UTC midnight. It is sized so that no legitimate day of traffic we have ever served would have been refused — if it fires, something is almost certainly looping.

Response headers

Every response carries both windows, so you can slow down before anything breaks:

HeaderDescription
X-RateLimit-LimitRequests allowed in the per-key burst window
X-RateLimit-RemainingRequests left in the current burst window
X-RateLimit-ResetWhen the burst window rolls over
X-RateLimit-Limit-DailyYour daily request cap (absent when unlimited)
X-RateLimit-Remaining-DailyRequests left today — the early-warning signal
X-RateLimit-Reset-DailyISO timestamp of the next UTC midnight
Retry-AfterSeconds to wait (429 responses only)

Telling the 429s apart

Branch on error.code first, then error.scope. The three cases want three different responses from your code:

You seeIt meansDo
rate_limit_exceeded
scope: per_api_key
You are going too fast right now.Back off and retry. Retry-After is seconds, usually small.
daily_limit_exceededYou are probably looping.Stop. Read top_operation, fix the call site, then resume. Retrying will not help.
rate_limit_exceeded
scope: monthly
You are out of plan requests for the month.Upgrade, or wait for the new billing month.
rate_limit_exceeded
scope: daily_ai
Out of AI calls for today only.Non-AI endpoints still work. Resume tomorrow, or upgrade.

Full recovery guides: daily_limit_exceeded and rate_limited.

Seeing your usage

GET /v1/usage/tier is the quota meter — cheap enough to poll, and it reports every window with its own reset time.

curl https://api.repull.dev/v1/usage/tier \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

{
  "tier": "starter",
  "limits": {
    "dailyRequests": 25000,
    "monthlyRequests": 100000,
    "dailyAiRequests": 1000,
    "dynamicPricingListings": 5
  },
  "used":      { "daily": 18000, "monthly": 40000, "dailyAi": 3 },
  "remaining": { "daily": 7000,  "monthly": 60000, "dailyAi": 997 },
  "dailyResetsAt": "2026-05-02T00:00:00.000Z",
  "resetsAt":      "2026-06-01T00:00:00.000Z"
}

GET /v1/usage/summaryadds a per-operation breakdown and a daily timeline. It is the fastest way to answer "where did my day go?" — sort the breakdown by request count and a runaway loop is usually the first row by a wide margin.

curl "https://api.repull.dev/v1/usage/summary?range=7d" \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

Getting warned before it bites

At 80% of your daily cap Repull emits a usage.quota.warning webhook — once per day, never once per request. It carries the same topOperation diagnosis the 429 would, so you can catch a loop while everything is still working. Subscribe to it and you should never meet the breaker itself.

{
  "type": "usage.quota.warning",
  "data": {
    "scope": "daily_requests",
    "tier": "starter",
    "used": 20000,
    "limit": 25000,
    "percentUsed": 80,
    "remaining": 5000,
    "resetsAt": "2026-05-02T00:00:00.000Z",
    "topOperation": {
      "operationId": "replay_webhook_delivery",
      "requestCount": 17000,
      "sharePercent": 85
    }
  }
}

Handling 429s in code

Back off on the burst limit. Do not back off on the daily cap — treat it as a bug report about your own code and stop:

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429) return res;

    const body = await res.clone().json();

    // The daily circuit breaker. Retrying re-runs the loop that caused it.
    if (body.error?.code === 'daily_limit_exceeded') {
      const top = body.error.top_operation;
      throw new Error(
        `Daily request cap hit (${body.error.used}/${body.error.limit}).` +
        (top ? ` Check ${top.operation_id} — ${top.share_percent}% of today's traffic.` : '')
      );
    }

    // Monthly quota: waiting will not help either; this needs a plan change.
    if (body.error?.scope === 'monthly') {
      throw new Error('Monthly request quota exhausted — upgrade the plan.');
    }

    // Burst limit: this is the one worth waiting out.
    const retryAfter = parseInt(res.headers.get('Retry-After') || '1', 10);
    const backoff = retryAfter * Math.pow(2, attempt) + Math.random();
    await new Promise(r => setTimeout(r, backoff * 1000));
  }
  throw new Error('Max retries exceeded');
}

Raising a limit

Every limit on this page can be set per account, independently of the plan — including the daily cap on its own, without changing your monthly quota. If your traffic is genuine and growing, contact support with your workspace id and a rough shape of the load. Before you do, check /v1/usage/summary: it is worth being sure the volume is work you meant to do.

AI