# Errors

Every error code, when it happens and what to do.

HTML version: https://fundfactsapi.com/docs/errors · Updated 2026-10-25

## 6. Errors

All errors have the shape `{ "error": { "code": string, "message": string, ...details } }`.

| HTTP | `error.code` | When | What to do |
|---|---|---|---|
| 400 | `invalid_isin` | The ISIN is not syntactically valid (format + checksum), or a batch / portfolio / overlap call contained no valid ISIN. | Fix the input; validate ISINs client-side first. |
| 400 | `invalid_body` | POST body is not the expected JSON shape (batch: { isins: [] }; portfolio: { positions: [{ isin, weight }] }). | Surface the message to the user. |
| 400 | `invalid_query` | Search: ?q= is missing or shorter than two characters. History (GET /funds/{isin}/history): ?fields= names a key other than ter, costs, aum, riskRating, topHoldings, or ?since= is not a calendar date (YYYY-MM-DD). Changes: ?since= is not an ISO-8601 timestamp. | Surface the message to the user. |
| 400 | `watchlist_full` | POST /watchlist: the distinct funds of every list with alerts on would exceed the plan's allowance (10 funds on Starter, 100 on Pro, 500 on Scale). Includes `limit`, `watched`, `wouldWatch` and `required` (the plan that lifts it) when one exists. | The plan's watch allowance is used: show limit, watched and wouldWatch, remove funds with DELETE /watchlist or tell the user which plan (required) lifts it. |
| 400 | `watchlist_required` | POST /webhooks on Starter, Pro and the Pro trial: the watchlist is empty (fill it with POST /watchlist first), or the ISINs named are not all watched (`unwatched` lists them; omit isins to follow every watched fund). | Fill the watchlist first (POST /watchlist with the ISINs in unwatched), then register the webhook; or omit isins to follow every watched fund. |
| 400 | `batch_too_large` | More ISINs than the plan's batch size (Free 1, Starter 10, Pro 50, Scale 200, Enterprise 1,000). Includes batchMax. | Surface the message to the user. |
| 401 | `missing_api_key` | No Authorization: Bearer header (or X-Api-Key) was sent. | Check FUNDFACTS_API_KEY / the Authorization header; ask the user for a valid key. |
| 401 | `invalid_api_key` | The key is malformed or unknown. | Check FUNDFACTS_API_KEY / the Authorization header; ask the user for a valid key. |
| 401 | `revoked_api_key` | The key exists but was revoked from the dashboard. | Ask the user to create a new key in the dashboard. |
| 404 | `fund_not_found` | No fund data for this ISIN. `reason` is not_a_fund (a share, bond or other security; `instrument` names it when known), issuer_not_covered, documents_unreadable or temporarily_unavailable. Not counted, up to 15 misses a month or the plan's allowance if larger (`billed` says which). Except for not_a_fund, the ISIN is queued: `recheck` gives `queuedAt`, the estimated daily run it is loaded again in (`nextRecheckAt`, 02:30 UTC; the next run when never rechecked, then every 7 days, or 3 when two or more accounts wait for it; up to 6 ISINs a day, so the queue ahead can push it back a day; null once nobody asked for it in 60 days) and `askers`; `message` states the same date; the account is e-mailed when the fund has data. `coverage.upload` says whether the account's plan can skip the wait by uploading the fund's own KID in the dashboard's Factsheets tab (/dashboard?tab=factsheets&isin=…, Pro and up or the trial) and names the cheapest plan that can. The response still contains the empty `data` skeleton. On GET /funds/{isin}/history the 404 is never counted, carries no data, cached or coverage key, and an ISIN never loaded (or whose first load is still running) has no reason: call GET /funds/{isin} once and its history starts with that refresh. | No fund data (not counted); error.reason says why. not_a_fund is final. For the other reasons the ISIN is queued: read recheck.nextRecheckAt (an estimated ISO date of the daily run that reloads it) and tell the user that date; the account is e-mailed when it has data. When coverage.upload.allowed is true, point the user to /dashboard?tab=factsheets&isin=<ISIN> to make the sheet now from the fund's own KID PDF. Do not retry in a loop or look elsewhere. |
| 400 | `template_not_found` | POST /factsheets: `template` names no template of this account (GET /factsheets/templates lists them). | Surface the message to the user. |
| 403 | `plan_required` | The endpoint or option needs a higher plan: portfolio and overlap need Starter; the watchlist, webhooks and the change feed need Starter (watched funds; every fund in the store on Scale); extract, factsheet templates, logos and design options, client reports and fund history (GET /funds/{isin}/history) need Pro; export needs Scale. Includes `required`. | The feature is not in the account's plan: tell the user neutrally which plan (required) has it, link /dashboard/billing, do not retry. |
| 404 | `not_cached` | Demo endpoint only: the ISIN has not been loaded yet. Use an API key to load it. | Surface the message to the user. |
| 404 | `not_found` | Webhooks: no such endpoint on this account. | Surface the message to the user. |
| 429 | `rate_limited` | Monthly requests (and overage cap) exhausted, or the per-minute burst limit hit. `reason` is quota_exhausted \| overage_cap \| burst; includes retryAfter (seconds) and resetAt; the Retry-After header is also set. For quota_exhausted and overage_cap, `upgrade` names the next plan up when there is one ({ plan, name, requests, price, url }) and `message` says what it gives. | Wait Retry-After seconds; reduce parallelism. On quota_exhausted or overage_cap, show the user error.upgrade (next plan, price, url) rather than retrying. |
| 502 | `upstream_error` | The fund data could not be retrieved (temporary upstream outage) and none is stored. Retry after `retryAfter` seconds (also in the Retry-After header); it is not counted. When a payload is stored, it is served instead (200, `cached: true`, `expiresAt` in the past and `stale: true`). | Retry after 60 s, at most twice; then surface the error. |

Validate ISINs before calling (12 characters: 2 letters, 9 alphanumerics, 1 check digit, Luhn over the digit expansion) to avoid spending a request on a typo.

Full reference: https://fundfactsapi.com/llms-full.txt
