> ## Documentation Index
> Fetch the complete documentation index at: https://linkskipper.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limits & quotas

> Per-minute limits, optional daily quotas, the Retry-After header, and backoff guidance.

API keys are subject to a rolling per-minute request limit and, optionally, a daily quota.
Both protect the resolve queue and keep the service responsive. Limits are configured per
key in the [dashboard](https://linkskipper.app/developers/keys).

## Per-minute rate limit

Each key allows a fixed number of requests per rolling 60-second window — **60 per minute by
default**, configurable per key. Exceeding it returns:

```json theme={null}
{
  "type": "https://linkskipper.app/developers/docs#rate_limited",
  "title": "Rate limited",
  "status": 429,
  "code": "rate_limited",
  "detail": "Too many requests. Retry after the cooldown."
}
```

The response carries a `Retry-After` header (in seconds) telling you when to try again:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json
```

## Daily quota

A key can optionally have a daily quota — a cap on resolve calls per UTC day. It is **off by
default** (no cap). When set and reached, requests return `quota_exceeded`:

```json theme={null}
{
  "type": "https://linkskipper.app/developers/docs#quota_exceeded",
  "title": "Daily quota exceeded",
  "status": 429,
  "code": "quota_exceeded",
  "detail": "The daily resolve quota for this key has been reached."
}
```

The quota window **resets at UTC 00:00**. Like the per-minute limit, the response includes
`Retry-After`.

<Note>
  Both `rate_limited` and `quota_exceeded` use HTTP **429** and send **`Retry-After`**.
  Branch on the `code` field to tell them apart: a per-minute limit clears in a minute; a
  daily quota only clears at the next UTC midnight.
</Note>

## Retry-After

`Retry-After` is the number of **seconds** to wait before retrying. It currently defaults to
`60` for both `429` codes. Always read it from the response rather than hard-coding a value —
respect whatever the header says.

<CodeGroup>
  ```js JavaScript theme={null}
  const res = await fetch("https://api.linkskipper.app/v1/resolve", {
    method: "POST",
    headers: {
      Authorization: "Bearer sk_live_XXXXXXXXXXXXXXXXXXXXXXXX",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ url: "https://ouo.io/abc123" }),
  });

  if (res.status === 429) {
    const retryAfter = Number(res.headers.get("retry-after") ?? 60);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    // retry...
  }
  ```

  ```php PHP theme={null}
  <?php

  // $headers is the parsed response header map, $status the HTTP code.
  if ($status === 429) {
      $retryAfter = (int) ($headers["retry-after"] ?? 60);
      sleep($retryAfter);
      // retry...
  }
  ```
</CodeGroup>

## Backoff guidance

<Steps>
  <Step title="Respect Retry-After first">
    On a `429`, wait at least `Retry-After` seconds before the next attempt. Never retry a
    `429` immediately.
  </Step>

  <Step title="Exponential backoff for 5xx">
    For `provider_down` (503) and transient `resolve_failed` (502), back off exponentially:
    e.g. 0.5s, 1s, 2s, 4s, with jitter, capped at a few seconds.
  </Step>

  <Step title="Cap attempts">
    Give up after a small number of attempts (3–5) and surface the error rather than looping
    forever.
  </Step>

  <Step title="Don't retry 4xx">
    `invalid_request`, `invalid_key`, `forbidden_scope`, `not_found`, `link_removed`, and
    `unsupported_link` won't change on retry. Fix the request instead.
  </Step>
</Steps>

## Polling and limits

Polling [`GET /v1/jobs/{id}`](/docs/jobs) counts toward your per-minute limit like any request.
When waiting on a queued job:

* Poll on a **1.5–2s** interval, not tighter than \~1s.
* For high volume, use [webhooks](/docs/webhooks) so you don't poll at all.
* Reduce request count by batching unrelated work rather than spinning many tight polls.

<Info>
  The official SDKs handle this for you: they automatically retry `429` and `5xx` responses,
  honor `Retry-After`, and apply exponential backoff with jitter. See
  [JavaScript](/docs/sdks/javascript) and [PHP](/docs/sdks/php).
</Info>
