Usage limits

Two layers protect the API: a per-source-IP guard in front of every request, and a per-organization, per-endpoint quota. Both expose their state in standard headers: read them and your client paces itself.

6 min read

The two-layer model

LayerScopeLimitCode when exceeded
Per-IP guardPer source IP (IPv6: per /64 block)35,000 requests per minuteRATE_DDOS_EXCEEDED
Per-endpoint quotaPer organization and per endpointSpecific to each endpoint (ping allows 20 per minute)RATE_TPS_EXCEEDED

The per-IP guard runs first: it is a safety floor, not a commercial commitment. Hitting it usually means a misconfigured shared IP or a clear abuse pattern; the threshold is generous and legitimate traffic does not get close. The per-endpoint quota is the one you will see in normal operation, and each endpoint states its own in the reference.

The two layers count differently: the IP layer adds up every request coming from the same IP, no matter the endpoint; the quota layer adds up requests to a single endpoint. Rotating endpoints does not multiply the IP budget, and rotating IPs does not multiply the endpoint quota.

IPv6: the /64 block is counted

With IPv6, the per-IP guard does not count the exact address but its /64 block. Every IPv6 connection is assigned a whole /64, so whoever uses it can pick a fresh address on every request without changing networks: counting the exact address would set no limit at all. Every address in the same /64 shares the 35,000 per minute. IPv4 addresses are counted one by one, as always.

The cut-off for API keys that do not exist

The per-IP guard has a second rule, for whoever is guessing keys. If 60 different API keys that do not authenticate arrive within one minute from the same IP (or the same /64) —keys that do not exist, or were revoked or expired—, that IP is cut off for 15 minutes: every request gets 429 RATE_DDOS_EXCEEDED with a Retry-After saying how much of the cut-off is left, even if the key you send is valid.

  • It counts distinct keys, not attempts: an integration that keeps sending the same revoked key gets 401 AUTH_INVALID_KEY every time, but never reaches the cut-off.
  • Only keys that do not authenticate count. A valid key without permission for the service, from a suspended organization or used from a network it does not allow does not add up: it exists, and it cannot be guessed at random.
  • If your integration spreads traffic across several keys, check that none is miscopied or revoked: behind the same internet egress, they all count together.

Sliding window, not fixed buckets

Both layers use a two-bucket weighted sliding window, the same technique Cloudflare and Stripe use. The intuition:

  • The current 60-second bucket counts at full weight.
  • The previous bucket counts with a weight that decays from 1.0 (start of the current bucket) to 0.0 (end of it).
  • The effective total is current + previous × weight.

With fixed buckets, a client could send the full limit at second 59 and the same again at second 0 of the next bucket: double the limit in two seconds. The sliding window catches that.

Response headers

The API follows RFC 9598 (RateLimit Header Fields for HTTP). Every quota-protected response carries:

HTTP
RateLimit-Limit:     20
RateLimit-Remaining: 18
RateLimit-Reset:     31
RateLimit-Policy:    20;w=60;name="endpoint"
RateLimit-Limit
Quota for the active window.
RateLimit-Remaining
Requests remaining in the active window.
RateLimit-Reset
Seconds until the active window resets.
RateLimit-Policy
Declarative quota in a parseable format. 20;w=60 means 20 requests in a 60-second sliding window.

On a 429 you also get Retry-After in seconds: wait at least that long before retrying.

Recommended strategy

  1. 01
    Throttle preemptively

    Read RateLimit-Remaining on every response. When it drops below 20 % of RateLimit-Limit, slow down so you reach the next reset with room to spare.

  2. 02
    Respect Retry-After

    When you get a 429, wait at least the seconds it reports before the next attempt. Retrying immediately just re-triggers the counter.

  3. 03
    Exponential backoff with jitter

    If retries keep failing, add an exponential delay (for example 2^n × 100 ms) with ±50 % random jitter. The jitter keeps thousands of clients from syncing on the same second.

Reference implementation
import time, random

def call_with_backoff(client, request, max_retries=5):
    for attempt in range(max_retries):
        r = client.send(request)
        if r.status_code != 429:
            return r
        wait = int(r.headers.get("Retry-After", "1"))
        # retroceso exponencial con techo en Retry-After * 4, mas variacion
        delay = min(wait * (2 ** attempt), wait * 4)
        delay += random.uniform(0, delay * 0.5)
        time.sleep(delay)
    return r  # quien llama decide que hacer al agotar los intentos

If you write your own client, parsing the policy is straightforward:

JavaScript
// RateLimit-Policy: 20;w=60;name="endpoint"
// -> limite 20, ventana 60 s, nombre "endpoint"
const [limit, ...params] = policy.split(';')
const window = Number(params.find((p) => p.startsWith('w='))?.slice(2))

What you see when it fires

Both layers return HTTP 429 with the standard envelope. The error.code tells you which one fired:

429 Too Many Requests
RateLimit-Limit:     20
RateLimit-Remaining: 0
RateLimit-Reset:     42
Retry-After:         42

{
  "success": false,
  "error": {
    "code": "RATE_TPS_EXCEEDED",
    "message": "You have exceeded the allowed request rate for this endpoint."
  }
}

Asking for a higher limit

If your traffic pattern legitimately exceeds an endpoint quota (bulk imports, campaigns, batch onboarding), talk to your account manager. We can raise the limit for your organization on a specific endpoint without touching the per-IP safety floor. Have this ready:

  • Expected peak and total daily volume.
  • Whether the peak is one-off (a launch) or sustained.
  • How you back off when limited, so we know the increase does not just move the problem.