error.retryable is true.
code is the stable identifier to branch on; message is human-readable and may change. The table below carries the live messages captured against production.
| Code | HTTP | Retryable | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | No | Send a valid key in the X-API-Key header. Message: “Missing or invalid X-API-Key header.” A revoked key lands here within about a minute of revocation. Do not retry the same key. |
PLAN_UPGRADE_REQUIRED | 403 | No | The route is gated above the key’s plan. Message: “This endpoint requires Starter or Builder. Free includes search, coverage, snapshot, and valuation.” Upgrade or stay on the four Free routes. See plans and limits. |
SYMBOL_INVALID | 400 | No | The symbol failed grammar. Message: “Symbol must match ^[A-Z0-9-]{1,8}$ with dash-suffix share classes like BRK-B.” Normalize before sending (BRK.B -> BRK-B). |
SYMBOL_UNKNOWN | 404 | No | Well-formed symbol, not in the universe. Message: “Symbol ‘ZZZQ’ is not in the supported universe. Use /v2/search to discover supported tickers.” Resolve it via /v2/search or /v2/coverage. |
NOT_FOUND | 404 | No | The request path is not a StockContext route, a typo or the wrong base path. Message: “Endpoint not found.” Distinct from SYMBOL_UNKNOWN (which is a valid route with an unknown symbol). This 404 fires before key verification, so it carries no rate-limit headers. |
PARAM_INVALID | 400 | No | A query parameter is missing, unknown, or out of range. The message names the offending parameter and, for enum/range errors, lists the allowed values: e.g. “range ‘99z’ is not a recognized window; allowed: 1m, 3m, 6m, …”, “Unknown query param(s): ‘bogusparam’. Allowed params: symbol.”, “Invalid request parameters: query param ‘symbol’: field required.” Branch on the named parameter. Duplicate params are tolerated (last value wins: ?symbol=AAPL&symbol=MSFT returns MSFT). |
RATE_LIMITED | 429 | Yes | Per-minute, daily, or monthly limit hit. Honor Retry-After (seconds), lower concurrency, and cache repeat reads. Limits are in plans and limits. |
UPSTREAM_UNAVAILABLE | 503 | Yes | A market-data or SEC-backed source is briefly unavailable with no usable cache. Retry with backoff; keep first-hit SEC paths tolerant of latency. |
AUTH_UPSTREAM_UNAVAILABLE | 503 | Yes | Key verification upstream is briefly unavailable. Retry shortly; the response sends Retry-After: 5. |
PREREQUISITE_MISSING | 409 | No | MCP resources only, never a REST endpoint. A resource was read before the tool call that populates it. Run the prerequisite tool first, then read the resource. |
403 PLAN_UPGRADE_REQUIRED, not 400. PARAM_INVALID now names the offending parameter (and usually its allowed values), so you can branch on it; the per-endpoint pages remain the authority on the full contract: allowed parameter names, ranges, and enum values (form lists, range/cadence values, the 2–12 symbol bound on /v2/compare, and so on).
The 429, with headers
Successful responses and every post-auth error (400 PARAM_INVALID, 403 PLAN_UPGRADE_REQUIRED, 404 SYMBOL_UNKNOWN, 429 RATE_LIMITED) carry X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix epoch seconds); track your budget from all of them, not just successes. Only failures that never reach key verification omit them: a pre-auth 401 UNAUTHORIZED and an unmatched-route 404 NOT_FOUND. A 429 adds Retry-After in seconds. This is a live rate-limit response from /v2/snapshot:
HTTP 429: headers + body
Retry-After: 21 is the live-captured wait; sleep at least that long before retrying. If Retry-After is absent on a retryable error, fall back to exponential backoff with jitter.
A retry loop that honors Retry-After
Retry only whenretryable is true, prefer the server’s Retry-After, and cap attempts so a sustained outage does not spin forever.
- Python
- TypeScript
retry.py
MCP surfaces the same errors
The MCP tools call the same REST endpoints and fail with the samecode, message, and retryable. A tool error renders as a single string: