Errors
Every error returns a JSON body with the right HTTP status code. There are two body shapes — one for the retrieve, documents, and usage endpoints, and a leaner financialdatasets-compatible shape for the financial data endpoints.
Error envelopes#
The API uses two error shapes, depending on the endpoint. The retrieve, documents, and usage endpoints return the standard FocusAlpha envelope: success is always false, and the error object carries the status code, a human-readable message, the error name, the request path, a timestamp, and — when an X-Request-ID header was supplied or generated — an optional requestId you can quote when contacting support.
{
"success": false,
"error": {
"statusCode": 401,
"message": "Invalid or missing API key",
"error": "Unauthorized",
"path": "/v1/retrieve",
"timestamp": "2026-05-30T12:00:00.000Z",
"requestId": "req_8f3c..."
}
}The financial data endpoints — Financial Statements, Financial Metrics, SEC Filings, Filing Items, Company Facts, and Institutional Holdings — return a leaner, financialdatasets-compatible body with only two fields: error (the HTTP reason phrase) and message (the human-readable string). There is no success, statusCode, path, or timestamp on these responses — the HTTP status line carries the code.
{
"error": "Unauthorized",
"message": "Invalid API key provided"
}Status codes#
400Bad Requestoptionalquery, an out-of-range top_k, an unknown field, or a malformed filter. Note that tickers, year, quarter, and source_types must be nested under a filters object, not sent as top-level fields. The message names the offending field.401Unauthorizedoptional404Not Foundoptional402Payment Requiredoptional429Too Many Requestsoptional402 for the same condition.)500Internal Server Erroroptional503Service UnavailableoptionalRetry-After header (in seconds); wait that long, then retry.500, never a misleading 401. A 401 always means the key itself was rejected.Handling errors#
Check the HTTP status first, then read the error message for specifics. Retry 429, 503, and 500 with exponential backoff; when a Retry-After header is present (on 503), honor it instead of your own backoff. On the financial data endpoints a 402 from a per-minute rate-limit breach is also transient and safe to retry — but a 402 indicating a hard plan or quota cap should not be blind-retried. Don’t retry 400 or 401 without changing the request or key.
error (so error.statusCode / error.message); the data endpoints return a flat { error, message }, where message is the human string. Read the HTTP status code from res.status rather than the body so the same handler works for both.const res = await fetch(url, options);
if (!res.ok) {
const body = await res.json();
// A 402 on a data endpoint is a rate-limit breach when Retry-After is set;
// a hard plan/quota cap has no Retry-After and should not be blind-retried.
const retryable =
res.status === 429 ||
res.status >= 500 ||
(res.status === 402 && res.headers.has("retry-after"));
if (retryable) {
const retryAfter = Number(res.headers.get("retry-after")) || 0;
// honor Retry-After (seconds) when present, else exponential backoff
await backoffAndRetry(retryAfter);
} else {
// 400 / 401 / hard 402 — fix the request, key, or plan
throw new Error(body.message);
}
}