Rate limits & errors
Rate limits
Section titled “Rate limits”Limits are per route, over a rolling 60-second window, and are counted per client IP address rather than per key. Several keys behind one egress address share a bucket.
| Route | Limit per 60s |
|---|---|
GET /api/v1/opportunities |
60 |
GET /api/v1/opportunities/{id} |
60 |
GET /api/v1/me |
60 |
POST /api/v1/opportunities/{id}/archive |
10 |
DELETE /api/v1/opportunities/{id}/archive |
10 |
POST /api/v1/opportunities/{id}/feedback |
10 |
GET /api/v1/api-keys |
10 |
POST /api/v1/api-keys |
10 |
DELETE /api/v1/api-keys/{id} |
10 |
POST /api/v1/session |
10 |
GET /api/v1/scopes, GET /api/v1/openapi.json, and DELETE /api/v1/session
carry no route limit.
Headers
Section titled “Headers”Throttled routes report their budget on every response:
| Header | Meaning |
|---|---|
x-ratelimit-limit |
The route’s maximum for the window. |
x-ratelimit-remaining |
Requests left in the current window. |
x-ratelimit-reset |
Seconds until the window resets. |
retry-after |
Seconds to wait. Sent on a 429 only. |
When you exceed a limit
Section titled “When you exceed a limit”The request is rejected with 429 and this body:
{ "error": "rate_limited", "message": "too many requests", "requestId": "req-b"}Honour retry-after when it is present, and fall back to
exponential backoff with jitter when it
is not. Do not retry immediately in a tight loop — the window is rolling, so a
burst of retries keeps the bucket empty and extends the lockout.
Error responses
Section titled “Error responses”Every error from the API — validation, auth, rate limiting, and internal failures alike — serializes to one shape:
{ "error": "insufficient_scope", "message": "this key lacks the opportunities:read scope", "requestId": "req-4f2"}| Field | Type | Notes |
|---|---|---|
error |
string | A stable code from the table below. Branch on this, never on message. |
message |
string | Human-readable detail. Wording may change; treat it as diagnostic text. |
requestId |
string | Identifies this specific request. |
issues |
array | Optional. Present on some validation failures, listing the specific problems. |
Error codes
Section titled “Error codes”| Code | Typical status | Meaning |
|---|---|---|
unauthorized |
401 | No credential presented, or the key is unknown, revoked, or expired. |
forbidden |
403 | The credential is valid but not permitted to perform this action. |
insufficient_scope |
403 | The key is missing a scope the route requires. |
no_membership |
403 | The account is not linked to a company. |
membership_disabled |
403 | The membership behind this credential has been disabled. |
unverified_email |
403 | The signed-in account has not confirmed its email address. |
identity_conflict |
403 | The membership is bound to a different identity. |
validation_failed |
400 | The request was malformed — a bad parameter, body, or cursor. |
not_found |
404 | No such resource for this company. |
rate_limited |
429 | The route’s limit for the window was exceeded. |
internal |
500 | Something failed on our side. Safe to retry with backoff. |
unauthorized, insufficient_scope, validation_failed, and not_found are
the four an API-key integration will realistically encounter. The
membership-related codes surface on browser sessions, not on key auth.
What is worth retrying
Section titled “What is worth retrying”429and5xx— transient. Retry with backoff.401and403— the credential itself is wrong. Retrying will not fix it; check the key and its scopes.400and404— the request is wrong. Retrying sends the same wrong request.