PPactDocs
Developers

Rate limits and pagination

Read the rate-limit headers, back off correctly, and page through large result sets.

This guide covers the two things every integration has to get right: staying under your rate limit, and paging through more records than fit in one response.

Rate-limit headers

Every authenticated response carries your current budget:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window
X-RateLimit-RemainingRequests left in the current window
X-RateLimit-ResetUnix epoch (seconds) when the window resets
X-RateLimit-PolicyThe matched policy pattern (e.g. /v1/email/send), if any

When you exceed the limit you get a 429 Too Many Requests with a Retry-After header (seconds to wait). Limits apply per key and per workspace — see rate limits & quotas for the per-tier numbers.

Back off correctly

Honor Retry-After on a 429, and use exponential backoff with jitter for transient 5xx errors. Never retry a 4xx other than 429 — fix the request instead.

ts
async function pactFetch(url: string, init: RequestInit, attempt = 0): Promise<Response> {
  const res = await fetch(url, init);
  if (res.status === 429 || res.status >= 500) {
    if (attempt >= 5) return res;
    const retryAfter = Number(res.headers.get("Retry-After"));
    const backoff =
      Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : Math.min(30_000, 2 ** attempt * 500) + Math.random() * 250; // jitter
    await new Promise((r) => setTimeout(r, backoff));
    return pactFetch(url, init, attempt + 1);
  }
  return res;
}
python
import random, time, requests

def pact_fetch(method, url, **kw):
    for attempt in range(6):
        res = requests.request(method, url, timeout=10, **kw)
        if res.status_code == 429 or res.status_code >= 500:
            retry_after = res.headers.get("Retry-After")
            backoff = (
                float(retry_after)
                if retry_after and retry_after.isdigit()
                else min(30, 2 ** attempt * 0.5) + random.random() * 0.25
            )
            time.sleep(backoff)
            continue
        return res
    return res

Pagination: limit + offset

Most list endpoints (/v1/companies, /v1/contacts, /v1/opportunities, …) page with limit and offset. Read total from the response envelope to know when to stop:

python
def all_companies(api_key, page_size=100):
    offset, out = 0, []
    while True:
        res = pact_fetch(
            "GET",
            "https://app.pact.place/v1/companies",
            params={"limit": page_size, "offset": offset},
            headers={"Authorization": f"Bearer {api_key}"},
        )
        res.raise_for_status()
        page = res.json()
        out.extend(page["items"])
        offset += page_size
        if offset >= page["total"] or not page["items"]:
            break
    return out

Keep page sizes reasonable

A limit of 50–100 balances round-trips against payload size. Very large pages count as a single request against your rate limit but take longer and use more memory.

Pagination: cursor (high-volume streams)

A few high-volume, append-only streams — most notably the webhook delivery log (GET /v1/webhooks/deliveries) — use cursor pagination instead. Pass the next_cursor from one response as the cursor of the next, and stop when it comes back null:

bash
curl "https://app.pact.place/v1/webhooks/deliveries?limit=100" \
  -H "Authorization: Bearer $PACT_API_KEY"
# → { "data": [...], "next_cursor": "2026-05-30T12:00:00", "limit": 100 }

Cursor pagination is stable under inserts (new rows don't shift your page), which is why it's used for event streams rather than offset.

What's next?