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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix epoch (seconds) when the window resets |
X-RateLimit-Policy | The 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.
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;
}
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:
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:
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?
- Rate limits & quotas — per-tier numbers and how policies match
- Webhook subscriptions — stop polling, get pushed