Errors always use the same envelope: { "error": { "code", "message", ... } }. Retry only 502 (twice, a minute apart, or after its Retry-After); never retry a 4xx.
| Status | Code | When |
|---|---|---|
| 400 | invalid_isin | The ISIN is not syntactically valid (format + checksum), or a batch / portfolio / overlap call contained no valid ISIN. |
| 400 | invalid_body | POST body is not the expected JSON shape (batch: { isins: [] }; portfolio: { positions: [{ isin, weight }] }). |
| 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. |
| 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. |
| 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). |
| 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. |
| 401 | missing_api_key | No Authorization: Bearer header (or X-Api-Key) was sent. |
| 401 | invalid_api_key | The key is malformed or unknown. |
| 401 | revoked_api_key | The key exists but was revoked from 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. |
| 400 | template_not_found | POST /factsheets: `template` names no template of this account (GET /factsheets/templates lists them). |
| 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`. |
| 404 | not_cached | Demo endpoint only: the ISIN has not been loaded yet. Use an API key to load it. |
| 404 | not_found | Webhooks: no such endpoint on this account. |
| 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. |
| 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`). |
A 404 fund_not_found for any reason but not_a_fund carries the coverage queue's promise. recheck.nextRecheckAt is the daily run (02:30 UTC) FundFacts expects to load the ISIN in: an ISIN never rechecked is due from the next run, then every 7 days, or every 3 days when two or more accounts wait for it; up to 6 ISINs are loaded a day, so the queue ahead can push an estimate back a day. It is an estimate, not a schedule; the account is e-mailed when the fund has data, and the message states the same date. coverage.upload says whether the account's plan can skip the wait: on Pro and up (or the trial) the dashboard's Factsheets tab (/dashboard?tab=factsheets&isin=…) makes the sheet now from the fund's own KID or factsheet PDF; upload.plan is the cheapest plan that can.
404 Not Found — json{"error": {"code": "fund_not_found","reason": "issuer_not_covered","message": "No published documents were found for this fund: its management company is not covered yet. It is queued for a recheck around 10 September 2026; we e-mail you when it has data. This lookup was not counted."},"isin": "SE0000522724","cached": false,"generatedAt": "2026-09-09T08:12:44.000Z","billed": false,"recheck": {"queuedAt": "2026-09-09T08:12:44.000Z","nextRecheckAt": "2026-09-10T02:30:00.000Z","askers": 1},"coverage": {"queued": true,"notify": "email","upload": {"allowed": false,"plan": "pro"}},"plan": "free","quota": {"…": "unchanged"},"data": {"…": "empty skeleton"}}