title: Errors description: Every error code the API returns, what causes it, and what to do about it.
An error from this API is never a bare string. Every failure carries the same shape:
{
"error": {
"code": "insufficient_scope",
"message": "This key was not issued the prices:read scope.",
"docsUrl": "https://markets.pulseline.io/docs/errors#insufficient_scope"
}
}
| Field | Meaning |
|---|---|
| code | Machine-readable and stable. Branch on this, never on message |
| message | Human-readable and specific. Never leaks stack traces, SQL, provider names or key material |
| docsUrl | A deep link to this page, anchored at that exact code |
| details | Optional. Only present on invalid_request, naming the offending fields |
code is part of the public contract. Changing one is a breaking change, so it
is safe to branch on. message is written for humans and may be reworded;
do not parse it.
invalid_request [#invalid_request]
HTTP 400. The request failed schema validation.
details names the offending fields, so you rarely need to guess:
{
"error": {
"code": "invalid_request",
"message": "…",
"docsUrl": "https://markets.pulseline.io/docs/errors#invalid_request",
"details": { "from": ["Expected a yyyy-mm-dd date."] }
}
}
What to do: fix the named fields. Common causes are a date that is not
yyyy-mm-dd, a from later than its to, a malformed instrument id, or a
boolean sent as something other than true or false. This is a bug in the
calling code, so retrying unchanged will not help.
missing_api_key [#missing_api_key]
HTTP 401. No Authorization header, or one the API does not understand.
What to do: send Authorization: Bearer <your key>. Check the literal
Bearer prefix, and check your HTTP client is not stripping the header on
redirect. See Authentication.
invalid_api_key [#invalid_api_key]
HTTP 401. The presented key does not exist. Also returned for a deleted key.
What to do: verify the key was copied whole. Full keys are shown exactly once at creation, so a truncated paste is the usual cause. Check you are not sending a sandbox key to a live-only integration or the reverse. If the key is genuinely lost, issue a new one from your dashboard; it cannot be recovered, only replaced.
revoked_api_key [#revoked_api_key]
HTTP 401. The key existed but has been revoked.
This is deliberately distinct from invalid_api_key so you can tell "someone
typo'd the key" from "this key was rolled and something is still pointing at the
old one".
What to do: find the consumer still using the old key and move it to the current one. Revocation is immediate and permanent; a revoked key never starts working again.
account_inactive [#account_inactive]
HTTP 403. The key is valid, but its owning account is disabled.
What to do: this is an account-level state, not a key-level one, so issuing a new key will not clear it. Check your billing status, then contact support.
insufficient_scope [#insufficient_scope]
HTTP 403. The key is valid but was not issued the scope this route requires.
Scope matching is exact, with no wildcard and no hierarchy: a grant of prices
does not imply prices:read.
What to do: check the route's required scope in Authentication, then issue a key carrying it. Grant only what that consumer needs. Widening scopes is what makes a leaked key expensive.
insufficient_tier [#insufficient_tier]
HTTP 403. The request is well-formed and your key holds the scope, but your plan tier does not grant what the request needs: real-time data, deeper history, a broader asset class, or redistribution rights.
This is distinct from insufficient_scope (a capability your key was not issued)
and rate_limited (your allowance is used up). The fix is an upgrade, and the
message names the tier that would unlock it.
What to do: upgrade to the named tier on your billing page, or adjust the request to stay inside your plan (for example, ask for delayed rather than real-time quotes, or a shorter history window). See Pricing for what each tier grants.
rate_limited [#rate_limited]
HTTP 429. The account's quota for the current window is exhausted.
The response carries X-RateLimit-Limit, X-RateLimit-Remaining,
X-RateLimit-Reset and Retry-After.
What to do: wait for Retry-After, then retry with exponential backoff and
jitter. Retrying sooner cannot succeed. To stop hitting it: use
/v1/prices/bulk instead of per-instrument loops, widen date ranges rather than
making more requests, cache settled history, and develop against sandbox, whose
allowance is separate from your paid one (it has its own limits, so this moves
the ceiling rather than removing it). See
Rate limits and quotas.
sandbox_restricted [#sandbox_restricted]
HTTP 403. The request is well-formed and your key holds the required scope, but it reaches further back than a sandbox key may read. The same request on a live key would succeed.
Sandbox keys (pk_test_) return the same real data as live keys, over a
rolling two-year window, and do not draw on your paid allowance. They are rate
limited on their own generous windows, counted separately. The window exists because
deep history is the product rather than the demo: two years spans multiple
earnings cycles and real corporate actions, which is what you need to evaluate
the API.
The message names the earliest date sandbox allows, so you can retry within the window without guessing.
What to do: either narrow from to the date named in the message, or switch
to a live key (pk_live_) for the full history. Nothing about the response shape
changes between the two — code written against sandbox works unmodified against
live. See Authentication.
not_found [#not_found]
HTTP 404. The requested resource does not exist. We never invent one to fill the gap.
What to do: resolve tickers to instrument ids through /v1/instruments
rather than constructing ids. Note that a resource can be absent as of a
date and present now: with asOf set, anything this
system had not yet learned about is correctly invisible.
On key revocation this code is also returned for "not yours", "unknown" and "already revoked" alike, because a distinguishable answer would let a caller probe which key ids exist on other accounts.
options_unavailable [#options_unavailable]
HTTP 404. The instrument exists, but there is no option chain to return for it.
Deliberately distinct from not_found, which would say the instrument itself is
unknown, and deliberately not a 200 carrying an empty array. An empty array
asserts "this underlying has no options", and that claim is false whenever the
real cause is that we have not ingested its chain yet. The message always states
which of the two cases applies:
- The asset class has no listed options served here. A permanent property of
the instrument, not a coverage gap. It will not change on a later ingest run.
Options are served for
equity,etfandindex. - No chain has been ingested for this instrument yet. A statement about our coverage, not about the market. It may change after the next ingest run.
What to do: branch on which case the message describes. The first means stop asking; the second means retry later. Treating them alike is how a consumer ends up either hammering an endpoint that will never answer, or silently concluding a liquid underlying has no options market.
unavailable [#unavailable]
HTTP 503. A required upstream or server-side capability is not configured or
is temporarily unreachable, so the answer cannot be produced honestly. This is
deliberately distinct from internal_error (an unexpected fault) and from
invalid_request (your parameters are wrong): the request is well-formed and
nothing crashed, but a dependency the endpoint needs - for example the macro
data source behind /v1/macro/** - is absent or down. You are never handed a
fabricated series in its place.
What to do: retry with backoff; a transient upstream outage usually clears. If the message names a missing configuration (for instance a data-source key an operator must set), the condition will persist until that is fixed - check status and contact support rather than retrying.
internal_error [#internal_error]
HTTP 500. An unexpected server-side failure. It deliberately carries no internal detail, because error bodies are exactly where stack traces and connection strings leak.
What to do: retry once with backoff. If it persists, check status for a known incident, then contact support with the time of the request and the endpoint. Do not retry aggressively.
A note on stale data
Staleness is not an error. When a dataset has not refreshed inside its
expected window you still get a 200 with meta.stale: true and a meta.asOf
telling you how old the answer is.
That is deliberate. A truthful stale number with its age attached is more useful
than either a fabricated fresh number or a failure. Check meta.stale on
responses whose freshness matters to you, and see status for
per-dataset ingest freshness.
Rate limits
Previous Page
Probability of reaching a target POST
The lognormal probability of a target level, as two DISTINCT numbers under two DISTINCT measures. probabilityInTheMoneyAtExpiry is the risk-neutral probability the price finishes at or beyond the target AT expiry - the figure a set-and-forget position cares about. probabilityOfTouchBeforeExpiry is the first-passage probability the target is reached at ANY time before expiry, and is always at least the at-expiry figure because a level can be touched intraday and then retraced. The touch probability is driftless and does not depend on the rate; the response states both measures so they are never conflated. POST because it is pure compute over supplied inputs.