Credits & errors

Error codes

Every error code the API returns, by family, with what to do about each.

Every error carries a machine-readable code from the closed set below. Branch on code, never on detail. The response shape and retry rules are in Errors.

Each code's type URL is https://dashboard.sproutsdata.ai/errors/<slug>, where the slug is the code with . and _ replaced by -. It redirects to that code's entry on this page.

auth.*#

Authentication and authorization.

CodeStatusWhen you see itWhat to do
auth.unauthorized401Any pre-auth failure on /v1/*: no credential, a dead session, a cookie that didn't travel. Deliberately unspecificSend a valid X-API-Key. Dashboard code: check GET /v1/auth/me. The real cause is logged against request_id
auth.missing_key401Reserved. A missing key currently returns the generic auth.unauthorizedTreat as auth.unauthorized
auth.invalid_key401A key was presented and is unknown, revoked, expired — or has the wrong prefix (data-platform keys begin sk_dsp_)Check the key value; create a new key if revoked/expired. Do not retry
auth.expired401Reserved. Expiry is currently reported via auth.invalid_key ("unknown, revoked, or expired")Create a new key
auth.insufficient_scope403The credential doesn't hold the required scope, or the key exists but doesn't grant access to this platformCreate a key with the needed scope. Not a sign-in failure — re-authenticating won't clear it
auth.unavailable503The auth service could not verify the key; nothing ranRetry shortly
auth.browser_origin403An X-API-Key arrived with an Origin header — i.e. from a browserMove the key server-side. There is no origin allowlist; this is architectural
auth.invalid_credentials401Dashboard sign-in: wrong email or password — deliberately indistinguishable (an account-existence oracle otherwise)Show a generic "wrong email or password" message
auth.account_exists409Dashboard signup with an email that already has a Sprouts accountNot a failure — route the user to sign in
auth.session_required401Dashboard surfaces only (/v1/auth/*, /v1/keys*): no valid sessionShow the login screen
auth.csrf403A session POST without a valid X-CSRF-Token. Only reachable with a valid session, so the session itself is fineRefetch the token via GET /v1/auth/me and retry
auth.mfa_required—Reserved for error use. Today MFA appears as a 200 login outcome (status: "mfa_required"), not an errorComplete the MFA verify step

request.*#

The request itself is the problem.

CodeStatusWhen you see itWhat to do
request.invalid422, 400, 404422: field validation failed — a bad value, an unknown parameter, a missing query/filters, an over-cap batch, or the retrieval engine rejected the query. extra.errors[] names the fields, and each entry's msg is written to be shown to a developer. 400: POST /v1/agents/runs without an Idempotency-Key, or with one longer than 255 characters. 404: an agent run id that doesn't exist or belongs to another workspace, or an unknown path (see the http-<status> fallback)Fix the request. Do not retry unchanged
request.too_large413Reserved: the request body exceeds the size capSend a smaller body
request.unsupported415Wrong content type — e.g. a form-encoded body on a session POSTSend Content-Type: application/json

The validation payload shape:

{
  "code": "request.invalid",
  "detail": "One or more fields failed validation.",
  "extra": {
    "errors": [
      { "type": "less_than_equal", "loc": ["body", "limit"], "msg": "Input should be less than or equal to 100", "input": 500, "ctx": { "le": 100 } }
    ]
  }
}

relationship.*#

Its own namespace rather than request.invalid, because the request was valid: you named something the corpus holds more than one of. A client should branch on that differently than on a malformed body — the same reasoning that keeps auth.* separate from request.*.

CodeStatusWhen you see itWhat to do
relationship.anchor_ambiguous422A company named by name — on any /v1/relationships* route, including inside a sentence sent to /ask — matched more than one recordRead extra.anchor (what you sent) and extra.candidates (what it could have been) and let someone pick. Anchoring by domain avoids it entirely

feedback.*#

The feedback routes. Reporting a bad result is free, so neither of these is ever a billing question.

CodeStatusWhen you see itWhat to do
feedback.subject_not_found404The request_id or run_id you reported against is unknown, outside the history window, or not yours — deliberately one code for all three, so it is not an existence oracle for another tenant's runsReport against a meta.request_id from your own recent response. Only the first page of a paginated call is recorded
feedback.unavailable503The feedback store could not be reached. Nothing was filedRetry shortly. Never surface this as "your report was rejected"

lists.*#

The saved lists routes. Lists are free, so none of these is a billing question.

CodeStatusWhen you see itWhat to do
lists.not_found404No such list in this workspace. A missing list, a deleted one and another workspace's get the same answer on purposeTake ids from a fresh list of your lists; don't retry
lists.item_cap_reached409Saving these items would take a free workspace past its saved-items cap, which counts items across all its lists. The cap is a platform setting; when none is set there is no cap. extra carries cap, saved and adding. Nothing was savedSave fewer items, remove some, or upgrade the workspace
lists.unavailable503Saved lists couldn't be reached. Nothing was changedRetry shortly

export.*#

CodeStatusWhen you see itWhat to do
export.not_enabled404CSV export (POST /v1/lists/{list_id}/export and POST /v1/search/export) isn't switched on yet. Both routes answer this todayDon't retry. Export will be announced in the changelog when it is available

pagination.*#

All 422s. Recovery is always the same: throw the cursor away and re-run the search from page 1. See Pagination.

CodeStatusWhen you see itWhat to do
pagination.cursor_invalid422The cursor is malformed, failed signature verification, or the request changed between pages (anything except limit)Restart from page 1. If a user changed a filter, that's working as intended
pagination.cursor_expired422The cursor is older than 24 hoursRe-run the search
pagination.depth_exceeded422Paging past 10,000 resultsNarrow the search into slices instead of paging deeper

idempotency.*#

Reserved: no route sends either code today. A reused Idempotency-Key never produces an error:

  • On the billed routes that honour it, the same key with the same body on the same UTC day is recognised as a retry. The same key with a different body silently starts a new request.
  • On POST /v1/agents/runs, the key is required, and a reused key returns the original run's handle with 200 instead of 202, whatever the body says. It starts and charges nothing. Run keys never expire and are scoped to your workspace.
CodeStatusWhen you see itWhat to do
idempotency.in_flight409Reserved: a request with the same idempotency key is still executingWait for the original to finish
idempotency.fingerprint_mismatch409Reserved: an idempotency key was reused with a different request bodyUse a fresh key for a different request

quota.*#

Infrastructure protection — always 429, never a billing question. There is no per-key request-rate limit on the REST API today; the codes below are the limits that exist. Honour Retry-After whenever it is present.

CodeStatusWhen you see itWhat to do
quota.burst_exceeded429Too many requests in a short window, with Retry-After. Sent by the playground's session throttles (30 requests a minute and 500 a day), the sign-in throttle, and the MCP server (60 calls a minute per credential)Wait Retry-After seconds. If you're automating through the playground, use an API key instead
quota.daily_exhausted429Reserved: never sent todayTreat like quota.burst_exceeded
quota.monthly_exhausted429Reserved: never sent todayTreat like quota.burst_exceeded
quota.concurrency_exceeded429Sent only by POST /v1/agents/runs: more than 5 runs are being started at the same moment by one credential. Carries Retry-After: 30Wait Retry-After seconds, then retry with the same Idempotency-Key
quota.entity_ceiling429Sent only by POST /v1/agents/runs: the credential has been shown 5,000,000 distinct records (the default ceiling) in the last 30 days, through searches and run results. extra carries seen and limitContact us to raise the ceiling

credits.*#

Commercial — a different family from quota.* by design, and the two are never conflated. A 402 is fixed by buying; a 429 is fixed by waiting. See Credits & usage.

CodeStatusWhen you see itWhat to do
credits.insufficient402The data-platform allocation cannot fund what this call delivered, so its results are not returned. extra carries available and required (credits, to three decimals), action_type (and sometimes unrestricted_not_usable)Buy or transfer credits. Never retry — no amount of waiting helps
credits.unavailable503The billing system could not be reached, or did not confirm the charge in time, so the request may have been charged. This is the one error that can cost creditsRetry shortly with the same Idempotency-Key, so rows already paid for aren't charged twice. Check meta.credits_charged on the retry
credits.unknown_action500Our misconfiguration: an action has no pricing row. Deliberately a 500 and never a free passReport it with the request_id; nothing was charged

payment.*#

Buying credits, on the dashboard's billing routes. Separate from credits.* because the failures are the buyer's to fix rather than a spend outcome:

  • a 402 credits.insufficient means "you have none";
  • a 402 payment.declined means "the card was refused", and sending it to a top-up screen is a dead end.

Almost all of these are reachable only with a signed-in dashboard session. The exception is payment.topup_not_found, which comes from the two top-up link routes that any credential can call. The rest are listed because code is a closed set and this page is the whole of it, as are the dashboard-only codes below.

CodeStatusWhen you see itWhat to do
payment.invalid_amount422The amount is not purchasableOffer a valid amount; detail says what was wrong
payment.declined402The bank said no. detail is the card network's own message, passed through verbatimShow detail and offer another card. Never route this to a "buy credits" screen
payment.no_method409No card on file — a missing prerequisite, not a failed purchaseSend the user to add a payment method first
payment.forbidden403The signed-in user lacks billing permission for the organisationPoint them at an owner; re-authenticating will not help
payment.unavailable503The payment service could not be reached. Nothing was chargedRetry after Retry-After
payment.topup_not_found404The top-up id is unknown, has expired (links last 15 minutes), or belongs to another workspace. The three cases get one answer on purposeStart a new top-up with POST /v1/billing/top-up/intents

upstream.*#

The retrieval engine or the agent engine failed. A metered call is charged only once its response is ready, so neither of these is charged. On search and lookup routes both carry Retry-After: 5.

CodeStatusWhen you see itWhat to do
upstream.unavailable503, 502503: the retrieval engine or the agent engine is unreachable. 502: on the agent-run routes only, the agent engine answered with an error. Not chargedRetry after Retry-After, or shortly when there is none. Retrying a run start with the same Idempotency-Key is safe
upstream.timeout504The retrieval engine did not answer in time. Not chargedRetry after Retry-After

history.*#

CodeStatusWhen you see itWhat to do
history.not_found404The run_id sent to GET /v1/usage/history/{run_id} is unknown, past the 90-day retention, or belongs to another credential — deliberately indistinguishableTake ids from a fresh GET /v1/usage/history page; don't retry

Other#

CodeStatusWhen you see itWhat to do
not_implemented501The server is the limitation, not your request: a category whose index isn't wired in this environment, or a capability that isn't built yet. detail says which capabilityDisable the feature; don't retry. Distinct from request.unsupported, which means you asked for something invalid
internal500An unexpected error on our side. No stack trace ever leaks; the details are logged against request_idQuote the request_id to support

Dashboard only#

An API key never receives these. They come from routes that need a signed-in dashboard session, and are listed so that every code has an entry.

CodeStatusWhen you see it
journey.unavailable503The dashboard couldn't record or read a step of your onboarding progress
cohort.not_offered409The dashboard's "which best describes you" question is switched off, so an answer can't be recorded
cohort.impersonation_cannot_answer403An admin acting as a user can't answer that question for them
referral.already_claimed409This account has already used its one referral code
referral.impersonation_cannot_claim403An admin acting as a user can't claim a referral code for them