{ "data": ... } or { "error": { "code", "message", "retryable" } }. Read error.retryable and do exactly what it says: retry when it is true, surface the fix when it is false. Do not infer recovery from the HTTP status alone, and never retry an unsupported data state: that is a 200 with real data, not an error.
Retry policy
This table is the whole decision.retryable comes straight off the envelope; the HTTP status is for logging, not branching.
| Code | HTTP | Retry? | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | No | Fix the X-API-Key header; recreate the key if it was revoked. |
PLAN_UPGRADE_REQUIRED | 403 | No | The route needs Starter or Builder; stay on Free routes or upgrade. |
SYMBOL_INVALID | 400 | No | Fix the ticker format (^[A-Z0-9-]{1,8}$, e.g. BRK-B). |
SYMBOL_UNKNOWN | 404 | No | Resolve the name with /v2/search first; the symbol is outside the universe. |
NOT_FOUND | 404 | No | The path is not a StockContext route; fix the URL or base path. Distinct from SYMBOL_UNKNOWN; carries no rate-limit headers (404s before key verification). |
PARAM_INVALID | 400 | No | Fix params. The message names the offending parameter and, for enum/range errors, the allowed values. |
RATE_LIMITED | 429 | Yes | Wait the Retry-After seconds, then resume slower. |
UPSTREAM_UNAVAILABLE | 503 | Yes | Retry with backoff; a cold SEC path may need a loading state. |
AUTH_UPSTREAM_UNAVAILABLE | 503 | Yes | Retry shortly; sends Retry-After: 5. |
PREREQUISITE_MISSING | 409 | No | A required cached prerequisite is missing; call the underlying endpoints first, then retry the request manually. |
PARAM_INVALID names the offending parameter and lists allowed values, so you can branch on it: a bad range returns range '99z' is not a recognized window; allowed: 10y, 1m, 1y, 2y, 30y, 3m, 5y, 6m, max, ytd., and an unsupported fields value returns the param name plus the full allowed set. Log the request you sent too: the message tells you which param, but your own log tells you the value. The error codes reference carries the message shapes.
The 429 envelope
A rate-limited response pairs the retryable error body with headers that tell you exactly how long to wait. This is the live fixture:HTTP/1.1 429 Too Many Requests
Retry-After is in seconds (21 here). Sleep at least that long before the next request for that key; sending sooner earns another 429.
A retry loop that obeys its own rules
The loop has to do four things the table implies: retry only onretryable: true, prefer the Retry-After header over a guessed backoff, cap total attempts so a persistent outage fails fast, and cap total wait so you never sleep for hours. The wait budget must cover a full minute window: a minute-bucket 429 can send Retry-After of up to 60 seconds, so a 30-second budget would give up on a limit that was about to clear. Below, three attempts and a 90-second ceiling.
- Python
- TypeScript
retry.py
429 comes from the plan quota rather than the minute bucket (Free’s daily 100, paid monthly quotas), Retry-After is the time to the window reset and can be hours (a Free daily 429 sends roughly the seconds until midnight UTC). The loop above correctly refuses to sleep that long and fails fast with wait budget exhausted; treat that as “stop the job and surface it”, not as an error to swallow. Plans and limits covers how the two windows show up in the headers.
The same loop works behind an MCP client: a tool failure renders as [CODE] message (retryable: true|false), so parse the retryable flag out of that string and branch identically. PREREQUISITE_MISSING is non-retryable; call the underlying endpoints it depends on first instead of looping.
Where this goes wrong
- Parsing a success as a raw object instead of reaching into
{ data }. - Retrying
UNAUTHORIZEDwith the same revoked key, or treatingPLAN_UPGRADE_REQUIREDas an outage. - Retrying
freshness: "unsupported"; it is a stable200, not a transient failure. - Ignoring
Retry-Afterand hammering the same key into a second 429. - Retrying forever. Without an attempt cap, a real upstream outage turns one slow request into a hung worker.
Production patterns
Drop this retry loop into a cached, key-safe client.