Rate limits and errors
Quota defaults, rate-limit headers, and the error envelope shapes you'll encounter
Every /v1/ request is metered against your organization's quota. Whether a
response carries rate-limit headers depends on whether the request reached
the quota check at all — most do, but a request that fails before that
point doesn't (see Rate-limit headers below for the
cases). This guide covers the defaults, the headers, what a 429 looks
like, and the error shapes used across the API.
Quota defaults
These are the platform defaults applied to every organization. Your organization's limits may differ — contact Pulse if you need higher limits.
| Limit | Default |
|---|---|
| Queries per minute | 60 |
| Queries per day | 10,000 |
| Rows returned per day | 500,000 |
An org's effective quota is the default merged with any per-org override on
file — any override field that's set wins over the matching default. A
quota can also be blocked outright (a hard kill switch, independent of
whether you have headroom left) — a blocked org's keys get a 429 on every
call until access is restored.
Rate-limit headers
Rate-limit headers appear on any response where your request reached the
quota check — successful calls, filter/column validation errors, and
ordinary rate-limit 429s all include them.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1735689660X-RateLimit-Limit— your queries-per-minute limit for the current window.X-RateLimit-Remaining— how many requests you have left in the current minute window.X-RateLimit-Reset— Unix timestamp (seconds) when the current minute window resets.
These headers reflect the minute window specifically — the day-level
query and row quotas aren't exposed as headers, only as the 429 you'll get
once you exceed them (see below).
A request that fails before that point carries no rate-limit headers. This list isn't exhaustive — anything that short-circuits ahead of the quota check falls in the same bucket — but the cases you're most likely to run into are:
- A
401— authentication runs first, so a bad or missing key never reaches the quota check. - A
400from a malformed JSON body — the request body is parsed before the quota check runs, so a body that fails to parse is rejected during parsing, ahead of it. - The blocked-organization
429(mcp_access_blocked, below) — the access-blocked check runs before the rate counters are consulted, so this particular429also has no rate-limit state behind it, unlike every other429.
When you're rate limited
HTTP 429
Once you exceed a quota, the API returns 429 with a body identifying which
limit you hit:
{
"error": {
"code": "mcp_rate_minute",
"message": "MCP request rate exceeded (60/minute)."
}
}The code tells you which specific limit was exceeded:
| Code | Meaning |
|---|---|
mcp_rate_minute | Per-minute query limit exceeded. |
mcp_rate_day | Per-day query limit exceeded. |
mcp_daily_rows | Per-day row quota exceeded (you've read too much data today, even if individual calls stayed under the query limits). |
mcp_access_blocked | Your organization's API access is blocked outright — contact Pulse. |
The code values above carry the mcp_ prefix from the shared internal
throttle these limits are implemented on top of — the same mechanism used by
Pulse's chat and MCP surfaces. It's not public-API-specific naming, but it's
exactly what you'll see on the wire, so match against these strings exactly
rather than assuming a public_api_-style prefix.
Retry guidance
- Always read the rate-limit headers rather than hardcoding your own assumption of the limit — your organization's effective quota can differ from the defaults above.
- On a
429, back off before retrying. A minute-window limit (mcp_rate_minute) clears atX-RateLimit-Reset; a daily limit (mcp_rate_day,mcp_daily_rows) doesn't clear until the next UTC day. - Don't tight-loop retries on
429— you'll keep hitting the same limit until its window resets.
The error envelope
Most error responses share one shape:
{
"error": {
"code": "...",
"message": "..."
}
}Some errors carry additional fields alongside code and message — for
example, a rejected filter field also returns which field and resource
triggered it. Treat code and message as the guaranteed fields and
anything else as extra context.
Two exceptions use DRF's stock error shape instead of the envelope above:
{ "detail": "..." }- Every
401(missing header, malformed key, unknown key, revoked key) — see Authentication for why presented-key failures are indistinguishable by design. - The
400for a request body that isn't valid JSON — the body is rejected during parsing, before the API's own error handling is reached, so it comes back as{ "detail": "JSON parse error ..." }.
Every other 400 and every 429 uses the nested {"error": {...}}
envelope.
There are no resource-level 404s on this API: a well-formed request to a
documented path never 404s. An unknown company or asset id, an unsupported
resource, or an unrecognized options endpoint all come back as a 400
invalid_argument rather than a 404 — "not found" is treated as a bad
argument, not a missing route. A wrong or slash-less path is a different
story and does 404 — see the trailing-slash note under
Endpoints.
Common error codes
These are the codes you can actually trigger by calling the API correctly but with bad input — not an exhaustive list of every internal error type.
| Code | HTTP status | When you'll see it |
|---|---|---|
invalid_argument | 400 | General bad input — an unknown company/asset id, an unsupported resource on /v1/filters/{resource}/, an unrecognized options_path, a non-integer limit, a bad logic value, or similar. |
unsupported_filter_field | 400 | A search filters entry referenced a field outside the resource's curated vocabulary (see Filters). |
unsupported_column | 400 | A search columns entry referenced a field outside the resource's curated vocabulary. |
unsupported_sort_field | 400 | A search sort value isn't a sortable key in the resource's curated vocabulary (or isn't a string). See Pagination and enumeration. |
query_timeout | 408 | The query took too long to execute — reachable on the full-text search endpoints (POST /v1/people/search/, POST /v1/publications/search/) under a broad enough query. Narrow your search term (or add a company/date filter on publications) and retry. |
Example 400 for a filter field outside the curated vocabulary:
{
"error": {
"code": "unsupported_filter_field",
"message": "Unknown or unsupported filter field for the public API: 'internal_score'.",
"field": "internal_score",
"resource": "companies"
}
}Go deeper
- Filters covers the curated filter vocabulary that
drives most
400s on the search endpoints. - The API Reference documents the
exact
400/401/429responses for each endpoint.