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_dailyentitlement. A value of-1means 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:
| Header | Value |
|---|---|
X-RateLimit-Limit-Minute | Your per-minute budget for this window |
X-RateLimit-Remaining-Minute | Requests left in this minute |
X-RateLimit-Limit-Day | Your per-day budget for this window |
X-RateLimit-Remaining-Day | Requests left in this day |
X-RateLimit-Reset | Unix 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: 1748678400The 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) and404s 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.
Recommended client behaviour
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 rBulk-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-Keywithin 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."