Skip to main content
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.

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:
{
  "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/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:
{
  "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.
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.

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.
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...
}

Backoff guidance

1

Respect Retry-After first

On a 429, wait at least Retry-After seconds before the next attempt. Never retry a 429 immediately.
2

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.
3

Cap attempts

Give up after a small number of attempts (3–5) and surface the error rather than looping forever.
4

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.

Polling and limits

Polling GET /v1/jobs/{id} 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 so you don’t poll at all.
  • Reduce request count by batching unrelated work rather than spinning many tight polls.
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 and PHP.