Lynkist Developers

Rate Limits

Per-plan quotas, response headers, 429 handling, and recommended client behaviour.

The Lynkist Public API enforces per-tenant rate limits on a fixed-window counter, with two windows running at once: a per-minute budget and a per-day budget. Hitting either returns 429 Too Many Requests.

How the budgets are set

Two windows run at once:

  • Per-minute: a flat anti-abuse burst limit of 120 requests/minute, the same for every tenant with API access. Plans do not define a custom per-minute budget.
  • Per-day: comes from your plan's api_requests_daily entitlement. A value of -1 means effectively unlimited.

Tenants whose plan does not include API access cannot call the Public API at all — every request returns 403 with error.code: UPGRADE_REQUIRED and feature_key: "access_api". Upgrade to a plan that includes API access to enable it.

Budgets are resolved live from your plan's billing entitlements (access_api + api_requests_daily). Redis does the per-request counting; billing supplies the daily ceiling.

Response headers

Every response — 2xx or 4xx — carries the same five headers, so you can pace yourself without polling a separate endpoint:

HeaderValue
X-RateLimit-Limit-MinuteYour per-minute budget for this window
X-RateLimit-Remaining-MinuteRequests left in this minute
X-RateLimit-Limit-DayYour per-day budget for this window
X-RateLimit-Remaining-DayRequests left in this day
X-RateLimit-ResetUnix epoch seconds when the tighter window resets

Example:

HTTP/1.1 200 OK
X-RateLimit-Limit-Minute: 120
X-RateLimit-Remaining-Minute: 83
X-RateLimit-Limit-Day: 5000
X-RateLimit-Remaining-Day: 4871
X-RateLimit-Reset: 1748678400

The reset value reflects the window that will trip first (usually the minute window).

The 429 response

HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit-Minute: 120
X-RateLimit-Remaining-Minute: 0
X-RateLimit-Limit-Day: 5000
X-RateLimit-Remaining-Day: 132
X-RateLimit-Reset: 1748678400

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Please slow down.",
    "window": "minute",
    "retry_after": 17
  },
  "meta": { "request_id": "req_…", "timestamp": "…" }
}

Retry-After is seconds until the budget refreshes. Honouring it is the simplest correct client behaviour.

Window semantics

  • Fixed window, per tenant. Both windows reset at the boundary (minute or UTC day), not on a rolling basis. A burst of 60 requests at 12:00:59 followed by 60 at 12:01:01 is fine even though it's 120 requests in two seconds.
  • The first refused request returns 429. Counters are increment-then-check: by the time you see Remaining: 0, the next request will 429. The current one still goes through.
  • Requests that we answer count — also the ones that fail with a 4xx. A validation error (400) and a 404 use budget. Authentication failures (401) and scope or plan denials (403) are refused before the counter and use none.
  • Our failures do not use your daily quota. A request that we answer with 5xx, and a request that we refuse with 429, is given back to the daily budget. The per-minute budget still counts them, so a retry loop is still slowed down.

Reactive: honour Retry-After

Simplest correct pattern. On 429 or 5xx, wait, then retry: use Retry-After when the response has it, else exponential backoff with jitter. Stop after a fixed number of attempts. Send an Idempotency-Key so a retried write does not happen twice.

import random, time, httpx

def call(method, url, *, attempts=6, **kwargs):
    for attempt in range(attempts):
        r = httpx.request(method, url, **kwargs)
        if r.status_code != 429 and r.status_code < 500:
            return r                       # success, or a 4xx that a retry cannot fix
        if r.status_code == 504:
            return r                       # outcome unknown: do not resend automatically
        wait = r.headers.get("Retry-After")
        delay = int(wait) if wait else min(60, 2 ** attempt) + random.random()
        time.sleep(delay)
    return r

Never retry in a loop with no wait. During an incident, clients that retry at once make the incident longer for everyone, themselves included.

Proactive: watch the headers

Slightly more elegant. Pace yourself off X-RateLimit-Remaining-Minute so you never hit 429 in the first place — useful inside batch jobs.

import time, httpx

def paced_call(method, url, **kwargs):
    r = httpx.request(method, url, **kwargs)
    if int(r.headers.get("X-RateLimit-Remaining-Minute", "1")) <= 1:
        time.sleep(max(0, int(r.headers["X-RateLimit-Reset"]) - int(time.time())))
    return r

Bulk-writes: prefer /contacts/bulk

Importing a CSV of contacts? POST /contacts/bulk carries up to 100 contacts per request — that's 100x the throughput of a per-contact loop with the same rate-limit cost.

Graceful degradation

If Redis (the rate-limit backend) is unavailable, the API degrades open — requests are allowed through without limit-checking, and the response headers are simply omitted. So if you ever see a response with no X-RateLimit-* headers, that is what happened — not a sign that limits have been removed.

What's not rate-limited

  • Webhook deliveries to your endpoints. Outgoing webhooks have no per-tenant rate limit — they're paced by the retry curve instead.
  • Read-after-write on idempotent retries. If you replay a request with the same Idempotency-Key within 24 hours, the response is replayed from cache without consuming a fresh slot.

Asking for a higher limit

On the Business plan we can provision a custom daily quota above the standard 10,000. Open a support ticket with a recent X-Request-Id and the burst pattern you need to handle — concrete numbers (requests / minute, duration of burst, frequency) help us give you a useful answer faster than "we need more."

Rate Limits | Lynkist Developers