Each account can send a fixed number of requests per time window. Responses tell you where you stand, so you can slow down before you hit the limit. If you do hit it, you get 429 rate_limited and a Retry-After header that says how long to wait.
Find your limit
Section titled “Find your limit”GET /v1/account returns your limit as rate_limit. It is free.
{ "plan": "growth", "credits_remaining": 9841, "credits_reset_at": "2026-11-01T00:00:00Z", "providers_enabled": ["betterjobs", "theirstack", "techmap"], "rate_limit": { "limit": 60, "window_seconds": 60 }}Illustrative. limit is requests per window and window_seconds is the window length. Read your own values from the endpoint rather than hard-coding them.
Headers
Section titled “Headers”| Header | On | What it tells you |
|---|---|---|
RateLimit-Limit |
Responses | Requests allowed in the current window. |
RateLimit-Remaining |
Responses | Requests left in the current window. |
RateLimit-Reset |
Responses | Seconds until the window resets. |
Retry-After |
429 only |
Seconds to wait before you retry. |
The names follow the IETF draft RateLimit header fields, without an X- prefix. RateLimit-Reset is a number of seconds from now, not a Unix timestamp.
A 429 body uses the standard error shape:
{ "error": { "type": "rate_limit_error", "code": "rate_limited", "message": "Rate limit exceeded. Retry after 12 seconds.", "doc_url": "https://docs.betterjobs.cc/platform/errors/#rate_limited", "request_id": "req_0Ig1JqH5sT" }}A 429 returns no jobs, so it charges nothing.
Retry with backoff
Section titled “Retry with backoff”Wrap every call in one helper:
- On
429, wait exactlyRetry-Afterseconds. - On
5xxor a network error, wait with exponential backoff plus jitter, capped at 30 seconds. - Stop after a few attempts and raise.
- For
POST, pass the sameIdempotency-Keyon every attempt, so a retry can never double up. See Idempotency.
# curl waits for Retry-After on 429 and backs off on 5xx.curl https://api.betterjobs.cc/v1/jobs/search \ --retry 5 --retry-max-time 120 \ -H "Authorization: Bearer $BETTERJOBS_API_KEY" \ -H "BetterJobs-Version: 2026-10-01" \ -H "Idempotency-Key: 7b8f2c4e-1a3d-4f5b-9c6e-0d2a4b6c8e1f" \ -H "Content-Type: application/json" \ -d '{ "filters": { "title_or": ["Head of RevOps"], "country_code_or": ["DE", "AT", "CH"] } }'import randomimport time
import requests
MAX_ATTEMPTS = 5
def send(method: str, url: str, **kwargs) -> requests.Response: """Send a request, retrying 429, 5xx and network errors. Reuses kwargs (and Idempotency-Key) on every attempt.""" for attempt in range(MAX_ATTEMPTS): last = attempt == MAX_ATTEMPTS - 1 try: resp = requests.request(method, url, timeout=60, **kwargs) except (requests.ConnectionError, requests.Timeout): if last: raise time.sleep(min(2**attempt, 30) + random.random()) continue if resp.status_code == 429 and not last: time.sleep(int(resp.headers["Retry-After"])) elif resp.status_code >= 500 and not last: time.sleep(min(2**attempt, 30) + random.random()) else: resp.raise_for_status() return resp raise AssertionError("unreachable")const MAX_ATTEMPTS = 5;const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));const backoffMs = (attempt: number) => (Math.min(2 ** attempt, 30) + Math.random()) * 1000;
/** Send a request, retrying 429, 5xx and network errors. Reuses init (and Idempotency-Key) on every attempt. */export async function send(url: string, init: RequestInit): Promise<Response> { for (let attempt = 0; ; attempt++) { const last = attempt === MAX_ATTEMPTS - 1; let res: Response; try { res = await fetch(url, init); } catch (err) { if (last) throw err; await sleep(backoffMs(attempt)); continue; } if (res.status === 429 && !last) { await sleep(Number(res.headers.get("Retry-After")) * 1000); } else if (res.status >= 500 && !last) { await sleep(backoffMs(attempt)); } else { if (!res.ok) throw new Error(`BetterJobs ${res.status}: ${await res.text()}`); return res; } }}Stay under the limit
Section titled “Stay under the limit”Backoff handles the occasional 429. For steady workloads, avoid them:
- Pace on
RateLimit-Remaining. When it reaches0, waitRateLimit-Resetseconds before the next call. - Ask for bigger pages.
limit: 100onPOST /v1/jobs/searchneeds a quarter of the requests that the default25does. - Use async searches for bulk work. One
POST /v1/searchescollects up to 10,000 jobs. See Async searches. - Prefer webhooks to polling. A watch or
webhook_urlpushes events to you. PollingGET /v1/searches/{id}orGET /v1/eventsin a tight loop spends requests on “nothing yet”. - Share one limiter. Parallel workers on one account share one budget. Put one rate limiter in front of all of them.