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." },
  "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.
  • Most requests count — including ones that 4xx out. Validation failures (400) and 404s consume budget. The exceptions: authentication failures (401) and scope/plan denials (403) are rejected before the rate-limit counter increments, so they don't consume budget. Still, be careful with retry loops on bad payloads.

Reactive: honour Retry-After

Simplest correct pattern. On 429, sleep Retry-After seconds, then retry. Couple this with an Idempotency-Key so retried mutations don't double-write.

import time, httpx

def call(method, url, **kwargs):
    while True:
        r = httpx.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        time.sleep(int(r.headers.get("Retry-After", "1")))

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

For enterprise customers we provision custom quotas. 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 | Lynkist