Errors

The typed error envelope and the closed code registry — what to retry, what not to

Every error response, on every /v1 endpoint:

json
{"error": {"type": "billing_error", "code": "insufficient_credits",
           "message": "Not enough credits for a standard run ($10).",
           "param": null, "retriable": false,
           "doc_url": "/developers/errors#insufficient_credits",
           "request_id": "req_a1b2c3…"}}

retriable answers the only question that matters mid-loop: can an identical retry succeed? request_id is on every response (header X-Request-Id too) — include it in support requests.

Code registry (closed)

CodeHTTPRetriableMeaning / correct response
invalid_request400nomalformed input — fix the request
parameter_renamed400noyou sent a parameter by an old name after its sunset (gh #1282: workspace_id is now blueprint_id). error.param names the old one and the message the new one: resend with the new name. Ask your human before changing saved skills or instructions that use the old name. Before the sunset the old name still works, and the response carries Deprecation/Sunset headers and a warnings entry instead
validation_failed422nobody failed validation; error.errors lists fields
authentication_required / invalid_api_key401nomissing/bad key
key_revoked / key_expired401noa key we recognise that no longer works — mint or rotate a new one at /settings/api-keys
key_suspended / account_suspended403nothe abuse guard paused this key / account (a runaway loop, off-purpose prompts). Stop and alert a human; do not mint a new key — a fresh key of a suspended user is refused too. Re-enable from /settings/api-keys or your support contact
insufficient_scope403nokey lacks the scope — mint one that has it
cross_origin_refused403noa write signed in with the browser session cookie came from a page outside this app. Never seen by a request that carries no session cookie: authenticate programmatic calls with Authorization: Bearer alone (a request that also sends the cookie is treated as the browser session)
insufficient_role403nothe caller's current role is below what this route (or scope) needs, whatever the key carries — e.g. data:write needs member or above. A new key won't help: ask an organization admin for the role
blueprint_forbidden403noyou may read this organization but not the Blueprint you named: your membership (or your key's creator's) is limited to specific Blueprints and this is not one of them. Ask an organization admin for access, or name a Blueprint GET /v1/blueprints lists. (Renamed from workspace_forbidden, gh #1282)
no_blueprint_access403noyour membership is limited to specific Blueprints and none of those granted is available (none is granted, they were archived, or the organization has no Blueprint yet), so there is nothing to act on. Stop and ask a human: an organization admin must grant you a Blueprint. GET /v1/me (blueprint: null), GET /v1/blueprints (an empty list) and GET /v1/members (who your admins are) still answer. Different from no_active_blueprint: designing a Blueprint will not fix this. If we cannot READ your grants at all, you get a retriable service_unavailable instead, never this code
not_found404nounknown resource/entity
conflict409nostate conflict (e.g. rotating a revoked key)
no_subscription409noa subscription cancel or resume for an organization with no plan
subscription_ended409noresuming a subscription that has already ended. Hand your human {app_url}/pricing: subscribing again is a checkout, which takes a card
billing_update_in_progress409yesanother billing update holds the organization's billing key. Nothing was changed; retry after Retry-After (30 seconds)
ties_unresolved409noa merge (POST /v1/merges) would settle a tie (two rows with equally good evidence disagree on a cell) that nobody decided. The message names each open choice_key; POST /v1/merges/preview lists them as unresolved_ties. Send choices for each (survivor or loser), or accept_survivor_on_ties: true to keep the survivor's value. Ask your human when you act for one: it is their data
merge_across_parents409noa merge (POST /v1/merges, or its preview) names rows that sit under different parents of an entity whose names are unique per parent. Such a merge could not stick: a merge keeps names together under one parent only. The message names the parents; param is loser_ids. Merge only rows under the same parent
merge_records_changed409yesa merge (POST /v1/merges) found links to a merged row being written while it took its locks, retried once, and found it again. Nothing was written. Send the same call again
merge_changed409noa merge (POST /v1/merges) is not the merge its preview_digest confirmed: since the preview, a conflict was decided differently, a tie's values changed, a cell is newly filled, or a link a person removed since would come off the kept row. Nothing was written. (A merged row deleted since is the 404 naming it, not this.) error.details.preview is the merge as it stands now, with its own preview_digest: check what moved (ask your human when you act for one), then send that digest
preview_required422noa merge (POST /v1/merges) sent neither the preview_digest of the preview it confirms nor apply_without_preview: true. A merge discards data, so a blind merge must be stated. Preview first and send its preview_digest
lock_confirmation_required409noturning a report's monitoring back on (PATCH /v1/research/runs/{run_id}/monitoring) after its free 30 days commits 30 UTC days, and the call did not send commit_lock: true. Nothing changed. details.locked_until is when the 30 days would end (the exclusive 00:00Z end, so the last billed day is the day before it); param is commit_lock. Ask your human, then resend with commit_lock: true and expected_locked_until set to that date
lock_quote_changed409nothe same monitoring call, sent with commit_lock: true and an expected_locked_until that is no longer the end: a UTC midnight passed after your human was quoted the date. Nothing changed. details.locked_until is the new end; param is expected_locked_until. Ask your human again with the new date, then resend with it
retry_confirmation_required409noa retry (POST /v1/research/runs/{run_id}/retry) would charge for a new run and was sent without expected_total_cents matching its quote. Nothing was created or charged. details is the quote: total_price_cents (the number to send back), interactions, unit_price_cents, shortfall_cents, the priced entities, the failed run's refund, max_depth, reuse_predicted, skipped_already_held, and funding (covered_by is free_run, plan_credits or balance: the total is the list price even when the free run or plan credits pay); param is expected_total_cents. Ask your human, then resend with the total, and with funding.covered_by as expected_funding so what pays is bound too
retry_price_changed409nothe same retry, sent with an expected_total_cents that is no longer the price, or an expected_funding that is no longer what would pay (for example, plan credits spent by another run meanwhile). Nothing was charged. details is the new quote; param is expected_funding whenever the rail moved (even if the total moved too), else expected_total_cents. If the rail moved at the debit itself, the refused attempt is left as a failed run, which the next retry replaces. Ask your human again with the new quote
rerun_price_changed409noapproving a report's waiting re-research when what is waiting changed since it was shown: the total moved, or different rows wait at the same total (the quote_id no longer matches). Nothing was charged. details carries the new total_cents and count (0 when nothing is waiting any more); param is expected_total_cents. Read GET …/reruns again, show your human the new list and total, then approve with its quote_id
run_funding_changed409yesPOST /v1/research/runs when how the run would be paid changed before it started: it was handled as your org's free run, and it would have been paid from plan credits or the balance. Nothing was charged; the run is recorded as failed, not charged (it is listed by GET /v1/research/runs). Estimate again, confirm the price it states with your human, then create again
run_price_changed409noPOST /v1/research/runs sent with an expected_total_cents the run now exceeds: a query read into more subjects, other seeds or depth, or a price change since the estimate. Nothing was charged or created. details is {expected_total_cents, total_price_cents, interactions, unit_price_cents}; param is expected_total_cents. Ask your human with the new total_price_cents, then resend with it. The same or a lower price is never refused
link_not_present409noremoving a many-to-many link the record does not have: it was removed already, or never linked. Nothing changed. Read the record again before acting
knowledge_conflict409noa knowledge entry contradicts or repeats an active one (one host as both reliable and misleading, two ranges that cannot both hold, the same statement twice). Nothing was saved; error.details.conflicts lists each as {id, reason, why, statement}, reason one of authority_and_caution, disjoint_ranges, duplicate. Ask your human, then resend with on_conflict: "supersede" or "keep_both"
knowledge_revision_conflict409noa knowledge edit named the revision it edited and the entry has moved on. Nothing was saved; re-read it (error.details.current_revision) and edit again
run_recovering409yesthe run is already retrying itself after an interruption, so a manual retry is refused for now
run_stopping409yesthe run is still stopping; retry once it is cancelled
cell_not_held409noapply, keep or release on a cell nobody holds (#2196). Re-read GET /v1/holds: it may have been released already
nothing_to_apply409noapply on a held cell with no pending finding: research has found nothing newer, or it was applied already
nothing_to_keep409nokeep on a held cell with no pending finding: research has found nothing newer, or it was dismissed already (#2294)
finding_changed409noapply or keep named a finding, and research found something newer since. Nothing was written or dismissed; the message names the newer finding. Show it to your human and decide on that one
choice_changed409nochoose named the value row you showed your human (expected_value_evidence_id), and the cell changed since. Nothing was written; the message names the value it shows now. Read it again and choose
not_a_candidate409nochoose named an evidence_id that is not one of the cell's values to choose from (its beside entries with choosable: true)
nothing_to_choose409nochoose on a cell one run stated only one way: it has no other value to choose
cell_held409nochoose on a cell holding a value someone set on purpose (chat, a merge, an applied finding). Release it first (POST /v1/cells/{cell_id}/release), then choose
finding_unusable409noapply named a finding research cannot hand over whole: it was too long to store in full, is not valid JSON for a structured column, or the column no longer accepts that value. Nothing was written. Edit the cell to the value your human wants
no_active_blueprint409nothe Blueprint this call acts on (the one you named with blueprint_id, or your active one) has not been designed yet, so there is no schema to read or research into — design one at /onboarding or with POST /v1/blueprints/drafts. Approving a draft also answers this when your organization has no workspace at all; then there is nothing to approve into. Start a draft with POST /v1/blueprints/drafts instead: in an organization with no workspace, the draft gets one of its own and approving it makes that the organization's workspace
idempotency_key_in_flight409yesoriginal still running — wait Retry-After
session_busy409yesa change to the draft is still running, or another first-run draft in the workspace is still being built — wait Retry-After, re-read the draft, then approve; if that other draft has gone live meanwhile, approving this one is human-only (403): start a new draft
idempotency_key_reused422nosame key, different payload — new key or same body
rate_limited429yesback off per Retry-After
model_allowance_exhausted429nothe ORGANIZATION's model-token allowance for this billing period is used (report chat, onboarding, the parse surfaces; a research run never counts). Retry-After is the seconds to the reset, and GET /v1/me model_allowance shows usage. A free organization's lifetime allowance is plan_upgrade_required with param: "model_allowance" instead, since only subscribing lifts it
llm_daily_limit_exceeded429yesthe per-day cap on the free model surfaces (parsers, onboarding, chat) — a rate limit, not a billing wall: topping up does not lift it; retry after the window or ask support to raise it
run_concurrency_exceeded429yesyour org's own run caps (set by your admin) — queue and retry
insufficient_credits402nobalance too low — a human must top up
plan_upgrade_required402nothe plan doesn't include this capability — topping up won't help; a human must upgrade
budget_exceeded402nothis key's budget ceiling — a human must raise it
payment_required402nosubscription required for this surface
authorization_pending400yesCLI sign-in not approved yet — keep polling at interval
slow_down400yespolling faster than interval — increase it, then continue
expired_token400nothe device code expired or was already used — run deepsieve login again
access_denied403nothe human refused the CLI sign-in; or this API key or browser-login session resolves to no organization you are an active member of (for example, you were removed from the team). GET /v1/me shows org_id: null
internal_error500yesour fault; retry with backoff
service_unavailable503yestransient; retry with backoff
blueprint_busy503yesanother write to the same Blueprint is in progress. Nothing was written; retry after Retry-After

The four device codes belong to the CLI sign-in flow (CLI). authorization_pending and slow_down are not failures — they are the normal path while a human approves in their browser, which is why both are retriable: true.

Additions to this registry are announced in the changelog; codes are never removed or renamed within v1.

Errors — DeepSieve API