# DeepSieve API > DeepSieve is a deep-research API: define a schema (Blueprint), start > research runs from a natural-language query, and read back a cited dataset, > re-verified while monitoring is on. ## Agent instructions - SETUP: if you are a coding agent being connected to DeepSieve for the first time, fetch https://deepsieve.ai/setup.md and follow it — it configures the MCP server, skill, and rules file for whichever agent you are - Base URL: https://deepsieve.ai - Auth: two ways. MCP over http → browser login (OAuth), no key needed. REST or headless MCP → `Authorization: Bearer ds_live_...` (mint at /settings/api-keys, Agent preset) - Verify first: `GET /v1/me` - Discover the schema before querying data: `GET /v1/blueprints` for the ids, then `GET /v1/blueprints/{id}` (never guess columns) - Test FREE first: `POST /v1/research/runs` with `{"query": "...", "dry_run": true}` → a free simulated run: 202 + Location, completes in ~15s of polling, sample cited rows (nothing billed/persisted) - Real runs are async: `POST /v1/research/runs` returns 202 + Location; poll `GET /v1/research/runs/{id}` every ~15s until `done`; they bill prepaid credits and take 15-60 min - Always send `Idempotency-Key` on POST /v1/research/runs — runs bill real money and retries must not double-charge - Ask the research agent about a finished run (`POST /v1/research/runs/{id}/chat/messages`) — your messages appear in the customer's own chat history attributed to your key, so write for a human reader - Schema boundary: you may CREATE a new Blueprint (POST /v1/blueprints/drafts) but can NEVER edit the active one — propose changes to your human - Citations carry a `verdict` (supported…contradicted) and `cited` flag — never present a contradicted or uncited cell as fact - If an entity-read response has `truncated: true` / `preview_row_cap`, you have a PREVIEW, not the dataset — tell the human - Errors: every failure has `{error: {code, message, retriable, request_id}}`; `retriable=false` means do NOT retry the same request - Terminal/CLI: `uv tool install deepsieve-cli` then `deepsieve login` (browser device flow) then `deepsieve --json data get ` — stable exit codes, `--profile` per deployment - Machine-readable spec: /openapi.json — markdown mirror of any docs page: append `.md` to its URL ## Get started - [Quickstart](https://deepsieve.ai/developers/index.md): Set up DeepSieve in your coding agent with one prompt - [Authentication & API keys](https://deepsieve.ai/developers/authentication.md): Scoped, revocable bearer keys — mint, rotate, revoke, and the scope model - [Your first research run](https://deepsieve.ai/developers/first-run.md): The async job contract — create, poll, read results, retry safely ## API components - [Blueprints & your schema](https://deepsieve.ai/developers/blueprints-and-data.md): The customer-defined schema behind every dataset — discover it, never guess it - [Research runs](https://deepsieve.ai/developers/runs.md): Statuses, retries, refunds, depth, and monitoring — the run lifecycle in full - [Webhooks](https://deepsieve.ai/developers/webhooks.md): Signed run.* and dataset.updated events — Standard Webhooks verification in 10 lines - [Dataset sync](https://deepsieve.ai/developers/dataset-sync.md): Read the cited dataset incrementally — cursors, updated_since, per-cell citations - [Global Reports](https://deepsieve.ai/developers/reports.md): Create, edit, and read saved cross-run reports programmatically - [Chat with the research agent](https://deepsieve.ai/developers/chat.md): Ask why a value says what it says, or request a re-check — visibly, in the customer's own thread ## Integrations - [The API](https://deepsieve.ai/developers/api.md): There is a REST API — this is its whole surface, one link per component - [Choosing an integration](https://deepsieve.ai/developers/integrations.md): Which way to connect DeepSieve — MCP, skill, SDK, or raw REST - [MCP server](https://deepsieve.ai/developers/mcp.md): Add DeepSieve to Claude Code, Cursor, or any MCP client — browser login, no key to paste - [SDKs](https://deepsieve.ai/developers/sdks.md): Python SDK (generated, typed, sync+async) — TypeScript next - [CLI](https://deepsieve.ai/developers/cli.md): Run cited research from your terminal — browser sign-in, no key to paste ## Reference - [Errors](https://deepsieve.ai/developers/errors.md): The typed error envelope and the closed code registry — what to retry, what not to - [Rate limits & budgets](https://deepsieve.ai/developers/rate-limits.md): RateLimit headers, 429 semantics, and per-key spend ceilings - [Versioning & stability](https://deepsieve.ai/developers/versioning.md): The /v1 contract — what is promised now, and what is promised at GA - [Changelog](https://deepsieve.ai/developers/changelog.md): API changes, newest first --- # Quickstart Connect DeepSieve and your agent gets a **cited, structured dataset it can query, re-verified while monitoring is on** — instead of re-researching the same subjects on every task. Don't configure it by hand — let your coding agent do it. Paste this into Claude Code, Cursor, Copilot, Codex, or any assistant that can read a URL: ```agent-prompt Set up DeepSieve by following the instructions here: https://deepsieve.ai/setup.md ``` **Your agent will then:** 1. Detect which agent it is and whether it can open a browser. 2. Connect the DeepSieve MCP server (`deepsieve`) — browser login, or an API key if it's running headless. 3. Install the DeepSieve Agent Skill, and append a `## DeepSieve` section to your agent's rules file (`CLAUDE.md`, `AGENTS.md`, or `.cursor/rules/`). 4. Offer to install the CLI if you work in a terminal. 5. Verify the connection and report your workspace, scopes, and run costs. **What it will not do**, stated as constraints the payload itself carries: - **Never start a research run.** Runs cost money — [$10 standard, $20 at max depth](/developers/runs). Setup spends nothing, explicitly including "to check it works". - **Never ask you for an API key in chat** while browser login is available — transcripts get logged and shared. - **Never install a global runtime or touch your shell config**, and nothing outside this project and your agent's own config. Read it yourself before you run it — it's plain markdown, and the constraints above are in its own words: **[https://deepsieve.ai/setup.md](https://deepsieve.ai/setup.md)**. ## Prefer to wire it yourself? There's a full REST API underneath all of this — see **[The API](/developers/api)** for the whole surface, one link per component. The options below are the common starting points.
Connect the MCP server manually The server is at `https://deepsieve.ai/mcp` and uses browser login — no API key. ```bash claude mcp add --transport http deepsieve https://deepsieve.ai/mcp ``` Then run `/mcp`, pick `deepsieve`, and choose **Authenticate**. For config-file agents (Cursor `.cursor/mcp.json`, VS Code `.vscode/mcp.json`), register the same URL — full per-agent snippets are in [MCP server](/developers/mcp).
Headless / CI (no browser) Create a key at [Settings → API keys](/settings/api-keys) with the **Agent** preset (runs research and reads results; can't change your schema or spend settings). Put it in the environment as `DEEPSIEVE_API_KEY` — never in a commit or a chat message. ```bash claude mcp add deepsieve -- uvx deepsieve-mcp ``` Or call the REST API directly: ```bash curl -s https://deepsieve.ai/v1/me -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json-response {"object": "identity", "org_id": "…", "role": "admin", "workspace_schema": "…", "auth_kind": "api_key", "scopes": ["data:read", "export:read", "runs:read", "runs:write", "…"]} ```
Work in a terminal? Use the CLI ```bash uv tool install deepsieve-cli deepsieve login ``` Browser sign-in, then `deepsieve runs create --query "..." --dry-run --wait`. Full reference: [CLI](/developers/cli).
Just the skill (no MCP) The skill teaches any assistant to drive the REST API — auth, running research, reading cited data: ```bash npx skills add https://deepsieve.ai ``` [Agent Skills](https://agentskills.io) is an open standard, so the same package works in Claude Code, Cursor, GitHub Copilot, Codex, Gemini CLI and ~70 other clients. Target one agent with `-a claude-code`, or install globally with `-g`. No Node? It's plain markdown at stable URLs — fetch and drop into your agent's skills directory, preserving the relative paths: ```bash curl -s https://deepsieve.ai/.well-known/skills/index.json # name + every file curl -sO https://deepsieve.ai/.well-known/skills/deepsieve/SKILL.md ```
## Two free paths, and they answer different questions **Your first real research run is free** — one per account, before you subscribe. It's a genuine run on your own question, and its report is readable (capped at 10 rows). If it fails, it isn't consumed. That's the one that tells you whether the *research* is any good, because you get real cells about a market you know, each with its sources and a `verdict` — including `contradicted` and `unverifiable` when we couldn't stand a claim up. See [reading a citation honestly](/developers/dataset-sync#reading-a-citation-honestly). **`dry_run` is free and unlimited** — it tells you whether your *integration* works. Same shape, same statuses, ~15 seconds, nothing billed or persisted, but the rows are samples. Build your poll loop here; judge the product on the free real run. Every integration should be built against `dry_run` first. It behaves exactly like a real run — `202` + `Location`, poll to completion — but finishes in ~15 seconds, costs nothing, and persists nothing: ```bash curl -s -X POST https://deepsieve.ai/v1/research/runs \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"query": "anything", "dry_run": true}' ``` It returns sample **cited** rows in your Blueprint's real shape (`"test": true`). Build your whole create → poll → parse loop here, then drop `dry_run`. Ask your agent to do this for you — "start a dry run and show me the rows" — once setup finishes. ## Where next - **See the whole API** → [The API](/developers/api) - **Run research from my app** → [Your first research run](/developers/first-run) - **All the MCP tools** → [MCP server](/developers/mcp) - **Work from a terminal** → [CLI](/developers/cli) - **Which integration should I use?** → [Choosing an integration](/developers/integrations) - **Sync results into my systems** → [Dataset sync](/developers/dataset-sync) + [Webhooks](/developers/webhooks) - **Ask about a value, or get it re-checked** → [Chat with the research agent](/developers/chat) Machine-readable: [/llms.txt](/llms.txt) · [/llms-full.txt](/llms-full.txt) · [/openapi.json](/openapi.json) — and any page here is markdown if you append `.md`. --- # Authentication & API keys Every API request authenticates with a bearer key: ```bash curl -s $BASE_URL/v1/me -H "Authorization: Bearer ds_live_..." ``` **API keys authenticate the `/v1` API only.** The legacy `/api/*` routes are the web app's private, cookie-authenticated surface — a key used there gets a typed 403 pointing back here. Keys are minted at **[Settings → API keys](/settings/api-keys)** (or via `POST /v1/keys` with an existing admin key). The secret is shown **once**; DeepSieve stores only a hash. ## Scopes A key holds `resource:action` scopes; a request needs the route's scope or it gets `403 insufficient_scope`. Presets: | Preset | Scopes | Use for | |---|---|---| | **Agent** (default) | `runs:*`, `data:read`, `reports:*`, `export:read`, `blueprint:read`, `blueprint:create` (create-only), `blueprint:propose` (ask-only), `webhooks:*` | coding assistants, agents — can start runs (prepaid credits; cap with a per-key budget), manage reports, and create NEW Blueprints. Editing the active Blueprint is human-only regardless of scopes | | **Read-only** | all `:read` scopes | dashboards, sync jobs | | **Full** | everything except platform admin | trusted backends | Custom scope lists: `POST /v1/keys {"name": "...", "scopes": ["runs:read", ...]}`. A key can never exceed its minter: it stores the minter's role, and a key can't mint or rotate its way to scopes it doesn't hold. **Some scopes require another, and we add it for you.** `blueprint:propose` requires `data:read`: the propose response contains the Blueprint's full spec, so it asks the same rights as reading that spec directly. Request `blueprint:propose` on its own and the key is created with `data:read` as well, rather than failing at its first call. **The `scopes` array in the response is what was actually granted** — compare it with what you asked for to see any addition. Nothing unrelated to your request is ever added. ## Rotate and revoke ```bash curl -s -X POST $BASE_URL/v1/keys/{key_id}/rotate -H "Authorization: Bearer ds_live_..." ``` Rotation returns a new secret; the old one keeps working for **24 hours** so you can redeploy without downtime. `DELETE /v1/keys/{key_id}` revokes immediately. ## Optional per-key limits At mint time you can set expiry, a workspace pin, and budgets — in the create-key dialog (expiry + budgets) or via `POST /v1/keys` (`expires_at`, `workspace_id`, `budget_runs`, `budget_cents`). A key at its budget gets `402 budget_exceeded` — the agent should stop and alert a human. Combined with prepaid billing, the worst an agent can do is exhaust its budget and stop; a key can never generate an invoice. ## Browser login for MCP (OAuth 2.1) When you add DeepSieve to a coding agent over `--transport http`, you don't paste a key — the client runs an OAuth 2.1 browser flow. The consent screen is **DeepSieve's own login** (via our identity provider); it grants a research + read session (`runs:*`, `data:read`, `reports:read`), never billing or key-management scopes — a browser session is a person, not a service credential. Tokens are short-lived and refresh silently; revoke a client's access from your account without affecting your API keys. Discovery follows RFC 9728: the endpoint advertises itself at `/.well-known/oauth-protected-resource`. Use an **API key** instead for CI, containers, and server-to-server, where no browser exists. --- # Your first research run Research runs are **asynchronous jobs**: creating one returns immediately; the research itself takes minutes. Polling is the contract — never hold a connection open waiting. ## Test mode — iterate for free `{"dry_run": true}` behaves exactly like a real run — `202` + `Location`, `status: "running"` — except it's free, instant to set up, and completes in ~15 simulated seconds. Poll the returned id like a real run; on completion it carries sample rows + per-cell citations in your Blueprint's real shape (`"test": true` throughout). Nothing is billed, persisted, or researched. Build your **entire** create → poll → parse loop against it first. ## Create ```bash curl -s -X POST $BASE_URL/v1/research/runs \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"query": "European industrial-automation companies using AI vision"}' ``` Send **either** `query` (a natural-language research intent) **or** `seeds` (a list of URLs or names). Optional: `guidance` (steering text), `depth` (`"standard"` | `"max"`), `monitored` (keep the results fresh after the run; default true). Response: `202` with a `Location` header and the run resource. ## Always send Idempotency-Key Runs bill real money. With an `Idempotency-Key` header, retrying the same request replays the original response instead of starting (and billing) a second run. Reusing a key with a *different* body is a `422`; retrying while the original is in flight is a `409` with `Retry-After`. ## Poll ```bash curl -s $BASE_URL/v1/research/runs/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json-response {"object": "research_run", "id": "…", "status": "running", "done": false, "error": null, "progress": {"phase": "investigate", "entities_found": 4, "entity_counts": {"companies": 4}}, "metrics": {"search_queries": 41, "sources_read": 12, "…": "…"}} ``` `status` is a closed enum: `queued | running | cancelling | completed | failed | cancelled`. `done: true` means terminal. On `failed`, `error.code` / `error.message` say why — a failed run is automatically refunded. Poll every ~15 seconds (the `Retry-After` header on non-terminal responses is the hint). `POST /v1/research/runs/{id}/cancel` stops an in-flight run. ## List `GET /v1/research/runs?limit=&cursor=&status=` returns the standard list envelope `{"data": [...], "has_more": bool, "next_cursor": "…"}`. ## Read the results Results land in your Blueprint's entity tables, not in the run object — see [Dataset sync](/developers/dataset-sync). Webhooks fire on `run.completed` / `run.failed` / `run.cancelled` if you'd rather not poll — see [Webhooks](/developers/webhooks). --- # Blueprints & your schema A **Blueprint** is the schema of one DeepSieve workspace: which entities exist (e.g. `companies`, `funds`), their columns, and their relationships. It's defined by the workspace owner during onboarding — **it is different for every customer and can change after you integrate**. **The boundary in one sentence:** an agent can build a *new* workspace's Blueprint end-to-end, but can never alter a *live* one — editing the active schema is human-only, enforced at the auth gate regardless of scopes. That has one hard consequence for integrators: **never hardcode entity or column names**. Discover them — `GET /v1/blueprints` lists the ids and marks the active one, then read the schema of the one you mean: ```bash curl -s $BASE_URL/v1/blueprints/{blueprint_id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json-response {"object": "blueprint", "blueprint": {"domain_name": "…", "entities": ["companies", "funds"]}, "entities": [{"key": "companies", "columns": [{"name": "name", "type": "text"}, {"name": "hq_city", "type": "text"}]}]} ``` Exports embed the same schema metadata so downstream consumers stay self-describing. ## Creating a Blueprint (agent-drivable) A key with `blueprint:create` can take a **new** workspace through onboarding end-to-end — describe, review the inferred draft, request adjustments, approve: ```bash # 1. Start: describe the research domain in natural language → 202 curl -s -X POST $BASE_URL/v1/blueprints/drafts \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"prompt": "Track industrial-automation vendors: products, funding, key people"}' # 2. Poll until status is "ready" (or "awaiting_input"), then REVIEW the draft curl -s $BASE_URL/v1/blueprints/drafts/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" # → { "draft_blueprint": {...}, "follow_up_questions": [...] } # 3. Answer follow-ups and/or ask the inference agent for adjustments — one # synchronous chat turn: the reply, the measured changes, and `applied` curl -s -X POST $BASE_URL/v1/blueprints/drafts/{id}/chat \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"message": "add a founded_year column to companies"}' # → { "reply": { "content": "...", "questions": [], "not_done": [] }, # "changes": [ { "kind": "add_column", "entity": "companies", "tier": "green", ... } ], # "applied": true, "draft_revision": 2, ... } # (send your own "message_id" UUID to make retries idempotent; without one a # retry is a second model call. `/adjust` — 202 → poll — is deprecated.) # 4. Approve → real tables are created and the workspace activates curl -s -X POST $BASE_URL/v1/blueprints/drafts/{id}/approve \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" # 202 → poll until "active" ``` Statuses: `interpreting | awaiting_input | ready | adjusting | instantiating | active | failed`. Approval creates real schema — an agent acting for a human should show them the draft before approving. ## Editing the active schema — human-only Edits to an **already-active** Blueprint are refused for API-key callers at the gate itself, regardless of role or scopes — an agent may *propose* a change to its human, never apply one. Humans edit in-app at **Edit Blueprint** (`/blueprint`) with data-safety tiers (green auto, amber confirm, red refused). --- # Research runs Base URL + auth as everywhere: `Authorization: Bearer ds_live_...`. ## Lifecycle `queued → running → completed | failed | cancelled` (with `cancelling` between a cancel request and its terminal state). The enum is closed — build exhaustive matches safely. - **completed** — results persisted with citations; `run.completed` webhook fires. - **failed** — `error.code`/`error.message` on the resource; the run charge is automatically refunded; `run.failed` webhook fires. Don't blindly re-create — read the message first. - **cancelled** — partial results may exist; the charge stands (work was done). ## Cost & latency A run bills prepaid credits when it starts (typically $10 standard / $20 max depth — see [pricing](/pricing), or `/billing` for your own account) and takes **15-60 minutes**. **One run researches one topic area** and fills your Blueprint with what it finds. How many rows that yields depends on the area, so don't budget from an entity count — price the specific run: ```bash curl -s -X POST $BASE_URL/v1/research/runs/estimate \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"query": "solid-state battery startups in Japan", "depth": "standard"}' ``` Your **first real run is free** (one per account, before you subscribe) and isn't consumed if it fails. Failed runs are refunded automatically; cancelled runs are charged (work was done). Spending is capped by the prepaid balance and any per-key budget — an agent can hit `402`, never an invoice. ## Top up a report instead of re-running it If you already have a report and want to extend it, pass `additive_to` rather than starting a fresh run. Entities that report already holds with in-TTL evidence are dropped before the run is batched, so **you pay for the gap, not the list** — re-sending 20 held seeds plus 6 new ones costs about the 6. There is no cap on how many seeds you send: price scales with the list, so a longer one simply costs more and you approve it first. ```bash curl -s -X POST $BASE_URL/v1/research/runs \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"seeds": ["newco.example", "otherco.example"], "additive_to": ""}' ``` This is the right shape for a recurring loop — a quarterly refresh, a watchlist you keep topping up. Without it, re-sending the full list starts a new run that re-researches, and re-charges for, everything you already had. `404` means that id isn't a run in your workspace. **Price it first.** `additive_to` works on `/runs/estimate` too, and it narrows the list exactly the way the charge does — so the quote is the gap, not the list you sent: ```bash curl -s -X POST $BASE_URL/v1/research/runs/estimate \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"seeds": ["newco.example", "otherco.example"], "additive_to": ""}' ``` The response carries `skipped_already_held` when anything was dropped — that is why the number is lower than the list you sent, and worth logging so a surprise looks like an explanation rather than a bug. ## Reuse what we already have If the workspace already holds current data on some of your seeds, you can keep it instead of paying to research those entities again: ```bash curl -s -X POST $BASE_URL/v1/research/runs \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"seeds": ["acme.example", "newco.example"], "reuse_existing": true}' ``` Entities we already cover with **in-TTL** evidence are carried into the new report as they stand, with their sources, and cost no run credit. Anything stale is researched again regardless — reuse never passes off out-of-date data as current. The skipped entities come back as `reused_existing` so a cheaper run than your seed list implies is explained rather than mysterious. This is the lineage-free sibling of `additive_to`: use `reuse_existing` when you just want whatever we have, and `additive_to` when you specifically mean "extend that report". ## Depth `"depth": "standard"` (default) or `"max"` — max lifts research budgets and uses the deeper agent preview; it costs more per run. ## Monitoring `"monitored": true` (default) keeps the run's target re-verified after the initial research, on a paid plan: every report is covered for its first 30 days, then while monitoring stays on. That is the "living dataset". Your plan keeps a set number of units fresh at no charge; each unit beyond that allowance bills per day (see [pricing](/pricing) for your plan's rate, or `/billing` for your account). **The unit is a DOSSIER, not a report** — one Deep Research call's returned data. A 20-entity request is researched as 4 dossiers, so it is 4 run credits AND 4 monitoring units, even though it reads as one report. Budget for the second number as well as the first: it is the recurring one. When a monitored value is re-verified and **changes**, a `dataset.updated` webhook fires with the record/column/evidence ids. Toggle per target later in the app. ### See everything you're paying to monitor An agent that creates reports on a schedule is the caller most likely to accumulate overlapping monitoring: re-researching a watchlist into a fresh report, rather than topping up the old one, is the natural loop — and it silently doubles the recurring cost. `GET /v1/research/monitoring` is how you audit that in one call. ```bash curl -s $BASE_URL/v1/research/monitoring \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json { "object": "monitoring_overview", "monitored_units": 12, "billing_units": 8, "duplicated_units": 4, "reports": [ {"run_id": "…", "label": "EU vendors", "units": 4, "entities": 20, "in_free_window": false, "free_until": "2026-07-30", "billing": true, "fully_covered_by": [""]}, {"run_id": "…", "label": "Watchlist", "units": 4, "entities": 18, "in_free_window": true, "free_until": "2026-09-04", "billing": false, "fully_covered_by": []} ], "warnings": [{"level": "INFO", "code": "monitoring_fully_duplicated", "…": "…"}] } ``` Three things worth wiring into a scheduled job: - **`in_free_window` and `billing`** are separate flags, not one status. The first means the report is inside its free 30 days and costs nothing yet (`free_until` is the date that ends); the second means it is past that and spending `units` daily. The word "monitored" hides that difference, and it is the difference that shows up on an invoice next month. - **`fully_covered_by`** lists reports whose in-TTL entity set is a *superset* of this one's — everything here is already kept fresh there, so these units buy nothing. Only reported for reports actually `billing`. - **`billing_units` vs `monitored_units`** is what you are charged for versus what you are watching. `duplicated_units` is the gap you could reclaim. Advisory only: `warnings` carries `INFO` entries and nothing is turned off on your behalf. Deciding which report keeps the coverage is a judgement call about which one your pipeline reads from, and we won't guess it. ## Attribution & budgets Runs created with an API key record that key (`api_key_id`) for audit and budget purposes. Keys can carry `budget_runs`/`budget_cents` ceilings — exceeding one returns `402 budget_exceeded`. ## Questions about a finished run Ask the research agent directly — see [Chat with the research agent](/developers/chat). Your questions appear in the customer's own chat history for that report, attributed to your API key. --- # Webhooks Register an endpoint and DeepSieve POSTs you events instead of making you poll: ```bash curl -s -X POST $BASE_URL/v1/webhooks \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://your-app.example.com/hooks/deepsieve", "events": ["run.completed", "run.failed", "dataset.updated"]}' ``` The response includes `"secret": "whsec_..."` — shown once; use it to verify signatures. Endpoints must be public HTTPS; up to 10 active endpoints per workspace. ## Events | Event | When | |---|---| | `run.completed` / `run.failed` / `run.cancelled` | a research run reaches a terminal state | | `dataset.updated` | a monitored value was re-verified and **changed** (payload: table, record_id, column, new + superseded evidence ids) | ## Verify signatures (Standard Webhooks) Headers on every delivery: `webhook-id`, `webhook-timestamp` (unix seconds), `webhook-signature` (`v1,` over `"{id}.{timestamp}.{body}"`). Any Standard Webhooks library works, or by hand: ```python import base64, hashlib, hmac, time def verify(secret: str, headers: dict, body: bytes) -> bool: ts = headers["webhook-timestamp"] if abs(time.time() - int(ts)) > 300: # ±5 min tolerance return False msg = f"{headers['webhook-id']}.{ts}.".encode() + body mac = hmac.new(secret.encode(), msg, hashlib.sha256) expected = "v1," + base64.b64encode(mac.digest()).decode() return hmac.compare_digest(expected, headers["webhook-signature"]) ``` ```typescript import crypto from "node:crypto"; function verify(secret: string, headers: Record, body: string): boolean { const ts = headers["webhook-timestamp"]; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const mac = crypto.createHmac("sha256", secret) .update(`${headers["webhook-id"]}.${ts}.${body}`).digest("base64"); return crypto.timingSafeEqual(Buffer.from(`v1,${mac}`), Buffer.from(headers["webhook-signature"])); } ``` ## Delivery & retries 2xx acknowledges. Anything else retries with backoff: 1m, 5m, 30m, 2h, 12h, then the delivery is dropped. `webhook-id` is stable across retries — use it to deduplicate. Test with `POST /v1/webhooks/{id}/ping`. Manage with `GET`/`PATCH`/`DELETE /v1/webhooks/{id}` (PATCH keeps the secret; `DELETE` disables). Rotate a leaked secret with `POST /v1/webhooks/{id}/rotate` — it returns a new `whsec_` once and the old secret stops signing **immediately**, so re-point your verifier first. Bring a disabled endpoint back with `POST /v1/webhooks/{id}/enable` (the secret is unchanged). --- # Dataset sync `GET /v1/blueprints/{blueprint_id}/entities/{entity_key}` is the canonical read: the **cross-run, canonical rows** of one entity, with citations. Entity keys come from `GET /v1/blueprints/{blueprint_id}`, and the ids from `GET /v1/blueprints` ([discover, don't guess](/developers/blueprints-and-data)). ```bash curl -s "$BASE_URL/v1/blueprints/$BP/entities/companies?limit=50&receipts=true" \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json-response {"data": [{"id": "…", "updated_at": "2026-08-03T11:02:44+00:00", "name": "Acme KK", "hq_city": "Osaka", "citations": {"hq_city": {"value": "Osaka", "confidence": 0.93, "evidence_id": "…", "source_urls": ["https://…"], "retrieved_at": "2026-08-01T…"}}}], "has_more": true, "next_cursor": "MjAyNi0wOC0wMy4uLg", "entity": {"key": "companies", "columns": [{"name": "name", "type": "text"}]}, "blueprint": {"domain_name": "…", "entities": ["companies"]}} ``` Parameters: - `limit` (≤200) + `cursor` — keyset pagination ordered by `(updated_at, id)` ascending. Stable while rows are inserted. - `updated_since=` — **the sync primitive**: only rows changed since your last sync. Store your high-water mark (or the final cursor) and poll on your own schedule — or let the `dataset.updated` webhook tell you when. - `fields=name,hq_city` — column subset. - `receipts=true` — nest per-cell citations. This is the differentiator; use it. ## Reading a citation honestly Each cell carries the same verdict the app shows, not just a number: | Field | Meaning | |---|---| | `verdict` | `supported` · `leans-supported` · `uncertain` · `corrected` · `contradicted` · `unverifiable` · `not checked` | | `confidence` | 0–1 float, or `null` when the cell was never graded | | `cited` | `false` when the cell has no source URL — never present it as sourced | | `verdict_note` | the QA agent's short justification | | `source_urls`, `evidence_id`, `retrieved_at` | provenance | A `contradicted` or `cited: false` cell is a signal to surface to a human, not a fact to act on. ## Preview caps (unsubscribed workspaces) Free-preview workspaces receive only the first 10 rows per entity. The response says so explicitly — `"truncated": true`, `"preview_row_cap": 10`, plus `has_more: true` — and paging past the cap requires a subscription. **Never report a truncated preview as the complete dataset.** ## Bulk export `GET /v1/export?format=ndjson|json|csv[&entity=][&workspace_id=]` — NDJSON is the agent default: line 1 is a metadata record with the Blueprint schema, then one record per line with citations. CSV is per-entity and **drops citations**. `workspace_id` exports a specific Blueprint by id (leaves the active workspace unchanged); omitted, it exports the active one. Free-preview (unsubscribed) workspaces are capped to the first rows of each table on both surfaces — flagged by `truncated` + `preview_row_cap`. --- # Global Reports A **Global Report** is a named, user- (or agent-) curated view over the canonical dataset: one tab per entity with an ordered column selection. The report stores only its definition — every read recomputes fresh rows from the latest data (with a short server-side cache), so a report is always current. Auth as everywhere: `Authorization: Bearer ds_live_...`. Reads need `reports:read`, writes `reports:write` (both in the Agent preset). Deleting a report deletes a view definition — never researched data. ## Discover the column vocabulary first ```bash curl -s $BASE_URL/v1/reports/columns -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` Returns the selectable columns per entity (own columns + relationship pill columns). Like everything Blueprint-shaped: discover, don't guess. ## Create ```bash curl -s -X POST $BASE_URL/v1/reports \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"name": "Vendor shortlist", "config": {"tabs": [{"id": "t1", "name": "Companies", "entity_key": "companies", "columns": ["name", "hq_city", "funding_total"]}]}}' ``` Invalid entities/columns are rejected with a typed 400 pointing back at `/v1/reports/columns`. ## Read, update, delete ```bash curl -s $BASE_URL/v1/reports -H "Authorization: Bearer $DEEPSIEVE_API_KEY" # list curl -s $BASE_URL/v1/reports/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" # definition curl -s -X PATCH $BASE_URL/v1/reports/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" \ -H "Content-Type: application/json" -d '{"name": "Renamed"}' # update curl -s -X DELETE $BASE_URL/v1/reports/{id} -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ## Read a tab's data ```bash curl -s $BASE_URL/v1/reports/{id}/tabs/{tab_id}/data \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` Fresh cross-run rows for that tab's entity, with the same per-cell evidence the UI shows. (The full column payload is returned; the tab's column selection defines the curated view in the app and its exports.) Free-preview workspaces are row-capped here exactly as on every other read surface. For raw entity access with sync cursors, use [Dataset sync](/developers/dataset-sync) instead — reports are for curated, shareable views. --- # Chat with the research agent The dataset is the deliverable, but the research agent is what can *explain* or *correct* a cell. When your user asks "why does this say $2.4B?" or "that funding number looks stale", you don't have to guess — ask. Needs `runs:write` to send and `runs:read` to read (both in the Agent preset). ## Ask a question ```bash curl -s -X POST $BASE_URL/v1/research/runs/{run_id}/chat/messages \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" -H "Content-Type: application/json" \ -d '{"content": "Where did the 2024 funding figure come from? Please re-check it.", "row_ref": {"table_name": "companies", "record_id": ""}}' ``` ```json-response {"object": "chat_message", "message_id": "…", "status": "running", "note": "Your message is visible in the customer's own chat history for this report, attributed to this API key."} ``` `row_ref` is optional — omit it to ask about the report as a whole. **Chat requires an active plan.** A free run previews its first ten rows, and the agent answers from the whole dossier — so this endpoint is `402` on an account without one, including a closed account inside its read-retention year. The refusal happens before any work is started, so it costs nothing and is safe to retry after subscribing. It is not retriable on its own: treat it like the other `402`s and surface it to a human. ## Read the thread ```bash curl -s $BASE_URL/v1/research/runs/{run_id}/chat \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ```json-response {"object": "chat_thread", "in_flight": false, "messages": [ {"role": "user", "content": "Where did the 2024 funding figure come from?", "sent_via": "api", "sent_by_key": "claude-code", "status": "complete"}, {"role": "agent", "content": "The $2.4B figure comes from…", "status": "complete", "actions": [ {"id": "…", "type": "reverify", "summary": "Re-verifying total_funding"}]} ]} ``` Poll while `in_flight` is true (a turn takes seconds, not minutes). Answers that changed data list them under `actions` — each revertible: ```bash curl -s -X POST $BASE_URL/v1/chat/actions/{action_id}/revert \ -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ## Your messages are visible, and attributed This is one shared thread, not a private side channel. Everything you send appears in the customer's own Chat view for that report, labelled **"Sent by your agent"** with the API key's name — so a person opening their report can always tell which questions they asked and which their agent did. Two things follow for you: - **Write for a human audience.** Your questions are read by the customer. - **Don't narrate.** Ask when you need an answer or a correction; this isn't a logging channel. Data changes an answer applies are audited and revertible by either side, and the research agent can never edit the active Blueprint — same boundary as everywhere else. --- # The API Yes, there's an API. DeepSieve is a **REST API** first; the MCP server, SDK, and CLI are all conveniences layered on top of it. Everything you can do in the app — define a schema, start research, read a cited dataset, get notified when it changes — you can do over plain HTTP. If you'd rather not read this page, point your coding agent at [https://deepsieve.ai/setup.md](https://deepsieve.ai/setup.md) and it wires the whole thing up. If you're choosing *how* to connect, start at [Choosing an integration](/developers/integrations). This page is the map of *what there is to connect to*. ## The shape of it You work with a handful of components, each with its own page below. The usual arc: 1. **Authenticate** — mint a key or log in through the browser. 2. Define a **Blueprint** (your schema) once. 3. Start **research runs** from natural-language queries. 4. Read the **dataset** back — cited, and re-verified while monitoring is on. 5. Have DeepSieve **push** changes to you (webhooks) instead of polling, and **ask the research agent** about any value. ```bash # The one call that proves you're connected: curl -s https://deepsieve.ai/v1/me -H "Authorization: Bearer $DEEPSIEVE_API_KEY" ``` ## API components Each of these is a first-class part of the API. Follow the link for the endpoints, request/response shapes, and the honest caveats. - **[Blueprints & your schema](/developers/blueprints-and-data)** — the schema that defines your entities, columns, and how the data reads back. You may *create* a new Blueprint over the API; the active one is edited only by a human. - **[Research runs](/developers/runs)** — start a run from a query, poll it to completion, and understand what a run costs. Test free with `dry_run` first. - **[Dataset sync](/developers/dataset-sync)** — read the cited dataset, page through it, and read a citation's `verdict` honestly before repeating a value as fact. - **[Webhooks](/developers/webhooks)** — get pushed an event when a run finishes or a cell changes, instead of polling. Signed and replayable. - **[Global Reports](/developers/reports)** — the saved, shareable views over your dataset. - **[Chat with the research agent](/developers/chat)** — ask about a value, or get a specific cell re-checked, and read the answer back programmatically. ## Reference - **[Authentication & API keys](/developers/authentication)** — browser login vs. `ds_live_...` keys, scopes, and the Agent preset. - **[Errors](/developers/errors)** — every failure is `{error: {code, message, retriable, request_id}}`; `retriable=false` means do not retry. - **[Rate limits & budgets](/developers/rate-limits)** — the limits, and how prepaid credits gate real runs. - **[Versioning & stability](/developers/versioning)** — what `/v1` promises now (announced breaking changes) and at GA (additive-only). ## Machine-readable Everything above is also available in forms an agent can consume directly: - **[/openapi.json](/openapi.json)** — the full machine-readable contract; the Python [SDK](/developers/sdks) is generated from it. - **[/llms.txt](/llms.txt)** · **[/llms-full.txt](/llms-full.txt)** — the docs, indexed and concatenated for an agent. - Any page here is markdown if you append **`.md`** to its URL. --- # Choosing an integration There are four ways to connect DeepSieve, and they are not competing options — they answer different questions. Most people end up using two. If you don't want to think about it: paste the [setup prompt](/developers) into your agent and it picks for you. | You want to… | Use | Auth | |---|---|---| | Give your coding assistant live access to your data | **MCP server** | Browser login | | Teach your assistant to drive the API *well* | **Agent Skill** | — (knowledge only) | | Work from a terminal, or script it in a shell | **CLI** | Browser login | | Have your own application call DeepSieve | **SDK** or **REST** | API key | | Run it in CI, a container, or a cron job | **CLI**, **MCP (stdio)**, or **REST** | API key | ## MCP server — live access from your editor The [MCP server](/developers/mcp) puts DeepSieve tools directly in your coding assistant: it can list your runs, read your cited dataset, start a research run, and ask the research agent questions — without you writing any integration code. Two transports, same tools: - **Remote (HTTP)** — `https://deepsieve.ai/mcp`, authenticated by **browser login**. No API key exists to leak. This is the default and what the setup prompt uses. - **stdio** — a local process (`uvx deepsieve-mcp`) authenticated with an API key. For CI, containers, and headless environments where nobody can click a consent screen. The server is a schema-bound translator: your assistant sees typed tools, not raw HTTP, and never handles your credentials directly. **Reach for it when** you're working *in* an editor or terminal assistant and want your data at hand. **Don't** build a production data pipeline on it — use the SDK or REST, which have stable contracts and webhooks. ## Agent Skill — knowing *how* to use it The [skill](https://agentskills.io) is procedural knowledge, not access. It teaches an assistant the things that make the difference between a working integration and a plausible-looking broken one: discover the schema instead of guessing column names, test with `dry_run` before spending credits, read a citation's `verdict` before repeating a value as fact. ```bash npx skills add https://deepsieve.ai ``` **Reach for it** alongside either MCP or REST — it's additive. The setup prompt installs it and also saves a short rules file, so the hazards stay loaded in every session while the detailed procedures load on demand. ## CLI — the operator's tool The [`deepsieve` CLI](/developers/cli) is what you reach for when you're already in a terminal: check a run, pull today's rows into a file, wire something into a cron job. It signs in through your browser (device flow) and stores a scoped, revocable key. ```bash uv tool install deepsieve-cli deepsieve data get companies --receipts --json | jq '.data[0]' ``` It's also the pragmatic choice for a **terminal-resident agent**: `--json` on every command, stable exit codes, and shell pipelines it can compose. Where MCP gives an agent typed tools, the CLI gives it a shell it already knows. **Reach for it when** you're at a prompt or writing a script. **Don't** build an application on it — shelling out from code is worse than the SDK in every way that matters (types, errors, retries). ## SDK — your application calling DeepSieve The [Python SDK](/developers/sdks) is generated from our OpenAPI contract, so it carries typed models, retries, and pagination helpers — and CI fails any change that leaves it out of step with the contract. The honest caveat: that guard keeps the SDK matching the *contract*, not automatically the *implementation*. When the two differ, the API is the truth — tell us, because that's a bug on our side. ```bash pip install deepsieve ``` **Reach for it when** you are writing software that talks to DeepSieve — a backend service, a scheduled sync, a data pipeline. **Don't** use it to give an assistant ad-hoc access; that's MCP's job. ## REST — everything else The [HTTP API](/developers/first-run) is the substrate all of the above sit on. It's versioned, and becomes additive-only at GA — until then breaking changes are announced in the [changelog](/developers/changelog). See [Versioning & stability](/developers/versioning). For the whole surface at a glance — one link per component — see [The API](/developers/api); every endpoint is in [/openapi.json](/openapi.json), and every docs page has a markdown mirror for agents (append `.md` to any URL). **Reach for it when** you're in a language we don't ship an SDK for, or you want zero dependencies. ## How the pieces fit - **[/llms.txt](/llms.txt)** tells an agent this API exists and how to find its way around. - **MCP tool schemas** tell it which operations are available and what they take. - **The skill and rules file** tell it how to combine those operations without wasting your money or overstating a finding. Access, vocabulary, and judgment — you generally want all three. --- # MCP server The MCP server gives any MCP client outcome-shaped tools over the DeepSieve API. There are two ways to connect, and for a human at their editor the first is easier — no key to create, copy, or store. **Easiest of all:** let your agent configure itself — paste `Set up DeepSieve by following the instructions here: https://deepsieve.ai/setup.md` into it and skip this page. See the [Quickstart](/developers). ## Browser login (recommended) ```bash claude mcp add --transport http deepsieve https://deepsieve.ai/mcp ``` Your client opens a browser, you sign in with the DeepSieve account you already have, and it stores a short-lived token in your OS keychain — nothing lands in a config file, and access is revocable per client. This is OAuth 2.1 (the MCP 2026 spec) with **DeepSieve's own login screen** doing the consent. Cursor, VS Code / Copilot, Codex and others follow the same `--transport http` shape. > Requires a DeepSieve deployment with MCP OAuth configured. On a deployment > that hasn't enabled it, use the API-key path below. ## CI, containers, and other headless environments No browser? Use the stdio server with a scoped API key — the right choice for pipelines and services, where OAuth's browser step can't run. Installs from PyPI (`uvx deepsieve-mcp`, no repo checkout). Create a key at [Settings → API keys](/settings/api-keys) with the **Agent** preset — it runs research and reads results but can't touch your schema or billing. ## Claude Code ```bash claude mcp add deepsieve \ --env DEEPSIEVE_API_KEY=ds_live_... \ --env DEEPSIEVE_API_URL=$BASE_URL \ -- uvx deepsieve-mcp ``` ## Cursor / Claude Desktop (manual JSON) ```json { "mcpServers": { "deepsieve": { "command": "uvx", "args": ["deepsieve-mcp"], "env": {"DEEPSIEVE_API_KEY": "ds_live_...", "DEEPSIEVE_API_URL": "https://deepsieve.ai"} } } } ``` `DEEPSIEVE_API_URL` defaults to `http://localhost:8200` (local dev stack). Two transports, both shipped: the **hosted Streamable-HTTP server with OAuth 2.1** (the browser-login path at the top of this page — no key in a config file, revocable per client) and the **local stdio** server shown here, which is the spec-recommended path when you want key-based auth or an air-gapped setup. --- # SDKs ## Python ```bash pip install deepsieve ``` Fully typed, sync + async, generated from the /v1 contract: ```python from deepsieve import SDK with SDK(bearer_auth="ds_live_...") as sdk: me = sdk.v1_meta.v1_meta_get_me().result # Free test run — exercises the real poll loop run = sdk.v1_research.v1_research_create_run(query="...", dry_run=True).result # Discover the schema, then read cited data catalog = sdk.v1_data.v1_data_data_catalog().result page = sdk.v1_data.v1_data_read_entity_data( entity_key=catalog.entities[0].key, receipts=True ).result ``` Every call returns `.result` (typed models) plus `.headers` (RateLimit-*, X-Request-Id). Errors raise typed exceptions carrying the same `{code, retriable, request_id}` envelope the REST API returns. The SDK is generated from [`/openapi.json`](/openapi.json)'s `/v1` subset and regenerated whenever the contract changes, and CI fails a PR that leaves the two out of step. That keeps the SDK matching the CONTRACT; where the contract and the running API disagree, the API is the truth and the gap is our bug. ## TypeScript Next up, generated from the same source. Until then the REST API is deliberately easy to consume raw — every list is `{data, has_more, next_cursor}`, every error is typed — and your coding assistant can build a thin client from the [Quickstart](/developers) prompt block in one shot. --- # CLI The `deepsieve` CLI is for humans at a terminal and for agents that live in one. It signs in through your browser, so there is no key to copy, and nothing sensitive lands in your shell history. ```bash uv tool install deepsieve-cli deepsieve login ``` (or `pipx install deepsieve-cli`. For a one-off without installing: `uvx --from deepsieve-cli deepsieve login` — but the later `deepsieve …` commands need it on your PATH.) You'll see a short code and a browser window: ```text Your code: BUPP-W4ZQ Open: https://deepsieve.ai/cli-login?code=BUPP-W4ZQ Waiting for approval… ``` Approve it and you're signed in. Behind the scenes this is the OAuth device flow ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) — the same shape as `gh auth login`. What it grants you is an ordinary scoped API key, so it shows up in [Settings → API keys](/settings/api-keys) and you can revoke it there whenever you like. ## The commands you'll use ```bash deepsieve whoami # identity, workspace, scopes deepsieve data catalog # your entities + columns deepsieve data get companies --receipts # rows with per-cell citations deepsieve runs create --query "..." --dry-run # free, ~15s, nothing persisted deepsieve runs create --query "..." --wait # real run: spends credits deepsieve runs list deepsieve runs get deepsieve runs cancel ``` Add `--json` to any command for machine-readable output — that's the mode to use when a script or an agent is reading it. ## Test before you spend `--dry-run` behaves exactly like a real run (same statuses, same row shape, `"test": true`) but finishes in about 15 seconds and costs nothing. Build your pipeline against it first: ```bash deepsieve runs create --query "anything" --dry-run --wait --json ``` A real run spends credits and takes 15–60 minutes, so the CLI asks you to confirm — and in a non-interactive shell it refuses outright unless you pass `--yes`. That's deliberate: a script shouldn't be able to spend your balance by surprise. ## Several deployments at once Profiles keep each deployment's origin and credential separate, so staging can never quietly become production: ```bash deepsieve login --profile staging --origin https://deepsieve.your-company.example deepsieve --profile staging runs list ``` `DEEPSIEVE_PROFILE` sets the default for a shell. ## CI and containers Skip `login`. Set `DEEPSIEVE_API_KEY` (and `DEEPSIEVE_BASE_URL` unless you're on production) and the CLI uses it directly — environment variables always win over a stored profile, so a pipeline can't accidentally pick up a developer's login. ```bash DEEPSIEVE_API_KEY=ds_live_... deepsieve --json data get companies ``` Create that key at [Settings → API keys](/settings/api-keys) with the **Agent** preset. ## Exit codes Stable, so shell scripts can branch on them: | Code | Meaning | |---|---| | `0` | success | | `1` | failure | | `2` | usage error | | `3` | not authenticated | | `4` | run still in progress | ## What it won't do The CLI can't touch billing, members, API keys, or edit an active Blueprint. Its credential is ceilinged at the **Agent** scope preset, so that isn't just a missing subcommand — the server refuses too. Schema changes and spending settings belong to a human in the app. ## Connect your agent while you're here ```bash deepsieve setup mcp ``` prints the MCP registration for whichever deployment the current profile points at. For the full agent setup — MCP, skills, and a rules file — use the [one-prompt quickstart](/developers) instead. --- # Errors 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) | Code | HTTP | Retriable | Meaning / correct response | |---|---|---|---| | `invalid_request` | 400 | no | malformed input — fix the request | | `validation_failed` | 422 | no | body failed validation; `error.errors` lists fields | | `authentication_required` / `invalid_api_key` | 401 | no | missing/bad key | | `key_revoked` / `key_expired` | 401 | no | a key we recognise that no longer works — mint or rotate a new one at `/settings/api-keys` | | `key_suspended` / `account_suspended` | 403 | no | the 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_role` / `insufficient_scope` | 403 | no | key lacks the scope — mint one that has it | | `workspace_forbidden` | 403 | no | key can't act in that workspace | | `not_found` | 404 | no | unknown resource/entity | | `conflict` | 409 | no | state conflict (e.g. rotating a revoked key) | | `no_active_blueprint` | 409 | no | the workspace holds no Blueprint yet — design one at `/onboarding` or with `POST /v1/blueprints/drafts`. Returned by every endpoint that reads the workspace's schema: export, reports, report columns, and run listing | | `idempotency_key_in_flight` | 409 | yes | original still running — wait `Retry-After` | | `idempotency_key_reused` | 422 | no | same key, different payload — new key or same body | | `rate_limited` | 429 | yes | back off per `Retry-After` | | `llm_daily_limit_exceeded` | 429 | yes | the 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_exceeded` | 429 | yes | your org's own run caps (set by your admin) — queue and retry | | `insufficient_credits` | 402 | no | balance too low — a human must top up | | `plan_upgrade_required` | 402 | no | the plan doesn't include this capability — **topping up won't help**; a human must upgrade | | `budget_exceeded` | 402 | no | this key's budget ceiling — a human must raise it | | `payment_required` | 402 | no | subscription required for this surface | | `authorization_pending` | 400 | yes | CLI sign-in not approved yet — keep polling at `interval` | | `slow_down` | 400 | yes | polling faster than `interval` — increase it, then continue | | `expired_token` | 400 | no | the device code expired or was already used — run `deepsieve login` again | | `access_denied` | 403 | no | the human refused the CLI sign-in | | `internal_error` | 500 | yes | our fault; retry with backoff | | `service_unavailable` | 503 | yes | transient; retry with backoff | The four device codes belong to the CLI sign-in flow ([CLI](/developers/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. --- # Rate limits & budgets ## Request rate /v1 requests are limited per caller (per API key). Every response carries: ```text RateLimit-Limit: 120 RateLimit-Remaining: 87 RateLimit-Reset: 17 ``` Hitting the limit returns `429 rate_limited` (retriable) with `Retry-After`. Polling a run every 15s uses ~4 requests/minute — the limit exists to stop runaway loops, not normal integrations. ## Model-call limits on the free surfaces (abuse guard) Some endpoints make a model call that is **not** a paid run: the intake and guidance parsers, run preflight, Blueprint onboarding infer/adjust, and report chat. These are metered per caller separately from the request rate above, so a stuck loop cannot burn tokens on work nobody is paying for: | surface | per minute | per day | on refusal | |---|---|---|---| | parsers, preflight, onboarding, chat | 30 | 500 (shared) | `429 rate_limited` / `429 llm_daily_limit_exceeded` | | `POST /v1/research/runs` and `/estimate` with a `query` | **120** (matches the request limit) | 500 (shared) | `429 rate_limited` | Founders and Enterprise plans get 3× these figures. `llm_daily_limit_exceeded` is a **rate limit, not a billing wall** — topping up does not lift it; retry after the rolling 24h window or ask support to raise it. Sending `seeds` instead of `query` skips the parse and is not subject to this table. **Auto-suspend.** A key that keeps hitting refusals — 20 blocked calls, or 5 `unparseable` / off-purpose verdicts, within 10 minutes — is paused: `403 key_suspended`, an audit entry, and an email to the org owner. Two paused keys in 24h pause the owning user (`403 account_suspended`; `suspended` on `/v1/me` says so up front). **Stop and alert a human; do not mint a new key** — a fresh key of a paused user is refused the same way. The `query` parse on run create/estimate is the one exception to the blocked-call count: a successful parse is charged as a run, so being *fast* there never pauses a key — only repeated `unparseable` verdicts do. ## Run concurrency & credits Separate from request rates, run creation can return: - `429 run_concurrency_exceeded` — your ORG's run caps, set by your own admin (`/api/billing/limits`). Queue and retry. - `402 insufficient_credits` — prepaid balance too low; a human must top up. - `402 plan_upgrade_required` — the plan does not include this capability (e.g. Max-depth runs, webhooks, the audit log). **Topping up will not help**; a human must upgrade. Never retry this one. - `402 budget_exceeded` — this key's own `budget_runs`/`budget_cents` ceiling (set in the create-key dialog or via `POST /v1/keys`). The three codes are distinct because the correct agent response differs: back off / queue / **stop and alert a human**. --- # Versioning & stability ## Before GA DeepSieve has not reached general availability, and **the stability guarantee below begins at GA — not today.** Until then `/v1` can change in ways that break a client, and this page says so rather than promising otherwise. What we commit to in the meantime: - **Every breaking change is announced in the [changelog](/developers/changelog)**, dated, naming the old path or field and what replaces it. That is the mechanism — if it is not in the changelog, it did not happen. - **Breaking changes are deliberate**, weighed one at a time, never incidental. Pre-GA is not a licence to churn the contract; it is an acknowledgement that a surface still being shaped should not be described as finished. - Our own clients — the [CLI](/developers/cli), the MCP server, and the Python SDK — are updated in the same change, so the changelog entry can tell you the version of each that works. If you are building on `/v1` now, pin the SDK version and read the changelog before upgrading. Tell us what you have built: it is the only way a change that would break you gets weighed against you specifically. ## At GA `/v1` becomes **additive-only**: - We may add endpoints, optional request fields, response fields, and error codes. We will not remove or rename fields, change types, or change semantics within v1. - **Clients must ignore unknown response fields.** (True today too — it is what lets an addition stay non-breaking.) - Run `status` and error `code` registries are closed sets; changes are announced ahead of time in the changelog. - Deprecations are signaled with `Deprecation` and `Sunset` headers at least 90 days before removal; removal only happens at a major version. Experimental surfaces ship behind an opt-in `DeepSieve-Beta` header and carry no stability promise until graduated — before or after GA. The legacy `/api/*` routes are the web app's private surface — no stability promise; don't build on them. --- # Changelog ## 2026-09-16 — the cost estimate now accounts for the free run **`affordable` changes value for trial accounts.** If you branch on it, read this. `POST /v1/research/runs/estimate` priced a run against plan credits and prepaid dollars only. An unsubscribed org with its one free run unspent was therefore quoted `affordable: false` with a `shortfall_cents` — and then `POST /v1/research/runs` succeeded and charged nothing, because the free run funds it. An autonomous caller obeying the estimate halted before reaching a run it could in fact have made. | Was | Now | |---|---| | `affordable: false`, `shortfall_cents: 1000` | `affordable: true`, `shortfall_cents: 0` | ### New on the estimate `uses_free_run` — whether THIS request would consume the org's one free run. **`run_price_cents` still quotes the dollar price**, exactly as it does for a credit-covered run, so never infer funding from the price: `uses_free_run` is the discriminator. `null` means eligibility could not be determined (billing inactive, or the read failed) and never "not free" — when it is null, `affordable` and `shortfall_cents` are null too, which is unknown, not unaffordable. It is `false` for `depth: max`. The free run covers a **Standard** run; a Max request is refused before the free run is ever considered, and `allowed` carries that refusal. `free_run` — present only when the free run's entity cap would trim THIS request. A free run researches at most one interaction's worth of entities and **drops the rest** rather than refusing, so the block names `entity_cap`, `requested`, `researched` and `dropped`. The run response returns the same block and is authoritative: `dropped` here is computed from the seeds as sent, while the run narrows first (`reuse_existing`, `additive_to`). ### New on `GET /v1/me` `free_run_available` — is the org's one free run still unspent, so you can check trial eligibility before committing to anything. Same nullability rule: `null` is "could not determine", never "no free run". ## 2026-09-08 — breaking, before GA **Proposing a Blueprint change now names the Blueprint.** | Was | Now | |---|---| | `POST /v1/blueprints/propose` | `POST /v1/blueprints/{blueprint_id}/propose` | **If you change nothing, this call returns `404`.** No alias is kept. Get a `blueprint_id` from **`GET /v1/blueprints`**. The old path proposed against whichever Blueprint happened to be *active* — a choice you never made and the response never named. With two Blueprints in one account, it could return a completely coherent proposal about the wrong one. This is not only a path change. The id now reaches everything the answer depends on: the Blueprint the proposal is built from, the Blueprint the `tier` is classified against, and the live tables checked to decide whether a change would lose data. A path that took an id and ignored it would have been worse than the old one, because the id would have implied a control it did not have. ### Also in the response `blueprint_id` — the Blueprint this proposal is about, echoed in the form `GET /v1/blueprints` uses. It is always present. **Name that Blueprint when you hand the change to a person.** The `{app_url}/blueprint?ask=…` link opens *their* active Blueprint, which is not necessarily the one you addressed, and their chat re-derives the proposal from your text alone. If those differ, the re-derivation succeeds against the wrong Blueprint — no error. There is no flag to check instead: your active workspace comes from your API key, theirs from their own switcher, and nothing on this API reports the second. ### Refusals Addressing a Blueprint explicitly does not widen what you can reach. An id you could not already read refuses exactly as the other by-id reads do: `404` for unknown, malformed, or another organization's id (existence is never confirmed across organizations), `403` for a scoped viewer without a grant, and `409` for a Blueprint that is still a draft or has no tables yet. A refusal costs no model call. Applying is unchanged and still human-only. An agent may ask what a change would be; only a person may make it. ### Clients Upgrade to at least: | Client | Version | |---|---| | Python SDK (`deepsieve`) | 0.3.0 | | MCP server — the stdio package and browser login alike | 0.3.0 | The MCP tool `propose_blueprint_change` takes an optional `blueprint_id`; omit it and it resolves the active Blueprint as before. The CLI does not call this endpoint and is unaffected. ## 2026-09-07 — breaking, before GA **A dataset read now names the Blueprint it reads.** Two endpoints are removed and six are moved. This is a **breaking change before GA**, announced here as [Versioning & stability](/developers/versioning) requires: `/v1` becomes additive-only at GA, and until then a break is allowed only when it is deliberate and appears in this changelog. **If you change nothing, the calls below return `404`.** That is an ordinary not-found, so a client that only checks for an error will look like it is reading an empty dataset rather than calling a route that no longer exists. Check the path before you check the data. ### Removed | Was | Now | |---|---| | `GET /v1/data` | `GET /v1/blueprints/{blueprint_id}` | | `GET /v1/data/{entity_key}` | `GET /v1/blueprints/{blueprint_id}/entities/{entity_key}` | Same response shape, same query parameters (`limit`, `cursor`, `updated_since`, `fields`, `receipts`), same free-preview cap. One field differs: the catalog's discriminator is now `"object": "blueprint"` rather than `"object": "data_catalog"`. Get a `blueprint_id` from **`GET /v1/blueprints`**, which lists the Blueprints you can see and marks the active one. That listing is the reason for the change. `GET /v1/data` answered for whichever Blueprint happened to be *active* — a choice the caller never made and the response never named — so an account with two Blueprints could receive a completely coherent answer to a question it had not asked, with nothing in the payload to reveal which one it described. No alias is kept: a shim with no consumer reads as a supported path. `GET /v1/export` is unchanged, and still takes `?workspace_id=`. ### Moved `/v1/blueprints/onboarding/…` → `/v1/blueprints/drafts/…` — all six: the collection itself, `{session_id}`, and its `/answers`, `/adjust`, `/chat` and `/approve`. Path only. Same handlers, same request and response bodies, and the same `operationId`s, so **generated SDK method names do not change**. ### Clients Upgrade to at least: | Client | Version | |---|---| | Python SDK (`deepsieve`) | 0.2.0 | | CLI (`deepsieve`) | 0.2.0 | | MCP server — the stdio package and browser login alike | 0.2.0 | `deepsieve data catalog` and `deepsieve data get` resolve the active Blueprint for you, and take `--blueprint ` to name one instead. The MCP tools `get_blueprint` and `query_entities` take an optional `blueprint_id`, and a new `list_blueprints` tool supplies the ids — on both transports. ## 2026-08 (later) - **`ApiKey` gains `usage` on `GET /v1/keys`** — `{runs, est_spent_cents}`, or `null` if the aggregation could not be completed. The API has returned it since the per-key usage work; the contract never declared it, so generated clients could not read it. Additive — existing fields unchanged. **Scope worth knowing: it counts runs in your ACTIVE WORKSPACE.** Keys are workspace-bound at mint time and the list is org-wide, so a key pinned to a different workspace reports zero rather than its real totals. - **`CreateRunRequest.reuse_existing` was declared `string`; it is a `boolean`.** A pasted property body had silently overwritten it in the contract, so SDK 0.1.1 types it `Optional[str]` and `reuse_existing=True` does not type-check. Corrected — the next SDK release carries the right type. Two other properties had the same defect without reaching the SDK: `MonitoringView.freshness` (object, was resolving to `number`) and `StartRunResponse.reused_existing` (carried the wrong description). - **Freshness overage wording**: the per-day rate and `auto_topup_suggested` described a *report*; both are metered per **monitored run** — one per Deep Research dossier, so a report assembled from several dossiers accrues several. Wording only; no behaviour or price changed. - **CLI sign-in** — `POST /v1/auth/device/code` and `/v1/auth/device/token`: the OAuth device flow behind `deepsieve login`. Unauthenticated by design; uses RFC 8628's own vocabulary (`authorization_pending`, `slow_down`, `expired_token`, `access_denied`), and the two retriable codes are the normal path, not failures. - **New error codes**: the four above. `invalid_api_key` is now returned for a credential that was presented but not accepted — previously indistinguishable from `authentication_required`, which means no credential was sent at all. - **`Citation` gains `verdict`, `verdict_note` and `cited`.** The API already returned them; the contract did not declare them, so generated clients could not read a verdict. Additive — existing fields unchanged. SDK 0.1.1 carries them. - **MCP over browser login reaches tool parity with the stdio package** (19 tools). No tool was removed. ## 2026-08 - **Initial `/v1` release**: typed error envelope + request ids; scoped revocable API keys (`/v1/keys`); async research runs with `Idempotency-Key` and query-first creation; outbound signed webhooks (`run.*`, `dataset.updated`); cursor-paginated dataset read (`/v1/data`) with `updated_since` sync and per-cell citations; NDJSON/JSON/CSV export; RateLimit headers + per-key budgets; MCP server; `/llms.txt`.