Changelog

API changes, newest first

2026-10-25: client release: deepsieve 0.9.0, deepsieve-mcp and deepsieve-cli 0.11.0

This release carries the API changes the entries below added since deepsieve 0.8.0 and deepsieve-mcp and deepsieve-cli 0.10.0. Every addition is backward compatible.

PackageVersionWhat it adds
Python SDK (deepsieve)0.9.0SelectionState merged, with items (MergedItem) on a citation's, a conflict's and a choose reply's selection, and selection on each change-log entry (ChangeEntrySelection); expired_from_plan, expired_from_plan_label and retention_until on the subscription from v1_meta_get_me, v1_subscription_cancel and v1_subscription_resume; metrics.search_queries typed as nullable on a run
MCP server (deepsieve-mcp)0.11.0get_account tells a closed or expired account from a new one; get_run_status describes metrics.search_queries; get_cell_changes describes a merged selection
CLI (deepsieve-cli)0.11.0deepsieve whoami prints an account line for a closed or expired account

2026-10-25: a verdict checks the cited page, not how credible the source is

A cell's verdict from the quality check is meant to answer one question: does the page the research agent cited state this value, for this record (gh #3269)? It is a check for a value the agent may have invented or misread, not a rating of how credible the page is. supported means a page QA read states the value or strongly implies it. A page about another record, or one that prints the same number as a different quantity, reads uncertain or contradicted. The one source rule that still lowers a verdict is your own: a cell whose only sources are ones your knowledge says to treat with caution.

Until now, the quality check's instructions also told it to hold a cell resting on one source, or on several aggregator pages, at leans-supported with confidence at most 0.6. That instruction is removed. A cell whose one cited page states its value can now read supported. How much to trust that page is a separate question, and the web app answers it beside the verdict: each source carries its tier (primary, trade, compiled for an aggregator, or unrated).

Existing cells change when the quality check next reads them, not all at once, so counts of supported cells (a run's "Cells QA supported") can rise as cells are re-checked. The API shape is unchanged.

2026-10-24: a run's search count is Google's, and can be null

metrics.search_queries on GET /v1/research/runs/{id} read 0 on every run, because it counted search steps the research agent never reports while it streams (gh #3271). It now carries Google's own count of the agent's searches, and it is nullable: null means not reported, never zero. Every run completed before this change reads null, as does a run still running, a stopped run, one recovered after an interruption by collecting only its finished research, and one where Google gave no count for some part of its research. If you read the field as an integer, handle null. The SDK types it from deepsieve 0.9.0. The MCP get_run_status tool describes it in deepsieve-mcp 0.11.0; the browser-login MCP server has the wording now.

2026-10-24: a closed or expired account is not a new one

GET /v1/me's subscription.status is none for every organization with no plan, so a retention-closed account, an expired Builders Trial and a new prospect read the same. Three additive, nullable fields on subscription now tell them apart (gh #3278), the same values the web app's billing page reads:

  • expired_from_plan and expired_from_plan_label: the termed plan whose term ended (for example beta, "Builders Trial"). Research stays readable and exportable with Report Chat; no new runs and no free run. Checked first.
  • retention_until: the account is closed. Its research is read-only and is deleted on this date; the date stays set afterwards, so once it has passed the research is gone. No free run, and a top-up is refused (account_closed).
  • Both null with status: none: a prospect. A plan is never off-plan.

The same block on POST /v1/subscription/cancel and /resume carries them too. The MCP get_account tool and deepsieve whoami name the two states, in deepsieve-mcp and deepsieve-cli 0.11.0. The SDK types them from deepsieve 0.9.0; the browser-login MCP server has the wording now.

2026-10-23: list cells merge their supported versions

When one research run states a list cell more than one way and the quality check supports two or more of the versions, the cell now shows their union instead of one version (gh #2370). All additive:

  • SelectionState gains merged. A merged cell's selection (on a /v1/data citation, a conflict, a choose reply, and each change-log entry) carries items: each item of the list, in the cell's order, with evidence_ids (the versions that stated it) and source_urls (their sources). items is null for every other state.
  • The versions stay in the citation's beside[] as also_stated, choosable: true: choose one with POST /v1/cells/{cell_id}/choose to show it alone, and undo that to get the merge back.
  • Each entry of a cell's or column's change log gains selection, its own: merged on the value the merge wrote.
  • A version the quality check contradicted or did not confirm, a version it corrected, and a value a source-caution entry kept out are never merged. A cell someone set is never merged into. Scalar cells are unchanged.
  • A run's stage_metrics.qa_repick gains lists_merged.

2026-10-22: client release: deepsieve 0.8.0

This release carries the API changes the entries below added since deepsieve 0.7.0. Every addition is backward compatible except the session cookie's name: if you passed cookie_auth, see the 2026-10-19 entry. deepsieve-mcp and deepsieve-cli are at 0.10.0 (see the 2026-10-21 entries).

PackageVersionWhat it adds
Python SDK (deepsieve)0.8.0v1_knowledge (list, add, edit, retire, restore, remove, and v1_knowledge_knowledge_usage); the draft-knowledge methods on v1_blueprints and skip on a follow-up answer; v1_research.v1_research_get_run_knowledge; v1_holds.v1_holds_list_conflicts and v1_holds_choose_cell_value, with selection on citations (SelectionState) and the run's conflicting_cells; expected_prepaid_cents on a retry; a payment's retry_on; the run_funding_changed and knowledge error codes; cookie_auth names __Host-ds_session

2026-10-21: domain knowledge on a Blueprint still being created

A Blueprint still being created can hold domain knowledge, and the notes go live with it when it is approved (gh #3126):

  • New routes, on the draft's session id: GET and POST /v1/blueprints/drafts/{session_id}/knowledge, PATCH and DELETE …/knowledge/{kid}, POST …/knowledge/{kid}/retire and POST …/knowledge/{kid}/restore. They take the live Blueprint's bodies and rules (revision, 409 knowledge_revision_conflict, 409 knowledge_conflict answered with on_conflict), with the scope checked against the draft. A deleted draft note is gone: a draft has no earlier versions.
  • Scopes: reads need blueprint:read; writes need blueprint:create (editing a draft is creating a Blueprint), not knowledge:write. Send an Idempotency-Key on writes.
  • New refusals on the writes: 409 session_busy while a change to the draft is running (retriable, Retry-After), and 409 conflict with details.reason no_draft (the draft does not exist yet) or already_built (it was approved: its notes are final, so write the live Blueprint's knowledge instead and do not retry). Before the draft exists, the list returns no entries.
  • Expertise your human states in their description, an answer to a follow-up question or a setup-chat message is saved on the draft as a note in their own words, marked origin.captured: true. Show those notes to them before you approve.
  • When the draft holds no knowledge yet, it asks one follow-up question, domain_knowledge, marked skippable: true. POST /v1/blueprints/drafts/{session_id}/answers takes {question_id, skip: true} for it: recorded as null, nothing re-runs, nothing is metered. Skipping any other question is a 400. Ask your human first; never answer it from your own knowledge.
  • MCP: list_knowledge, add_knowledge, edit_knowledge, retire_knowledge, restore_knowledge and delete_knowledge take draft_id, and answer_blueprint_question takes skip. CLI: deepsieve knowledge list|add|edit|retire|restore|delete take --draft <session-id>. The CLI's default sign-in does not carry blueprint:create, so to write a draft's notes sign in with deepsieve login --preset agent. Both arrive in the 0.10.0 releases of deepsieve-mcp and deepsieve-cli; the browser-login MCP server has them now.

2026-10-21: a payment awaiting the bank, and when it can be retried

One additive field and one new retry value (gh #3034):

  • the subscription object (POST /v1/subscription/cancel and /resume) gains retry_on, a date or null;
  • a resume's retry can be awaiting_confirmation: a subscription payment is still open, usually because your human's bank asked them to confirm it (3-D Secure). Nothing was charged. retry_on is the first day a retry can go through (on that day the open payment is cancelled); null when none is scheduled.

2026-10-21: how each knowledge entry was used across recent runs

One new read, GET /v1/blueprints/{blueprint_id}/knowledge/usage (data:read), with its app twin and the SDK, MCP and CLI surfaces (gh #3136):

  • ?runs= (default 20, at most 50) of the most recent started runs, of every type.
  • Per entry one state: decided, in_scope_no_decision, out_of_scope, or not_checked with not_checked_reason (over_budget, run_in_progress, not_reached, no_run_since_written, not_in_force, no_recorded_run), so "nothing it governs came up" and "nobody checked" never read alike. A run still in progress never makes an entry out_of_scope. can_decide is false for the five kinds that only steer prompts.
  • decisions counts decision events, not cells. cited stays QA's claim. A run with no record counts in window.not_recorded and in no entry's figures.
  • MCP: get_knowledge_usage on both transports (deepsieve-mcp 0.10.0 on stdio). CLI: deepsieve knowledge usage, in deepsieve-cli 0.10.0. The app's Domain knowledge tab shows one line per note.

2026-10-21: undoing a choice later, and seeing a replaced conflicting value

Two additive fields, and the re-pick after QA is now on for every run (gh #2370):

  • every hold object (GET /v1/holds items, a citation's hold) gains choice_id: on a conflict_choice hold, the choice change that made it, so you can undo it with POST /v1/changes/{change_id}/undo (the choice_id is the change id) without having kept the choose reply; null for every other hold;
  • each entry of a cell's or column's change log gains previous_selection, the linked earlier value's selection. state: "conflicting" means this entry replaced a value a run had stated more than one way before anyone chose; null when nothing was chosen among.

2026-10-21: domain knowledge from MCP and the CLI

MCP connections can now edit domain knowledge; CLI users run deepsieve login again to get it (gh #1792).

  • MCP, both transports: list_knowledge, add_knowledge, edit_knowledge, retire_knowledge, restore_knowledge, delete_knowledge and get_run_knowledge, one per route under /v1/blueprints/{blueprint_id}/knowledge and GET /v1/research/runs/{run_id}/knowledge. query_entities now describes a cell's knowledge_decided and knowledge_cited.
  • CLI: deepsieve knowledge list|add|edit|retire|restore|delete and deepsieve runs knowledge <run-id>. A knowledge conflict or a stale revision exits 2; a sign-in that cannot write knowledge yet exits 3.
  • knowledge:write joins the CLI preset and the MCP browser-login grant. It has a member floor: a viewer's connection is still refused (403 insufficient_role).
  • No API or SDK change. The stdio server (deepsieve-mcp) and deepsieve-cli carry these in their 0.10.0 releases.

2026-10-20: remove a misread exact phrase or file type before you buy

POST /v1/research/runs/estimate now takes search_hints, as the create does (gh #2657): an explicit value wins over the hints the parser reads from query, an unusable entry is the same 400 invalid_request with param: search_hints, and intake.search_hints shows the hints the create would use. So you can see that "PDFs only" was read as file_types: ["pdf"] and drop it before you commit. Hints are extra searches, not interactions: the price never changes.

Fixed: an explicit search_hints: {} (every parsed hint removed), or excluded_sources: [] (every parsed exclusion cleared), on the create, a dry run or the estimate left intake showing the parsed ones, although the run carried none. It now omits them, as it does for a request that names none.

The SDK's v1_research_estimate_run, both MCP transports' start_research and estimate_research, and the CLI's runs create (--phrase, --file-type, --no-search-hints) take them too.

2026-10-20: cells stated more than one way, and choosing between them

When one run states a cell more than one way and the quality check supports two or more of the values, the cell shows the better-sourced one until a person chooses (gh #2370).

  • Each citation carries selection: {state, reason}; state is picked, repicked, conflicting or chosen (SelectionState), or the field is null when the run stated the cell one way. Its beside entries gain choosable (true for also_stated) and refused_by, the source-caution entries that kept that value out when the run chose;
  • GET /v1/conflicts lists the conflicting cells, each with candidates to choose from;
  • POST /v1/cells/{cell_id}/choose with {evidence_id, expected_value_evidence_id} shows one of them, holds the cell, and records a choice change in GET /v1/changes (its undo shows the earlier value again). New 409 codes: choice_changed, not_a_candidate, nothing_to_choose, cell_held;
  • a hold's source can be conflict_choice; the run resource's metrics gains conflicting_cells (null when not counted);
  • MCP: list_conflicting_cells, choose_cell_value. CLI: deepsieve holds conflicts, deepsieve holds choose. SDK: v1_holds.v1_holds_list_conflicts, v1_holds.v1_holds_choose_cell_value.

2026-10-20: knowledge weighs the choice among a cell's values

When one run states a cell more than one way, the value the cell shows is now chosen with the run's knowledge too (gh #2370): a value whose every cited page is on a source_caution entry's hosts counts as an unrated source, and a value outside a value_meaning entry's range ranks below one inside it, among values equally cited.

  • When a caution entry changed which value the cell shows, the cell records source_refused, and the run's knowledge read counts it under the stage persist (by_stage.persist);
  • source_refused is reworded to cover that case: "a value not written to the cell because every page it cited is a host a source-caution entry names: a correction or update that was refused, or one of several values a run stated that ranked below another because of the entry". Nothing about existing decisions changes;
  • a range never records a decision here.

2026-10-19: what knowledge decided on each cell

Each cell's citation (GET /v1/blueprints/{blueprint_id}/entities/{entity_key} with receipts=true), and each beside row on it, now carries two lists (gh #1792):

  • knowledge_decided: [{id, outcome, range?, revision?, run_id?}], what an entry decided on that row, by code. A range_flag carries the range the value breached as it stood when it was flagged, and revision the entry's revision then; a flag recorded before this release has neither, so do not explain it with the entry's current range. run_id names the run that decided: a re-check can decide on an earlier run's row, and replaces that row's decisions, so count a run's decisions from its knowledge read, not from its cells;
  • knowledge_cited: the entry ids QA's verdict cited, a model's claim. Never added to the first.

Both are read from stored fields, never from the verdict note. A run's knowledge read also gains snapshot_entries: how many entries the run's snapshot holds, or null when it has none, so a recorded: false run that HAD knowledge can be told from one that never did.

2026-10-19: knowledge that decides: range flags and caution sources

A run's knowledge read (GET /v1/research/runs/{run_id}/knowledge) now reports decisions the code took because of an entry, with no model involved (gh #3084):

  • range_flag: a cell whose value was outside a value_meaning entry's range. The value is kept and gets a note asking for a second look;
  • source_refused: a changed value not written because every page it cited is a host a source_caution entry names;
  • source_downscored (new): a cell QA called supported, capped at leans-supported because every page it cites is such a host.

A caution entry acts only when all of a cell's citations are on its hosts.

2026-10-19: see what knowledge a run carried, and what it decided

GET /v1/research/runs/{run_id}/knowledge (gh #1792) returns the domain knowledge a run carried, in the wording each entry had when the run was created, and what each entry did in it. Editing or deleting an entry later does not change a run's record. Four counts per entry, in total and by stage, that are never added together:

  • carried and dropped: calls whose prompt held the entry, or where it applied but did not fit;
  • cited: QA verdicts that said the entry changed their verdict. A model's claim, kept only when that cell's prompt carried the entry; any other is refused (refused_refs);
  • decided, by outcome (decided_by): outcomes the code took because of the entry. This release produces held_value only; range_flag and source_refused follow.

Per run: cells_cited and cells_decided (distinct cells, a cell being one record's column: its value and any also-stated or held row beside it count once, and so does a record merged before the run's last update; counted once even across a resume, as of that update) and decided_by_outcome. A run created before this release, one whose knowledge could not be recorded when it was created, or one whose record could not be read or kept while it ran, reads recorded: false. Needs runs:read. The Python SDK has the method from deepsieve 0.8.0.

2026-10-19: write domain knowledge through the API

A Blueprint's domain knowledge (what its owners know about the field: where reliable data lives, which sources mislead, what a value means, what to ignore) can now be read and written through /v1/blueprints/{blueprint_id}/knowledge (gh #1792): list, add, edit, retire, restore and delete. Later runs carry the active entries that apply to each stage. Writing needs the new knowledge:write scope, which the agent key preset carries, plus data:read and the member role. Unlike changing a Blueprint's shape, it is open to agents: it changes no stored value and no table, only what future runs are told.

  • New error codes: 409 knowledge_conflict (an entry contradicts or repeats an active one; error.details.conflicts lists them; resend with on_conflict), 409 knowledge_revision_conflict, and 503 blueprint_busy (another write to the same Blueprint is in progress; retriable, honour Retry-After).
  • The Python SDK (deepsieve) has v1_knowledge from 0.8.0.

2026-10-19: a create whose funding changed is refused 409 run_funding_changed

POST /v1/research/runs can answer a new 409 run_funding_changed (retriable) (gh #3055). It means how the run would be paid changed before it started: the run was handled as your org's free run (trimmed to the free-run cap), and it would have been paid from plan credits or the balance. Before, it was charged, with no price ever shown. Nothing is charged now; the run is recorded as failed, not charged. Estimate again and confirm the price it states with your human, then create it again. It can arrive with or without expected_total_cents. The Python SDK's V1ErrorCode names it from deepsieve 0.8.0. deepsieve-cli and deepsieve-mcp need no change: they print the message, and the MCP server passes code and retriable through.

The browser session cookie is renamed from ds_session to __Host-ds_session (gh #3009). The __Host- prefix makes a browser refuse the cookie unless it is Secure, Path=/ and has no Domain, so no other site under our domain can plant one. The /v1 contract's cookieAuth scheme names __Host-ds_session, and so does the Python SDK's cookie_auth field from deepsieve 0.8.0; the old name is not read. Clients that authenticate with Authorization: Bearer are unaffected. If you passed cookie_auth (a browser session cookie, which the contract reserves for browsers), upgrade the SDK or switch to the bearer. A browser signed in before this change is signed out once and signs in again.

2026-10-18: plan run credits and the prepaid balance can fund one run together

A run whose plan run credits cover only part of it is now funded from BOTH (gh #1858): the credits pay for the interactions they cover, in whole interactions (credits first), and the prepaid balance pays the rest at the run price. Before, it was refused unless the balance alone covered the whole run. An org with 2 credits and $20 against a 3-interaction run now runs it.

  • POST /v1/research/runs/estimate: a new funding: {rail, credits_drawn, prepaid_cents} says what pays and how it splits (rail: "mixed" when both do). affordable and shortfall_cents now count only the balance's part, so a run the credits partly fund can be affordable: true where it was false. Every other field keeps its meaning (plan_credits_cover is still "the credits alone").
  • POST /v1/research/runs/{run_id}/retry: funding.covered_by can be mixed, with funding.prepaid_cents the balance's part. Confirm it with expected_funding: "mixed" AND expected_prepaid_cents set to that value; without it the confirmation is 422 validation_failed (param: expected_prepaid_cents), and a different split is 409 retry_price_changed.
  • A refund of a mixed run gives each part back: the credits to your plan, the dollars to your balance. A partial refund returns the balance's part first.
  • Clients: deepsieve-cli 0.9.0 (--expected-prepaid) and deepsieve-mcp 0.9.0 (expected_prepaid_cents on retry_run) send it for you. The Python SDK (deepsieve) has the new fields from 0.8.0. A client released before this cannot confirm a mixed retry: the 422 says to upgrade it.

2026-10-18: affordable is the charge's own verdict, so a planless org with no free run is false

POST /v1/research/runs/estimate now answers affordable with the same funding rule the charge runs (gh #1862). No field was added, removed or renamed; one value changes. An org with no plan and no free run to take (its free run is used, its Builders Trial has ended, its account is closed, or the run is Max depth) is now affordable: false whatever its balance, because the charge refuses it with a 402 and only a plan opens it:

OrgBeforeNow
no plan, free run used, balance covers the priceaffordable: true, shortfall_cents: 0affordable: false, shortfall_cents: 0
no plan, free run used, balance unknownaffordable: nullaffordable: false, shortfall_cents: null
no plan, a Max run, or an ended Builders Trial, or a closed account, balance covers itaffordable: trueaffordable: false
the same, balance unknownaffordable: nullaffordable: false

For such an org shortfall_cents is only the dollar gap: no top-up clears it, so do not buy credit for it; tell your human they need a plan (for a Max run, one that includes Max). Every other org's answer is unchanged.

2026-10-16: a write signed in with the browser session must come from the app (cross_origin_refused)

A POST, PUT, PATCH or DELETE that carries the browser session cookie (ds_session) is now refused 403 cross_origin_refused unless its Origin is the app's own. This is a CSRF defense: it stops another site, including another subdomain of ours, from acting as a signed-in user. API keys are unaffected. A request authenticated by Authorization: Bearer alone is never checked. A request that sends the session cookie AS WELL as a bearer is treated as the browser session and is checked, so send the bearer alone. The code joins the V1ErrorCode enum (in the Python SDK from deepsieve 0.7.0). It is not retriable: the same request from the same page is refused again.

2026-10-16: a retry's confirmation can bind what pays, not only the total

A charging retry (POST /v1/research/runs/{run_id}/retry) is confirmed by sending back its quoted expected_total_cents. That total is the list price whichever rail pays, so a retry your human agreed to as "uses 2 plan run credits" could be paid from the balance if another run spent those credits before the confirmation arrived. Send the quote's funding.covered_by back as expected_funding (free_run, plan_credits or balance) beside the total. If what pays has moved, the answer is 409 retry_price_changed with the new quote and param: expected_funding, and nothing is charged. It is optional: without it, the total alone is bound, as before. The MCP retry_run tool takes expected_funding, the CLI's deepsieve runs retry takes --expected-funding and sends it for you when you confirm interactively (from deepsieve-mcp 0.8.0 and deepsieve-cli 0.8.0; the Python SDK has the field from deepsieve 0.7.0).

2026-10-15: read a cell's change history from the CLI

deepsieve data history reads the change logs the app, the API and the MCP tools already expose. deepsieve data history <entity> <column> lists every change to a column across records, newest first; add --record <id> for one record's cell. deepsieve data history --cell <cell_id> reads one cell by its cell_id (from --json output or deepsieve holds list), and --rechecks adds re-checks that found a different value and did not apply it. The table uses the app's words (QA corrected, earlier value not recorded); --json prints the API's JSON response. It needs data:read, which the CLI login already has, and a deepsieve-cli release after 0.8.0.

2026-10-15: a cell's change log shows the value a QA correction replaced

When QA corrected a value research wrote, it rewrote that evidence row in place, so the change log showed no trace of the value it replaced: the entry read change: "initial", calling QA's correction the cell's first value, or unknown. It now reads change: "amended", with two new fields on every change entry (GET /v1/cells/{cell_id}/changes and GET /v1/blueprints/{blueprint_id}/entities/{entity_key}/columns/{column}/changes): pre_amend (recorded, not_recorded or none) and pre_amend_value, the value QA replaced. not_recorded means the replaced value is not available: the correction predates DeepSieve keeping it, or that value was empty. none means the entry is not an in-place correction. A re-check's correction reads none too: it is a new row linked to the one it corrected, so its replaced value is previous_value, as before. Treat a change value you do not recognise as unknown, and a missing pre_amend as none; the Python SDK already accepts both. The MCP tools' descriptions say the same.

2026-10-15: a value the cell no longer holds is listed beside it, never as its support

A cell can carry several sources at once. When research, a re-check or a chat edit replaced its value, a source still stating the old value used to stay listed with the cell's sources, as if it supported the new one. It now appears in the citation's beside list with the new kind replaced: its own value, verdict and source_hosts, never the cell's. A source stating the new value, however it is spelled, still supports it. Treat an unknown kind as a row beside the cell; the Python SDK already accepts one.

2026-10-15 — client releases: deepsieve 0.7.0, deepsieve-mcp 0.8.0, deepsieve-cli 0.8.0

These releases carry the agent-facing changes the entries below added since deepsieve 0.6.0, deepsieve-mcp 0.7.1 and deepsieve-cli 0.7.0. Every addition is backward compatible; older clients keep working against the API.

PackageVersionWhat it adds
Python SDK (deepsieve)0.7.0v1_subscription.cancel and resume (sync and async); the subscription block and its trial on GET /v1/me (Subscription, SubscriptionState, TrialState); the billing:cancel scope and its new error code; run token counts described as the run's research only
MCP server (deepsieve-mcp, stdio)0.8.0the cancel_subscription tool; get_account explains the subscription and the free trial; get_run_status's token counts are research only and survive a restart; approve_blueprint explains a busy first-run workspace
CLI (deepsieve-cli)0.8.0deepsieve subscription cancel and deepsieve subscription resume; the account summary says when a free trial's first charge is

2026-10-15: a run's token counts are its research only, and survive a restart

metrics.llm_input_tokens and llm_output_tokens on GET /v1/research/runs/{id} now count the run's research calls only. A chat turn about the run used to count partly: its own call never did, but its helper calls did whenever the run was drained again, so the number could grow after the run had finished. It now counts toward nothing on the run. And the counts no longer lose what was counted before an API restart: a run recovered by another process keeps every call. When two processes executed a run, metrics.usage_complete still reads null, because whether every call reported its usage is not known; it no longer means the counts themselves may be short. Token counts are for your own accounting: a run is billed per run, never by tokens.

2026-10-15: an Idempotency-Key no longer 503s for an organization with no Blueprint yet

POST /v1/keys, POST /v1/keys/{key_id}/rotate and POST /v1/blueprints/drafts answered 503 service_unavailable ("Workspace resolution failed") when an organization with no Blueprint yet sent an Idempotency-Key. Retrying never helped. They now behave the same with or without the key, and a same-key retry replays. A key is remembered for the organization on these calls, so a draft retried after it created your first Blueprint replays instead of starting a second one. Nothing you send changes.

2026-10-14 — cancel and resume a subscription over /v1, MCP and the CLI

New: POST /v1/subscription/cancel and POST /v1/subscription/resume, the billing page's two buttons. Both answer with the subscription block GET /v1/me serves (plus object and retry), are safe to repeat, and honour Idempotency-Key. Cancel needs the new billing:cancel scope (it only stops spend, so the CLI login and the MCP browser login carry it); resume needs billing:write. Both need the admin role. New error codes: no_subscription, subscription_ended, and billing_update_in_progress (retriable, with Retry-After). The Python SDK, the MCP cancel_subscription tool and deepsieve subscription cancel|resume follow. There is no MCP resume tool, on purpose: resuming re-arms a charge, so it needs the organization's admin on the billing page, or an API key with billing:write.

Behaviour changes:

  • a cancelled account that is past due is no longer charged by a manual retry; resume it instead;
  • a scheduled downgrade now survives a cancel that is resumed, so the renewal charges the plan you chose.

2026-10-14 — GET /v1/me says when money moves: the subscription and the free trial

GET /v1/me gains subscription: status (none, past_due, ended, trialing or active), cancel_at_period_end, access_through, and trial. trial says whether this organization may start a free trial (eligible, offer_days), and, during one, when it ends, the days left, and the date and amount of the first charge. The first charge's date and amount are null once a trial is cancelled, because a cancelled trial is never charged, unless that charge is already in flight. After the trial ends, conversion_pending, conversion_submitted, conversion_failed and ended_without_charge say where the first charge is. The fields are listed in Authentication.

The Python SDK types it as SubscriptionState and TrialState on the identity. The MCP get_account tool and deepsieve whoami show it too. Nothing you send changes.

2026-10-13 — an organization with no workspace can design its first Blueprint

Behaviour change. In an organization with no workspace yet, POST /v1/blueprints/drafts (and the web onboarding start) answered a retriable 500 internal_error, so an API key or OAuth caller there could never create its first Blueprint, and a retry could never succeed.

  • The draft now starts (202) and gets a workspace of its own. Approving it builds the Blueprint and makes that the organization's workspace; after that, /v1 calls read it as usual.
  • Looking up a session id that is not one of your drafts in such an organization answers 404 not_found, with the same body whether or not the id exists in another organization. It was a retriable 500.
  • Approving such an id answers 409 no_active_blueprint, as /v1 already did.
  • 403 no_blueprint_access now also names "the organization has no Blueprint yet" among its reasons. The code and status are unchanged.
  • The web POST /api/onboarding/sessions now declares its 409 in the contract. It was already served, with a plain detail message, when the organization holds the maximum number of unfinished drafts. It is now also served, as a typed conflict, when the caller acts in no organization at all.
  • Approving another organization's draft id in such an organization answers the same 409 no_active_blueprint as an id that exists nowhere. It was 404, which told you the id was a draft somewhere.

Nothing you send changes, and charges are unchanged.

2026-10-13 — client releases: deepsieve 0.6.0, deepsieve-mcp 0.7.1, deepsieve-cli 0.7.0

These releases carry the agent-facing changes the entries below added since deepsieve 0.5.0, deepsieve-mcp 0.7.0 and deepsieve-cli 0.6.0. Every addition is backward compatible; older clients keep working against the API.

PackageVersionWhat it adds
Python SDK (deepsieve)0.6.0a re-check run's check_results and a follow-up search's update_result on the run resource, including the stopped outcome and the could_not_finish messages; unknown on a failed chat action's type
MCP server (deepsieve-mcp, stdio)0.7.1get_run_status explains a re-check's results and its follow-up search
CLI (deepsieve-cli)0.7.0deepsieve runs get prints what a re-check found and what its follow-up search found

2026-10-13 — approving a draft in an organization with no workspace is 409 no_active_blueprint

Behaviour change. POST /v1/blueprints/drafts/{session_id}/approve, from an API key or OAuth caller in an organization that has no usable workspace, answered 403 blueprint_forbidden. That code means you may read the organization but not the Blueprint you named, which is a different condition with a different fix. It now answers 409 no_active_blueprint (not retriable), the code /v1 calls that read a Blueprint return for the same caller.

  • The status changes from 403 to 409, and the code from blueprint_forbidden to no_active_blueprint. A handler that matched either one for this case needs updating.
  • A caller limited to specific Blueprints, none of which is available, still gets 403 no_blueprint_access on approve.
  • Nothing else about approve changes, and nothing you send changes.

2026-10-13 — a re-check or search that fails after saving no longer says nothing changed

A follow-up search that failed after it had started saving what it found read update_result.outcome could_not_finish with "Nothing was changed", although the value it found could already be in the cell. A re-check that failed while applying its result said the same, although part of the result could already be applied.

  • A search that fails once it has started saving now says it couldn't finish saving, and asks you to reload to see the cell's current value.
  • A re-check that fails after the value was checked now says it was checked but couldn't finish applying the result.
  • A failure before anything was saved or checked still says "Nothing was changed".

The outcome is still could_not_finish; read the message to tell which. Charges are unchanged, and nothing you send changes.

2026-10-13 — stopping a re-check or a follow-up search stops it

POST /v1/research/runs/{id}/cancel accepted a re-check run or a follow-up search run and answered cancelling, but the run then ignored it: a search that finished afterwards read completed, and a stopped re-check stayed cancelling, its value held as being re-checked. Both now end cancelled.

  • A re-check someone started (in the app or in chat) stops where it is. A value it had not applied yet is handed back, and its check_results entry reads stopped. The message says whether that value had already been checked.
  • A re-check that monitoring started cannot be interrupted. It finishes, then reads cancelled with every result.
  • A search stopped before it saved anything, or while it was saving, reads update_result.outcome stopped, and the message says which. A search stopped after it saved a value keeps that value, and its update_result says what it saved.

Charges are unchanged, and nothing you send changes.

2026-10-13 — a failed chat action's type is always one you were told about

When the research agent proposed a kind of change that doesn't exist, the failed action it left in the thread carried whatever type the agent had named, outside the documented action_type values. It now reads unknown, a value the API contract and the Python SDK list. unknown appears only on a failed action, and its note still says what the agent asked for. Every other action's type is unchanged.

2026-10-13 — a chat re-check says when the sources didn't confirm a value

When a re-check you asked for in chat found that the sources didn't confirm a value, its check_results entry said not_checked and "This value couldn't be checked", though the check had run. That entry now says not_confirmed, with a message that it was checked, that the sources didn't confirm the value, and where in the app to look for the current one. Nothing changes in the value itself, and nothing you send changes. not_checked still means a value the check could not reach.

2026-10-13 — a re-check's result is on the run resource

A re-check run said what it found only in the app and on the chat card: the run resource had no field for it. It now has two, on GET /v1/research/runs/{id} and every response carrying the run resource, MCP get_run_status, the Python SDK, and deepsieve runs get in the CLI.

  • check_results: on a re-check run, one entry per checked value, with evidence_id, outcome (a stable key), message (the sentence to show your human) and followup_run_id (set when the check started a search for the current value).
  • update_result: on a follow-up search run, {outcome, message}: what the search found. Monitoring's re-checks and the app's Re-verify button can start one; a re-check you ask for in chat does not.
  • Both are null on every other run, until the run finishes, and on a run that ended before it could record a result (status and error say why). outcome is an open, documented string: read a key you don't know as neutral. A chat reverify action's reverify_run_id is the re-check run to read. Nothing you send changes.

2026-10-09 — re-checks you pay for no longer use up a report's included refreshes

A monitored report has a monthly allowance of background re-checks (freshness.monthly_budget on the run resource). Re-checks you paid for used to count against it too: a rerun you approved, or a re-check you asked for in the app or in chat. A report could then show throttled: true, and slow its background refreshing, because of work you had already bought. Now only the background re-checks count, so freshness.used_this_month and remaining reflect those alone. A paid re-check of a value that was already refreshed in the background this month does not undo that refresh: used_this_month never goes down within a month. Background re-checks made earlier this month, before this change, are not counted, so this month's figure may start low. Nothing you send changes.

2026-10-09 — MCP can extend a report, reuse held data and bound a run by date

The MCP tools start_research and estimate_research (both transports: the browser-login server and the deepsieve-mcp package) take additive_to, reuse_existing and temporal_cutoff, as POST /v1/research/runs and its estimate do. Each is sent to /v1 as given and refused there exactly as there. Pass the same values to both tools and the estimate prices what the start charges. reuse_existing must be a real boolean: a string such as "yes" is refused, never read as true. Before this, both tools ignored these arguments without an error, so a request that passed them was quoted and charged for the whole list.

2026-10-09 — a run says whether its token counts are complete

A run's metrics.llm_input_tokens and llm_output_tokens silently under-counted when a model call's provider reported no usage: they summed what was reported, and nothing said a part was missing. The run resource now says so.

  • metrics.usage_complete: true when every call reported its usage; false when at least one did not, so the counts are a floor; null when unknown (a run from before this, one still running, or one executed across an API restart). Never read null as complete.
  • metrics.usage_unreported_stages: where it happened, such as investigate.
  • On GET /v1/research/runs/{id} and every response carrying the run resource, MCP get_run_status, and the Python SDK. Display only: research bills per run, never by tokens.
  • Additive: nothing an earlier client sends or reads changes.

2026-10-09 — the run estimate prices what the run will charge

POST /v1/research/runs/estimate now prices a request on the same steps the create charges on, so for an unchanged request the estimate's run_price_cents is the create's price, and binding the create to it (expected_total_cents) holds exactly.

  • New request fields reuse_existing and temporal_cutoff, the create's own: with reuse_existing, the seeds you already hold current data on are priced out and listed in the new response field reuse_predicted.
  • A known free run is priced on the entities it researches (entities, interactions and run_price_cents count the trimmed list). Where free-run eligibility cannot be read, nothing is trimmed and the quote is the higher number.
  • The estimate now refuses a request the create would refuse: every seed already in the additive_to report, or every seed predicted held under reuse_existing, is a 422 with the create's own message, so you find out before you commit.
  • A quote never rises: a caller that sends neither new field sees the same number as before, except a known free run over its cap, which was quoted too high and now matches the charge.

2026-10-09 — a run can be bound to its estimate

POST /v1/research/runs takes an optional expected_total_cents: the estimate's run_price_cents, echoed back once your human approves it. A create that would now cost MORE is refused 409 run_price_changed, with the new total in error.details.total_price_cents (and expected_total_cents, interactions, unit_price_cents), and nothing is charged or created. The same or a lower price starts as usual; how the run is funded never triggers it.

  • Also on MCP start_research, the CLI (deepsieve runs create --expected-total CENTS) and the Python SDK.
  • Additive: omit the field and nothing changes.

2026-10-09 — a request names the seeds it counted once

Two spellings of one entity in one request are priced once (see below). Which seed was counted as which is now named, before you pay, so you can see the judgment and change the list if you disagree.

  • folded_duplicates: [{seed, kept_as, reason}] on POST /v1/research/runs/estimate, on the 202 from POST /v1/research/runs (and its dry_run), and on the app's preview. kept_as is the spelling kept and researched; reason is case, same_record, legal_form or alias (an open string: read a new value as "the same entity").
  • Present only when something folded. Additive: nothing an earlier client sends or reads changes.

2026-10-09 — two spellings of one record in one request are researched and charged once

A request that named one entity twice, as "Acme" and "Acme, Inc.", or as a name and one you merged into it, was priced and researched twice and landed on one record. A request without typed URLs did not even fold "Acme" and "acme". Now every request keeps the first spelling and drops the other before it is priced.

  • POST /v1/research/runs, its estimate and the app's preview all list and price the folded request, so they agree with each other and with the charge. seeds on the run lists the spellings kept.
  • A spelling with an incompatible legal form ("Fund X, L.P." vs "Fund X, Ltd.") is a different entity and stays.
  • A report that already holds one record researched under two spellings in separate batches counts it once from now on. The included refreshes for a report that was counting a spelling variant twice now match the records it actually holds.

2026-10-09 — a different spelling of a record a report holds is not a new dossier

Adding entities to a report (additive_to) used to treat a different spelling of a record the report already holds ("Acme, Inc." for "Acme", or a name you merged into it) as a new dossier: it was priced and researched again, and counted as one more monitored unit, though it landed on the same record. Now it is held, exactly as the same spelling is.

  • skipped_already_held (on POST /v1/research/runs and its estimate) also lists such a seed. It is not charged and not researched, as long as the record is held with current evidence.
  • A report's monitored units count records, not spellings: the units, the free-window notice and the monthly charge drop by any spelling counted twice. The included refreshes for a report that was counting a spelling variant twice now match the records it actually holds.
  • A spelling with an incompatible legal form ("Fund X, L.P." vs "Fund X, Ltd.") is a different entity and stays new.

2026-10-03 — a failed automatic re-research is retried, and says so when it gives up

A cell's automatic re-research that failed used to count as "re-researched recently", so the cell paused for the whole cooldown (30 days) with no research done. Now a failed re-research that delivered nothing is retried once inside the cooldown, once per record and its parent together (a fund's re-research is its company's). If the retry fails too, the cell pauses until the cooldown ends with the new reason below. A re-research that delivered, or that you stopped, still starts the cooldown as before.

  • re_research_pause_reason (on a citation, in GET /v1/blueprints/{blueprint_id}/entities/{entity_key} and JSON exports) gains rerun_failed: "Automatic re-research of this record failed twice; it will be tried again on or after <date> (UTC). You can re-check it now." A manual re-check is available while it is paused. A client that validates the enum strictly must accept the new value; the Python SDK's enum already accepts unknown values.

2026-10-02 — every change proposed in chat waits for your OK

A change the agent proposes in chat is never written, held or started until someone applies it with POST /v1/chat/actions/{action_id}/confirm (MCP confirm_chat_action), including one you asked for. This replaces 2026-10-01's "a change you ask for applies as before".

  • ChatWarningCode gains confirm_required ("Changes from chat wait for your OK."), carried by a change the agent says you asked for. A client that validates the enum strictly must accept the new value. model_proposed still marks the agent's own idea.
  • initiated_by is the agent's claim about who asked. It is shown to help you decide and changes nothing about what happens.
  • confirm_required, like model_proposed and no_data_write, is about who asked: it is never recorded on a cell's evidence as a warning the change was applied despite.

2026-10-02 — client releases: deepsieve 0.5.0, deepsieve-mcp 0.7.0, deepsieve-cli 0.6.0

These releases carry the agent features the entries below added since 0.4.0 / 0.6.0 / 0.5.0. Each is additive: nothing an earlier client sends changes meaning.

ClientVersionAdds
Python SDK (deepsieve)0.5.0run retry, confirmed by its total; a report's waiting re-research (get, approve against its quote) and auto-approve with a monthly cap; monitoring on or off, and monitored on a run; removing a many-to-many link; the merge preview_digest; the estimate's check_urls and url_outcomes; a cell's evidence beside its value and the value a source stated; seed_labels on a run start, and seed_mode and seed_labels on a run
MCP server (deepsieve-mcp, stdio)0.7.0retry_run; get_reruns, approve_reruns, set_rerun_auto_approve; set_monitoring, list_monitoring; remove_link; check_urls on estimate_research; the merge preview digest; seed_labels on start_research; the same parameters as the hosted server on every tool
CLI (deepsieve-cli)0.6.0deepsieve runs retry, runs reruns (approve, auto-approve), runs monitoring --on|--off; deepsieve links remove; --preview-digest (or --without-preview) on merges

The browser-login MCP server already has all of it.

2026-10-02 — URLs you list as "use only these" stay your targets, with your labels

A run seeded with URLs used to read each page to find what to research and research those finds by name, so a list of pages became a list of names, and a label you gave a page was lost. Now:

  • When a query gives URLs and says "use only these" (the parser infers allowed sources of only those pages' hosts), each URL is its own target, in your order, named P01 (https://…). The run reports seed_mode: "as_typed" ("discover" is the old behaviour, unchanged for every other run, and always the mode of a seeds request), and so does the estimate.
  • Labels: POST /v1/research/runs takes seed_labels ({seed: label}, e.g. "P01" beside a URL). With a query, the parser reads labels you wrote (P01: https://…). The run resource returns seed_labels, and a labelled target is named by its label in the run's records.
  • In as_typed the estimate no longer raises allowed_sources_seed_hosts_only: nothing is searched by name, so that warning would be false.

All additive. MCP start_research takes seed_labels too.

2026-10-01 — a merge applies only the merge you previewed

POST /v1/merges/preview now returns preview_digest, a digest of everything the merge decides. POST /v1/merges requires it (or apply_without_preview: true, to merge without a preview on purpose):

  • If the merge changed since the preview (a conflict decided differently, a tie whose values changed, a cell newly filled, a link a person removed since), it is refused 409 merge_changed, nothing written. error.details.preview is the merge as it stands now, with its own preview_digest.
  • Sending neither is 422 preview_required; sending both is a 400.
  • The result's new preview_checked is true when the digest matched, false for a merge applied without a preview.

Breaking: a call to POST /v1/merges that sends neither field is now refused. Preview first and send the digest, or add apply_without_preview: true to keep merging blind. The MCP merge_records tool takes preview_digest / apply_without_preview, and the CLI's merge apply takes --preview-digest / --without-preview.

2026-10-01 — a change the agent proposes on its own waits for you

A chat change the agent makes on its own (initiated_by: model, rather than one you asked for) now always waits for confirmation, with the warning model_proposed. Nothing is written, and no cell is held, until it is applied (POST /v1/chat/actions/{action_id}/confirm, MCP confirm_chat_action). A re-check it proposes starts its paid run only once applied. A change you ask for applies as before. ChatWarningCode gains model_proposed.

2026-10-01 — an approval that charged always answers what it did

POST /v1/research/runs/{run_id}/reruns/approve with auto_approve: true charges first and then saves the setting. If saving the setting fails after the charge, the answer is still 200 with what was approved, and its auto_approve is the setting as saved (unchanged), not the one you asked for. A retry with the same Idempotency-Key replays that answer and charges nothing more. Read auto_approve in the answer, not your request, and set it again with PATCH …/reruns/auto-approve if it did not take.

2026-10-01 — the run says whether it is monitored

GET /v1/research/runs/{run_id} (and the create, the list and a retry) now carries monitored: true or false is whether the run is monitored NOW, the same state PATCH …/monitoring sets, so a later toggle shows; an additive run reports its report's state. null means monitoring does not apply to the run (a cascade rerun, an update child, or a run from before monitoring), never that it is off.

2026-10-01 — the estimate can check your URL seeds

A URL seed is read to find what to research, and each entity found on the page is then researched by name, so a page that names nothing adds nothing while it is still priced. POST /v1/research/runs/estimate (MCP estimate_research) now says so before you spend:

  • check_urls: true reads each URL seed now and returns url_outcomes, one per URL, in the order sent: found (with a count), none_found or unreadable. Each URL can take several fetches and model calls against your organization's model allowance, so it is opt-in, and it reads at most 25 URLs. It never changes the price.
  • Without it, an estimate with URL seeds carries check_urls_hint and reads nothing.
  • warnings[] gains allowed_sources_seed_hosts_only when the allowed sources are only your URL seeds' own sites. Each page's finds are researched across the web, and keeping only those sites' citations can leave nothing to write.

All three are additive; SDK clients receive an unknown warning code as a string.

2026-10-01 — a citation is the source of the value the cell holds

A cell's evidence can include rows recorded beside its value: another value the same run stated, a value later research found and did not apply, a value found only on sites outside the run's allowed sources, or one that broke the column's rule. Some of these are newer than the value, and the citation used to be the newest row, so it could cite a value the cell does not hold. Now:

  • The citation (GET /v1/blueprints/{id}/entities/{key}, GET /v1/export) is always the source of the value the cell holds: its value, verdict and source_urls are that row's.
  • The rows beside the cell are listed in the citation's new beside array, each with its kind, its own verdict, source_hosts and note, its value (never for withheld), and for not_applied a reason in plain words. Read a beside verdict as that row's, never as the cell's.
  • A cell with no value row is cited from its newest non-conforming observation. A cell holding only other rows beside it has an uncited citation (evidence_id: null) with them in beside.

2026-09-30 — agents can turn auto-approve on

PATCH /v1/research/runs/{run_id}/reruns/auto-approve and auto_approve: true on POST …/reruns/approve now work for an API key or an OAuth app (MCP set_rerun_auto_approve, CLI deepsieve runs reruns <id> --auto-approve-on --cap <USD>). Auto-approve is standing authority to spend up to a monthly limit, so ask your human first.

  • It is recorded against the key or app that turned it on. It stops when that key is revoked or expires, or its user signs out everywhere: auto-approve turns off and says why.
  • It pauses while the key is suspended, and resumes when the key is re-enabled.
  • Revoking an app at AuthKit alone does not stop it; sign out everywhere, or turn it off.
  • The code 403 auto_approve_requires_app is gone: nothing returns it any more.

2026-09-30 — a value withheld because of its source is never the cell's value

When a re-check finds a value only on a site outside a report's allowed sources, it withholds the value and records the site beside the cell. Some readers still treated that record as the cell's current value. Now none do:

  • GET /v1/changes and MCP get_cell_changes show it as observed, not as a changed value.
  • A report's confidence and "supported" counts score the value the cell holds, not the withheld record. A re-check run whose only finding was withheld no longer counts that cell as researched and cited.
  • What a rerun costs can change. For a record whose name has expired, its other spellings (one QA set aside, or an older one) are re-researched when a rerun reaches them. Before, a withheld record with no expiry kept them counted as already researched, backed by a source the report may not use.

Research adds a record's many-to-many links and never removes one, so a wrong link (a person who left a fund, a mis-matched name) could not be removed by anyone. Now:

  • POST /v1/links/remove (MCP remove_link, CLI deepsieve links remove) takes one link out. Research does not add it back. If it was the primary link, the next becomes primary.
  • It is a change like a merge: link_removed in GET /v1/changes, undone with POST /v1/changes/{change_id}/undo, which puts the link back in the list (at its old position when that still fits).
  • A link the record does not have is the new code 409 link_not_present.
  • A merge keeps a removal: the other row's list does not bring the link back, and the merge preview says so (links_combined[].stays_removed).

2026-09-30 — approve a report's waiting re-research over the API

An agent could see that re-research was waiting on a monitored report but could not clear it. It can now:

  • GET /v1/research/runs/{run_id}/reruns lists what is waiting (every item, with the entity, the fact and what the re-check found), today's total (awaiting_total_cents), the included re-research this period and when it renews.
  • POST /v1/research/runs/{run_id}/reruns/approve pays for it. It needs expected_total_cents and quote_id (both from the list your human approved) and an Idempotency-Key, and buys exactly those rows. If anything waiting changed, even at the same total, it is the new code 409 rerun_price_changed with the new total (read the list again for its quote_id); too little credit is 402 insufficient_credits. Nothing is charged on either. An agent asks its human first.
  • PATCH /v1/research/runs/{run_id}/reruns/auto-approve turns automatic approval off. Turning it on is done in the app for now: an API key or OAuth app gets the new code 403 auto_approve_requires_app, because revoking the key would not stop the spending.
  • There is no decline: a rerun left waiting costs nothing, but its value is not treated as verified until it is decided again when the report's re-research renews (never, on a plan with no included re-research).
  • MCP tools get_reruns, approve_reruns and set_rerun_auto_approve (the browser-login server has them now; the stdio deepsieve-mcp gets them in its next release), and the CLI command deepsieve runs reruns <id> [--approve --expected-total <cents> --quote-id <id>] [--auto-approve-off] in its next release.

Marking two rows as duplicates used to compare their many-to-many links as if they were one value: two funds that list different people were a conflict, often a tie, and the list you did not pick was dropped. Now the links are combined, the kept row's first, and are never a conflict or a tie.

  • POST /v1/merges/preview and POST /v1/merges return links_combined[]: one entry per merged row that added links or changed a held list, {column, to_entity, to_label, added, from, changes_held}. to_label is the Blueprint's name for the linked entity.
  • changes_held is true when a list a person set on purpose would change. Nothing sets a hold on these lists today, so it is currently always false; read it for forward compatibility.
  • An owner's link columns no longer appear in conflicts or unresolved_ties.

2026-09-30 — Stop reaches a run between automatic retries

POST /v1/research/runs/{run_id}/cancel on a run that crashed and is being retried automatically (it shows as running) now stops it even between two retry attempts. Before, a Stop in that gap did nothing: no refund, and the run could carry on.

  • What it had not delivered is refunded (cancel.refunded, cancel.refund_scope). A run paid from the included re-research allowance gets the allowance back instead, and refunded reads false.
  • cancel.status reads cancelling, and the run settles cancelled. Stopped between attempts with everything already delivered, it settles completed, with nothing refunded.
  • The advice to wait and send the Stop again is gone: one Stop is enough.
  • cancel.status never reads the internal awaiting_recovery state any more.

2026-09-30 — retry a failed run over the API

POST /v1/research/runs/{run_id}/retry retries a failed, stopped or empty run, as the app's Retry does: it resumes the same run when its research is still alive, otherwise it starts a new run with the same inputs, linked to this one (retry.source_run_id).

  • A retry that would charge is 409 retry_confirmation_required until it is sent with expected_total_cents equal to the quote's total_price_cents. The quote is in error.details, with the failed run's refund and what is carried over or skipped at no charge. An agent asks its human before confirming. A moved price is 409 retry_price_changed with the new quote. Nothing is created or charged on either.
  • A retry that charges nothing starts at once. A second call after a retry started returns it. The Idempotency-Key is honoured, and a refusal releases it for the confirmation.
  • New codes: retry_confirmation_required, retry_price_changed, run_recovering and run_stopping (retriable). runs:write and a member role.
  • The MCP tool is retry_run (expected_total_cents, strictly an integer), and the CLI command is deepsieve runs retry <id> [--expected-total <cents>]. The browser-login MCP server has it now. The stdio deepsieve-mcp, the SDK and the CLI get it in their next releases.

2026-09-30 — download filenames work with any characters

Every download (/v1/blueprints/{blueprint_id}/export, the report's markdown and the app's exports) now sends its filename twice in Content-Disposition: an ASCII filename="…" fallback, and the exact name as filename*=UTF-8''… (RFC 6266). Read filename* when it is there. A report whose title had a dash, curly quotes, CJK or an emoji used to fail to download.

2026-09-29 — older reports show monitoring off

Reports made before monitoring existed now show monitoring off, and are no longer re-checked on a schedule. Turn it on from the report to keep one fresh.

2026-09-29 — a converted quantity says what the source stated

A citation gains stated: when a quantity value was converted into the column's unit, the value and unit exactly as the source stated them, before conversion (for example {"value": "5.0", "unit": "mi"} beside a stored 8.05 km). value is a decimal string, so the source's own precision survives. null when nothing was converted.

2026-09-28 — turn a report's monitoring on or off over the API

PATCH /v1/research/runs/{run_id}/monitoring with {"active": bool} stops or starts a report's monitoring, as the app's toggle does. GET /v1/research/monitoring shows which reports duplicate each other (duplicate_monitoring); this is how an agent acts on it.

  • The response is the report in the overview's vocabulary (in_free_window, free_until, billing, units, reruns, locked_until, stop_scheduled_at, grace_until), read back after the change. A new schema, ReportMonitoring.
  • A UTC day bills if monitoring was on at any moment of it: OFF stops billing from the next day, and ON bills today. A repeat is a no-op.
  • Turning it back ON after the free 30 days commits 30 days: the call is 409 lock_confirmation_required with error.details.locked_until until it is sent with commit_lock: true, and an agent asks its human before sending that. Send expected_locked_until (the date they agreed to) with it: if a UTC midnight passed meanwhile, the call is 409 lock_quote_changed with the new date and nothing is committed. The error envelope gains an optional details object for facts like these.
  • runs:write and a member role. A run with no monitoring is 404 not_found.
  • The MCP tool is set_monitoring (commit_lock=True, expected_locked_until), and the CLI command is deepsieve runs monitoring <id> --on|--off [--commit-lock --expected-locked-until <date>]. The browser-login MCP server has it now. The stdio deepsieve-mcp, the SDK and the CLI get it in their next releases.
  • GET /v1/research/monitoring reports gain grace_until.

2026-09-28 — a topped-up report bills as one report, each top-up after its own 30 days

An additive run (additive_to) now joins the monitoring of the report it extends. It used to register monitoring of its own that never billed, so a report bought with additive_to kept some of its dossiers fresh without counting them.

  • Its new dossiers count in the report's units (the monitoring pill, GET /v1/research/monitoring, and the report's included re-research). Each additive run keeps its own free 30 days from when it ran, then bills with the report.
  • GET /v1/research/monitoring rows gain units_in_free_window, billing_units (what the report bills today) and additive_windows[] (run_id, units, free_until, in_free_window). The top-level billing_units is now the sum of the rows', so an additive run still inside its window is not in it.
  • monitored is ignored with additive_to, silently in this version: the report's setting governs the run.

2026-09-28 — reuse_existing: what was predicted, and what was carried

A reuse_existing run used to drop the entities we already hold before the run existed, so reuse never saw them: nothing was carried and the report lacked them, while the 202 said they were "carried into this run". Now every seed reaches the run, and:

  • The 202 lists the entities the price left out as reuse_predicted, with your allowed_sources applied. reused_existing stays on the 202 with the same value, for compatibility.
  • The run resource (GET /v1/research/runs/{run_id}) gains reuse_carried (what reuse actually carried), being_refreshed ([{name, run_id}]: entities skipped because a refresh of them was already running) and reuse_not_researched (see below). All three are null until the run decides, and on every run without reuse_existing.
  • An entity predicted held that reuse cannot carry is researched anyway, at no extra charge, up to a per-run limit. Any past the limit are listed in the run's reuse_not_researched (re-run to research them). You never pay more than the quote.
  • A reuse_existing run whose every seed is predicted held is refused 422 even with guidance, instead of being charged an interaction to research nothing.

2026-09-28 — MCP: both transports take the same parameters and call the same endpoints

The browser-login MCP server and the stdio package (deepsieve-mcp) exposed the same tools, but four of them took different arguments or called a different endpoint. They now match:

  • adjust_blueprint is one synchronous turn on both (POST /v1/blueprints/drafts/{id}/chat), returning the agent's reply and the measured changes. It was a 202 you had to poll on stdio. It waits for the model; pass the first response's message_id to retry one that timed out without a second model call.
  • start_research takes guidance and monitored on both. On stdio this is new: monitored was not accepted, so every stdio run was monitored. Pass false for a one-off run.
  • seeds and allowed_sources on start_research and estimate_research take a list or one comma-separated string on both.
  • start_research on stdio now sends an idempotency key on every call, as the browser-login server does. Pass your own idempotency_key to make a retry safe.

The browser-login server has these now. On stdio they ship in the next deepsieve-mcp release after 0.6.0.

2026-09-28 — turning monitoring back on after the free 30 days commits 30 days

Turning a report's monitoring back on after its free 30 days commits 30 UTC days. The app's monitoring toggle needs commit_lock: true for that ON; without it the response is a 409 lock_confirmation_required naming the would-be locked_until. Turned off during those 30 days, monitoring stays on, and billing, until they end. GET /v1/research/monitoring reports gain locked_until (the exclusive 00:00Z end, so the last billed day is the day before it) and stop_scheduled_at.

2026-09-28 — monitoring bills each UTC day it was on at any moment

Monitoring bills each UTC day it was on at any moment. Turning it off stops billing from the next day, and turning it on bills today. A report's free 30 days are unaffected, and run on the calendar whether monitoring is on or off.

2026-09-27 — MCP: list_monitoring, what monitoring costs and where it overlaps

A new read-only MCP tool, list_monitoring(blueprint_id), on both transports, over GET /v1/research/monitoring. An agent could switch monitoring on with start_research but could not see what it was paying for. It now can:

  • billing_units past the free 30 days;
  • duplicated_units, the units paid for on reports another report already keeps fresh, with a duplicate_monitoring warning;
  • reruns, the included re-research used versus included this billing period, where an unreadable report's numbers are null, never 0.

It needs a member role. The browser-login server has it now; the stdio deepsieve-mcp gets it in its next release after 0.6.0.

2026-09-27 — client releases: deepsieve 0.4.0, deepsieve-mcp 0.6.0, deepsieve-cli 0.5.0

These releases carry the entries below, from the workspace_id → blueprint_id rename to the model allowance. Upgrade before workspace_id stops being accepted: these are the first releases that name the Blueprint with blueprint_id on every call that takes one.

ClientVersionAdds
Python SDK (deepsieve)0.4.0blueprint_id wherever workspace_id was, and the parameter_renamed warning; v1_holds (list, apply, keep, release) and its five error codes; model_allowance on the identity; model_allowance_exhausted, merge_across_parents, and blueprint_forbidden (was workspace_forbidden); included re-research used, on monitoring
MCP server (deepsieve-mcp, stdio)0.6.0blueprint_id on every tool that names a Blueprint; list_held_cells, apply_cell_finding, keep_cell_value, release_cell; overwrite_changes on undo_change
CLI (deepsieve-cli)0.5.0deepsieve holds list|apply|keep|release; --by on changes list and --overwrite-changes on changes undo; exit code 5 when the organization's model allowance is used up. --blueprint is now required on runs create unless --dry-run

The browser-login MCP server already has all of it.

2026-09-27 — an organization-wide model allowance: 429 model_allowance_exhausted

Report chat, Blueprint onboarding and the query/guidance parsers now share one allowance per organization: 1,000,000 input and 1,000,000 output tokens, counted from what the model provider reports. Research runs never count.

  • On a paid plan it resets every billing period. At the cap those calls are refused 429 model_allowance_exhausted (a new code, not retriable now), with Retry-After set to the reset.
  • A free organization's allowance is for its lifetime. At the cap: 402 plan_upgrade_required with param: "model_allowance", and no Retry-After: subscribe to continue.
  • GET /v1/me gains model_allowance (used vs limit, input and output, resets_on; absent when it does not apply), and a counted call's response near either cap carries X-DeepSieve-Warning. The CLI exits 5 on model_allowance_exhausted.

POST /v1/merges now re-points every many-to-many link to a merged row: the all_<x>_ids list, not only primary_<x>_id. Before, the list kept the deleted row's id, so the row dropped out of reports that join through it. Undoing the merge puts those links back.

If links to a merged row are being written while the merge runs, it retries once, then refuses 409 merge_records_changed (a new code, retriable: send the same call again). Nothing is written.

2026-09-26 — a merge across parents is refused: 409 merge_across_parents

POST /v1/merges and POST /v1/merges/preview now refuse rows of an entity whose names are unique per parent (for example a fund per company) when those rows sit under different parents. Before, such a merge went through, but it could not stick: a merge keeps names together under one parent only, so the other parent's next run re-created the row.

The refusal is 409 merge_across_parents (a new code, not retriable, param: "loser_ids"), with a message naming the parents. Nothing is written. Merge only rows under the same parent.

2026-09-26 — Keep mine: keep your value and dismiss research's finding

  • POST /v1/cells/{cell_id}/keep keeps the value a person set and dismisses the held cell's pending_finding. The cell stays protected and its value is unchanged. The finding stays in GET /v1/cells/{cell_id}/changes, as change: "observed", with a note that it was dismissed.
  • Research finding a dismissed value again from no new page does not raise it again. A different value, or the same value from a new page, becomes a new pending_finding. "The same page" folds the scheme, host case, www., a default port, percent-encoding, a trailing slash, the fragment and query-parameter order.
  • Send the finding_evidence_id you showed your human, as for apply: a newer one since is 409 finding_changed, and nothing is dismissed. New error code: 409 nothing_to_keep (nothing pending). Needs data:write and the member role; send Idempotency-Key.
  • MCP: keep_cell_value on the browser-login server now, and in the next deepsieve-mcp. CLI: deepsieve holds keep in the next deepsieve-cli. SDK: v1_holds.v1_holds_keep_cell_value in the next deepsieve.

2026-09-26 — workspace_id is renamed blueprint_id (30 days to switch)

The id GET /v1/blueprints lists is now called blueprint_id everywhere on /v1. The old name keeps working for 30 days after this ships, and every response to a call that uses it tells you so.

WhereWasNow
GET /v1/export?workspace_id=?blueprint_id=
POST /v1/keys bodyworkspace_idblueprint_id
ApiKey (list, create, rotate)workspace_idblueprint_id (and workspace_id, deprecated, for the window)
GET /v1/members rowsworkspace_idsblueprint_ids (and workspace_ids, deprecated, for the window)

If you change nothing:

  • Until the sunset, your calls work. Each response carries Deprecation and Sunset headers (the Sunset date is when the old name stops), a Link to this changelog, an X-DeepSieve-Warning header, and a warnings: [{code: "parameter_renamed", message}] entry on the key or in the export's metadata record. A CSV export has no metadata record, so it gets the headers only. A call that uses the old name and then fails for another reason (an unknown entity, a 403) carries the same headers.
  • After the sunset, a call using workspace_id gets 400 parameter_renamed (a new code), which names blueprint_id.
  • Sending both names with different values gets 400 invalid_request. It is refused rather than guessed.

If you have saved skills or instructions that use workspace_id, ask your user for permission to update them to blueprint_id.

Also breaking: workspace_forbidden is now blueprint_forbidden

The error code is renamed. Its 403 is now also RAISED in a place it wasn't before: when a member limited to specific Blueprints (or a key whose creator is) names one outside those grants, for example GET /v1/export?blueprint_id= or GET /v1/blueprints/{blueprint_id}. Those calls used to get 403 insufficient_role, which pointed at the wrong fix: a role change does not grant a Blueprint. Handle blueprint_forbidden as "ask an organization admin for access to this Blueprint".

Reading the old response fields

ApiKey.workspace_id and the member rows' workspace_ids stay in responses, marked deprecated in the contract. Reading them triggers no warning, since the server cannot see what you read. The Python SDK generated from this release marks them deprecated, so Python raises a DeprecationWarning when you access them. Removing these response fields is a separate, later step, announced here on its own, not part of the workspace_id sunset: SDK releases before this one treat workspace_ids as required and would fail to parse a member list without it. Upgrade before that step is announced.

Clients

The browser-login MCP server takes blueprint_id now, and passes a workspace_id you send through unchanged, so you see the warning (for a CSV export, in front of the returned text). The Python SDK and deepsieve-mcp send workspace_id only if your code passes it, so switch the argument you pass. Their next releases add blueprint_id. deepsieve-cli never sends workspace_id; members list --json prints the member rows as they come, so it shows both blueprint_ids and the deprecated workspace_ids.

2026-09-26 — starting a run, pricing one, or adding a webhook names the Blueprint (breaking, before GA)

A call that spends money, or subscribes an endpoint for good, now has to say which Blueprint it is for. Until now, leaving blueprint_id out meant your active Blueprint, and a caller with several could start, and pay for, a run in one it never meant.

CallWasNow
POST /v1/research/runsblueprint_id optional; omitted = your active Blueprintrequired, unless dry_run: true
POST /v1/research/runs/estimateoptional; omitted = your active Blueprintrequired
POST /v1/webhooksoptional; omitted = your active Blueprintrequired

If you change nothing, these calls return 400 invalid_request with param: "blueprint_id", and the message says where the id comes from: GET /v1/blueprints. The refusal comes first: nothing is charged, no query is parsed, and the Idempotency-Key is not used up, so retry with the id under the same key.

  • A dry_run may still leave it out (it is free and saves nothing) and uses your active Blueprint. If you do send an id on a dry run, it is checked exactly as on a real run, so a wrong id shows up before you pay.
  • The field stays optional in the OpenAPI schema so that a generated client does not refuse a valid dry run. The description and the 400 carry the rule.
  • Reads, reports and the run list are unchanged: blueprint_id is still optional there.

Clients

The released clients cannot send blueprint_id on these calls: Python SDK (deepsieve) 0.3.4, deepsieve-mcp 0.4.0 and deepsieve-cli 0.3.0, and anything earlier. With them, a real run and an estimate get the 400 above; a dry run still works. Upgrade to at least deepsieve 0.3.5, deepsieve-mcp 0.5.0 and deepsieve-cli 0.4.0:

  • SDK: v1_research_create_run, v1_research_estimate_run and v1_webhooks_create_webhook take blueprint_id.
  • MCP: start_research and estimate_research take blueprint_id as a required argument (from list_blueprints). The browser-login MCP server has it now.
  • CLI: deepsieve runs create needs --blueprint <id> unless --dry-run, and stops before sending anything without it. deepsieve whoami shows your active Blueprint's id.

2026-09-26 — a report's rows say which cells a person set

GET /v1/reports/{id}/tabs/{tab_id}/data rows carry holds, {column: {source, set_at, pending_finding}}, for the cells a person set on purpose. In a pivot tab a hold also carries address ({entity_key, record_id, column}), since a pivot key is a generated column. To act on one, find its cell_id in GET /v1/holds and use /v1/cells/{cell_id}/apply or release.

2026-09-26 — a held cell with no citation still says it is held

  • In GET /v1/data (with receipts), a cell a user set on purpose now carries its hold even when no live evidence row stands behind it. That cell's citation has value: null, cited: false, verdict: "not checked" and evidence_id: null: Citation.evidence_id is now nullable, for that case only. Check for null before using it as an id.
  • A list value written as JSON text (["A", "B"]) is stored as that list, not split on its commas.

2026-09-26 — merges DeepSieve's dedup made say so

  • GET /v1/changes items gain by: "operator" for a merge DeepSieve's own duplicate cleanup made, and ?by=operator lists just those. They were by: "user" before; they are not your human's merges. Undo works the same way.
  • Those merges now follow the same rules as yours: a value a person set on purpose wins, and each merge is undoable.

2026-09-26 — a value you set wins; apply or release it over the API

  • A cell a user set on purpose is no longer overwritten by research. A rerun, an expiry re-check or a research update keeps what it found BESIDE the value instead. Each cell citation from GET /v1/data now carries hold (null, or why it is held and the pending_finding), and GET /v1/cells/{cell_id}/changes lists the finding as change: "observed".
  • GET /v1/holds lists those cells; pending=true narrows to the ones with a newer finding. POST /v1/cells/{cell_id}/apply takes the finding (the cell stays protected). POST /v1/cells/{cell_id}/release stops protecting the cell. Both need data:write and the member role.
  • New error codes, all 409: cell_not_held, nothing_to_apply, finding_changed (research found something newer than the finding you named: nothing was written), and finding_unusable (research's value cannot be applied whole: too long, not valid JSON, or no longer accepted by the column; edit the cell instead).
  • MCP: list_held_cells, apply_cell_finding, release_cell on the browser-login server now, and in the next deepsieve-mcp. CLI: deepsieve holds list|apply|release in the next deepsieve-cli. SDK: v1_holds in the next deepsieve.

2026-09-25 — merge choices hold; a merge undo asks before overwriting

  • A tie you settle when merging (choices) is now a value set on purpose: later research stores its finding beside it instead of overwriting it.
  • In a merge, a value a person set on purpose beats research. Conflicts gain basis: "held"; two such values are a tie.
  • Undoing a merge whose cells changed since is 409 conflict naming them. POST /v1/changes/{change_id}/undo takes {"overwrite_changes": true} to undo anyway. MCP undo_change(overwrite_changes=True); CLI deepsieve changes undo --overwrite-changes.

2026-09-25 — a rename in Report Chat sticks

  • Renaming a row through Report Chat (a change to name) now keeps the old name as an alias, as an automatic rename does. So later research that finds the row under its old name updates it instead of adding a duplicate.
  • The rename is listed in GET /v1/changes with by: "user", and undone there or by reverting the chat change. GET /v1/changes gains a by filter (user, automatic).
  • A chat rename to a name another row already has is refused (two rows cannot share a name: mark them as duplicates). One that only resembles another row's name waits for confirmation with a new warning code, name_matches_another_row.

2026-09-25 — exports say where each row came from and went

Every record in GET /v1/export and GET /v1/blueprints/{blueprint_id}/export (ndjson and json), and every row of the app's JSON export, gains _identity (underscored so it can never collide with one of your columns):

  • formerly: the names a rerun renamed the row from;
  • succeeded_by: {id, name} of the record that replaced it, or null;
  • succeeded_from: the records it replaced;
  • merged_from: the names of rows merged into it.

Additive: every field is always present, undone changes are left out, and a free-preview export references only rows it contains. The app's CSV export gains a "Succeeded from" column, as its last column, and its "Merged from" column now stays filled when the surviving row is renamed after the merge.

2026-09-25 — client releases: deepsieve 0.3.5, deepsieve-mcp 0.5.0, deepsieve-cli 0.4.0

These releases carry what the entries below announce for "the next release" of each client. Upgrade before blueprint_id becomes required on starting a run, the estimate and creating a webhook endpoint: the earlier releases cannot send it on those calls.

ClientVersionAdds
Python SDK (deepsieve)0.3.5blueprint_id on runs, estimates, reports and webhooks, and blueprint on their responses; v1_merges; v1_changes; chat confirm / dismiss and the new ChatAction fields
MCP server (deepsieve-mcp, stdio)0.5.0blueprint_id on start_research, estimate_research, list_runs, list_reports, get_report_columns, create_report and list_webhooks; preview_merge, merge_records, list_changes, undo_change; confirm_chat_action, dismiss_chat_action, revert_chat_action
CLI (deepsieve-cli)0.4.0--blueprint on runs create and runs list, and the Blueprint in whoami; deepsieve merge preview|apply; deepsieve changes list|undo; deepsieve chat thread|confirm|dismiss|revert

The browser-login MCP server already has all of it.

2026-09-25 — mark rows as duplicates over the API

  • POST /v1/merges/preview shows what merging rows would do and writes nothing. POST /v1/merges merges them, as the app's "Mark as duplicate" does. The result's change_id undoes it with POST /v1/changes/{change_id}/undo.
  • A tie is never settled silently. Where two rows have equally good evidence for different values, the merge is refused with a new error code, 409 ties_unresolved, until each tie has a choice in choices or you send accept_survivor_on_ties: true.
  • choices is strict: a key that matches no conflict, or a choice on a conflict the evidence already decided, is a 400.
  • MCP: preview_merge and merge_records on the browser-login server now, and in the next deepsieve-mcp. CLI: deepsieve merge preview|apply in the next deepsieve-cli. SDK: v1_merges in the next deepsieve.

2026-09-25 — applying or reverting a chat change needs data:write (breaking, before GA)

A chat answer can change your data, and changing data now needs the data:write scope and the member role, the same as undoing a merge.

  • POST /v1/chat/actions/{action_id}/confirm and /revert need data:write. Without it they are refused 403 insufficient_scope (or insufficient_role below member). Dismissing still needs only runs:write: it writes nothing.
  • A chat turn sent without data:write still answers. Every change it proposes waits (status: pending_confirmation, warning no_data_write) for a member to apply it.
  • The thread gains can_apply: whether the caller reading it may apply or revert.

⚠️ Keys store their scopes when they are minted. An agent or cli key minted before data:write existed does not have it, so its chat changes now wait and it cannot confirm or revert. Mint a new key, or run deepsieve login again. In the app, a teammate with the viewer role can still ask questions, but can no longer apply or revert a change.

A re-check (a chat reverify, or the Re-verify button on a cited value) starts a paid research run, so it now needs what starting any run needs: runs:write and the member role. A turn sent without them gets that re-check back as failed with the reason, before anything is charged or started.

2026-09-25 — a member limited to some Blueprints acts only on those

A member can be limited to specific Blueprints. Calls that name no Blueprint used to act on the organization's oldest Blueprint even when the member was not granted it. They now act on one the member is granted, for an API key, a browser-login MCP session and the web app alike. An API key follows its creator's grants as they are NOW, checked on every request.

  • pin_effective on an API key (GET /v1/keys, and the create and rotate responses): false when the key's workspace_id names a Blueprint its creator is no longer granted, so the pin is ignored, and whenever the key cannot act at all (revoked, expired, or its creator is no longer a member). null for an unpinned key.
  • no_blueprint_access (403, not retriable): the member is granted no Blueprint, so there is nothing to act on. Ask an organization admin to grant one. GET /v1/me (blueprint: null), GET /v1/blueprints (an empty list) and GET /v1/members still answer.

Both are additive. A member who is not limited to specific Blueprints sees no change.

2026-09-25 — chat changes can wait for confirmation (breaking, before GA)

A change the research agent makes from chat can now come back with "status": "pending_confirmation" and a warnings list instead of being applied. Examples: every source it cites is outside the run's allowed sources, or the new value is the old one in another unit. Nothing is written until someone applies it anyway with POST /v1/chat/actions/{action_id}/confirm, or declines it with POST /v1/chat/actions/{action_id}/dismiss. A waiting change expires after seven days and then reads "status": "dismissed".

⚠️ Breaking, before GA. Two things change for an existing integration:

  • ChatAction.status gains two values, pending_confirmation and dismissed. It is an output enum, so an exhaustive matcher must handle them.
  • Changes that used to be applied, or used to fail, can now wait. A change whose every source was outside the run's allowed sources used to be failed; it now waits. A change the checks warn about (a unit conversion, a terminal or identity column, a cited page stating an adjustment) used to be applied at once; it now waits too.

The action also gains warnings, initiated_by, source_urls, expires_at, confirmed_at, confirmed_via, dismissed_reason and dismissed_via. The MCP server gains confirm_chat_action, dismiss_chat_action and revert_chat_action in the release after deepsieve-mcp 0.4.0, and the CLI gains deepsieve chat thread|confirm|dismiss|revert in the release after deepsieve-cli 0.3.0. The browser-login MCP server has them now. See Chat.

  • GET /v1/changes lists the record-level changes in a Blueprint, newest first: merge (a person marked rows as duplicates), renamed and succeeded_by (a rerun renamed a record, or linked it to the record that replaced it). Filter by entity, kind and live.
  • POST /v1/changes/{change_id}/undo undoes one, as the app's Undo does, and honours Idempotency-Key.
  • A new scope, data:write, for changing stored data outside a run. It is in the agent, cli and full presets and the MCP browser consent. It has a role floor: a caller below member is refused 403 insufficient_role whatever its key or token carries. API keys store their scopes when they are minted, so an existing agent or cli key does not have it: mint a new key, or run deepsieve login again. An MCP connection made through the browser without asking for particular scopes gets the default set on each request, so a member's existing connection has data:write from its next request.
  • MCP: list_changes and undo_change; the browser-login MCP server has them now, and deepsieve-mcp gains them in its next release. CLI: deepsieve changes list and deepsieve changes undo in the next deepsieve-cli release. SDK: v1_changes in the next deepsieve release.

2026-09-25 — measurement columns: a number and its unit as one value

A Blueprint column can now be a quantity: a measurement such as a length, a weight or a duration. Its value on /v1/blueprints/{blueprint_id}/entities/{entity_key} is an object, never a bare number:

json
{"length_limit": {"value": 1800, "unit": "in"}}

The column in entity.columns carries type: "quantity" plus:

  • unit: the unit new values are converted into, or null when the unit may differ per row. Each value carries its own unit, and a value stored before a unit change keeps its own, so read unit from each value.
  • dimension: what every value measures (length, mass, …) when the unit may differ per row.

Both keys are present only on quantity columns, so every other column's descriptor is unchanged. CSV exports write a quantity as 1800 in. A value the pipeline would have had to guess a unit for (no unit, tons, a weight in a length column, a unit its own citation contradicts) is not stored. Its citation carries a [NON-CONFORMING] verdict_note explaining why.

This is additive. No existing column changes type.

2026-09-25 — progress.phase reports the stage a run actually reached

A running research run's progress.phase could stick on linkedin_research through qa and persist, and otherwise return an internal stage name that isn't in the documented list. It now reports the furthest documented stage the run has reached. A run whose Blueprint has no linkedin_url column never reports linkedin_research, because that stage has nothing to fill.

A test run (dry_run: true) now steps through the stages a real run on your Blueprint reaches, including qa and persist. It no longer ends on linkedin_research for every Blueprint.

The documented values are unchanged.

2026-09-24 — every Blueprint-scoped response names its Blueprint

Additive: nothing you send today changes meaning, and no field is removed.

Responses say which Blueprint they acted on. Runs, the run list, estimates, the monitoring overview, reports, report columns, webhook endpoints, chat messages, threads and actions, and GET /v1/me now carry blueprint: {id, name} — id is what GET /v1/blueprints lists, name is what the app's Blueprint switcher shows. An API key row names the Blueprint it is pinned to. null means there is none (an unpinned key, an org with no usable Blueprint, or a webhook list that spans every Blueprint).

Name the Blueprint you mean. POST /v1/research/runs, the estimate, POST /v1/reports and POST /v1/webhooks accept blueprint_id; the run, report, column, webhook and monitoring reads take ?blueprint_id=. Omitted, they act on your ambient Blueprint (the active one: is_active in GET /v1/blueprints), exactly as before — and the response now says which one that was. A report's config is checked against the Blueprint the report is saved in. To confirm the Blueprint before a run spends, read blueprint on the estimate or a dry_run: by the time a create returns 202 the run has started.

An id is found wherever you can read it. A run, report, webhook endpoint or chat action id is found in any Blueprint your key can read, not only the ambient one, and the response names that Blueprint. An id in a Blueprint you cannot read is still a 404.

Webhook endpoints are per Blueprint, and events say so. An endpoint receives only its own Blueprint's events. Every delivery carries a top-level blueprint, and GET /v1/webhooks without a filter now lists the endpoints of every Blueprint you can see. A member who can see no Blueprint sees no endpoints.

Clients. The Python SDK gains BlueprintRef and ReportCreate models (the create body, which alone takes blueprint_id) and optional blueprint_id arguments; both MCP transports add blueprint_id to start_research, estimate_research, list_runs, list_reports, get_report_columns, create_report and list_webhooks; the CLI's whoami prints the Blueprint, and runs create and runs list take --blueprint. All from their next releases.

Coming next. blueprint_id will become required on the calls that spend or subscribe (starting a run, the estimate, and creating a webhook endpoint), so a costly call can never land on the wrong Blueprint by default. That change is announced here before it ships.

2026-09-24 — agents can create an additional Blueprint; plan limits are typed

An agent may now create a Blueprint on an org that already has one. POST /v1/blueprints/drafts and /approve refused every API key and OAuth caller with 403 insufficient_scope whenever the workspace had an active Blueprint, and told them to create a new workspace. The draft is now created in its own workspace, so approving it builds a new Blueprint beside the live one and never changes the live one. Editing a live Blueprint is still human-only.

Blueprint plan limits return 402 plan_upgrade_required (param blueprint) instead of 402 payment_required, on both draft creation and approve. The message names how many Blueprints the plan covers. Topping up credit does not lift it; a human has to change the plan.

deepsieve login --preset agent requests the agent preset, which can create Blueprints. Plain deepsieve login is unchanged and cannot.

2026-09-23 — ChatAction.table_name is a plain string

The contract published table_name on a chat action as the enum companies / funds / individuals. Those are one Blueprint's tables. The API returns the tables of your Blueprint here, so the enum was wrong for every other workspace. It is now string: on an applied action, an entity key from GET /v1/blueprints/{blueprint_id}. A failed action echoes the table the agent named, which may not exist, and the field is null when the agent named none.

Nothing changes on the wire. In the Python SDK the deepsieve.models.TableName type is removed and ChatAction.table_name is typed str, from the next SDK release. If you imported TableName, use str.

The contract also no longer lists 20 schemas that no /v1 endpoint returns or accepts (among them TableResponse, RunSummary, EvidenceCell and ErrorResponse). None was reachable from an endpoint, and none was in the SDK.

2026-09-18 — change history per cell, and source allowlists a run enforces

Two additions, both additive. Existing fields are unchanged.

Change history

EndpointReturns
GET /v1/blueprints/{blueprint_id}/entities/{entity_key}/columns/{column}/changesthe log of one column across records, newest first
GET /v1/cells/{cell_id}/changesevery value one cell has ever held, newest first

Each entry carries value, previous_value, captured_at, the writing run_id, source_urls, confidence_label, qa_notes and a derived change label: initial, changed, unchanged, observed (seen but outside the column's allowed set) or unknown (no link was recorded, which is stated rather than inferred). Every entry also carries a persistent cell_id, the handle for the second endpoint. It is opaque but not secret: do not build or parse one, and expect it to survive runs and rewrites but not an entity merge. The cell log takes include=rechecks to show re-checks that found a change and did not apply it (change: "not_applied").

Under the free-preview cap the column log returns preview rows only and cursor / since are 402; read truncated.

Sources a run enforces

allowed_sources on POST /v1/research/runs and /runs/estimate: a list of hosts, wildcard suffixes (*.gov), exact-host URL prefixes, or blueprint:primary / blueprint:trade. A cell cited only from outside the list is not written and its URLs come back as a lead. [] is a 400; omit the field for no restriction. The run resource echoes the resolved list and allowed_sources_origin; the estimate's warnings[] says when a list is bound or unusually narrow; stage_metrics.persisting.allowed_sources reports what the filter did.

Clients

Upgrade to at least:

ClientVersion
Python SDK (deepsieve)0.3.4 — v1_lineage_read_column_changes, v1_lineage_read_cell_changes, allowed_sources on create and estimate
MCP server (deepsieve-mcp, stdio)0.4.0 — get_column_changes, get_cell_changes, allowed_sources on start_research and estimate_research
CLI (deepsieve-cli)0.3.0 — runs create --allowed-source (repeatable); setup mcp now probes whether the deployment serves browser OAuth and prints the stdio registration when it does not, saying so when it could not tell

The browser-login MCP server always serves the current tool set.

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.

WasNow
affordable: false, shortfall_cents: 1000affordable: 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.

WasNow
POST /v1/blueprints/proposePOST /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:

ClientVersion
Python SDK (deepsieve)0.3.0
MCP server — the stdio package and browser login alike0.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 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

WasNow
GET /v1/dataGET /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 operationIds, so generated SDK method names do not change.

Clients

Upgrade to at least:

ClientVersion
Python SDK (deepsieve)0.2.0
CLI (deepsieve)0.2.0
MCP server — the stdio package and browser login alike0.2.0

deepsieve data catalog and deepsieve data get resolve the active Blueprint for you, and take --blueprint <id> 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.
Changelog — DeepSieve API