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.
| Package | Version | What it adds |
|---|---|---|
Python SDK (deepsieve) | 0.9.0 | SelectionState 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.0 | get_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.0 | deepsieve 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_planandexpired_from_plan_label: the termed plan whose term ended (for examplebeta, "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:
SelectionStategainsmerged. A merged cell'sselection(on a/v1/datacitation, a conflict, a choose reply, and each change-log entry) carriesitems: each item of the list, in the cell's order, withevidence_ids(the versions that stated it) andsource_urls(their sources).itemsisnullfor every other state.- The versions stay in the citation's
beside[]asalso_stated,choosable: true: choose one withPOST /v1/cells/{cell_id}/chooseto 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:mergedon 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_repickgainslists_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).
| Package | Version | What it adds |
|---|---|---|
Python SDK (deepsieve) | 0.8.0 | v1_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:
GETandPOST /v1/blueprints/drafts/{session_id}/knowledge,PATCHandDELETE …/knowledge/{kid},POST …/knowledge/{kid}/retireandPOST …/knowledge/{kid}/restore. They take the live Blueprint's bodies and rules (revision,409 knowledge_revision_conflict,409 knowledge_conflictanswered withon_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 needblueprint:create(editing a draft is creating a Blueprint), notknowledge:write. Send anIdempotency-Keyon writes. - New refusals on the writes:
409 session_busywhile a change to the draft is running (retriable,Retry-After), and409 conflictwithdetails.reasonno_draft(the draft does not exist yet) oralready_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, markedskippable: true.POST /v1/blueprints/drafts/{session_id}/answerstakes{question_id, skip: true}for it: recorded asnull, nothing re-runs, nothing is metered. Skipping any other question is a400. Ask your human first; never answer it from your own knowledge. - MCP:
list_knowledge,add_knowledge,edit_knowledge,retire_knowledge,restore_knowledgeanddelete_knowledgetakedraft_id, andanswer_blueprint_questiontakesskip. CLI:deepsieve knowledge list|add|edit|retire|restore|deletetake--draft <session-id>. The CLI's default sign-in does not carryblueprint:create, so to write a draft's notes sign in withdeepsieve login --preset agent. Both arrive in the 0.10.0 releases ofdeepsieve-mcpanddeepsieve-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/canceland/resume) gainsretry_on, a date ornull; - a resume's
retrycan beawaiting_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_onis the first day a retry can go through (on that day the open payment is cancelled);nullwhen 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, ornot_checkedwithnot_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 entryout_of_scope.can_decideis false for the five kinds that only steer prompts. decisionscounts decision events, not cells.citedstays QA's claim. A run with no record counts inwindow.not_recordedand in no entry's figures.- MCP:
get_knowledge_usageon both transports (deepsieve-mcp0.10.0 on stdio). CLI:deepsieve knowledge usage, indeepsieve-cli0.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/holdsitems, a citation'shold) gainschoice_id: on aconflict_choicehold, thechoicechange that made it, so you can undo it withPOST /v1/changes/{change_id}/undo(thechoice_idis the change id) without having kept the choose reply;nullfor every other hold; - each entry of a cell's or column's change log gains
previous_selection, the linked earlier value'sselection.state: "conflicting"means this entry replaced a value a run had stated more than one way before anyone chose;nullwhen 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_knowledgeandget_run_knowledge, one per route under/v1/blueprints/{blueprint_id}/knowledgeandGET /v1/research/runs/{run_id}/knowledge.query_entitiesnow describes a cell'sknowledge_decidedandknowledge_cited. - CLI:
deepsieve knowledge list|add|edit|retire|restore|deleteanddeepsieve runs knowledge <run-id>. A knowledge conflict or a stale revision exits2; a sign-in that cannot write knowledge yet exits3. knowledge:writejoins 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) anddeepsieve-clicarry 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};stateispicked,repicked,conflictingorchosen(SelectionState), or the field isnullwhen the run stated the cell one way. Itsbesideentries gainchoosable(true foralso_stated) andrefused_by, the source-caution entries that kept that value out when the run chose; GET /v1/conflictslists theconflictingcells, each withcandidatesto choose from;POST /v1/cells/{cell_id}/choosewith{evidence_id, expected_value_evidence_id}shows one of them, holds the cell, and records achoicechange inGET /v1/changes(its undo shows the earlier value again). New409codes:choice_changed,not_a_candidate,nothing_to_choose,cell_held;- a hold's
sourcecan beconflict_choice; the run resource'smetricsgainsconflicting_cells(nullwhen 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 stagepersist(by_stage.persist); source_refusedis 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. Arange_flagcarries therangethe value breached as it stood when it was flagged, andrevisionthe entry's revision then; a flag recorded before this release has neither, so do not explain it with the entry's current range.run_idnames 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 avalue_meaningentry'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 asource_cautionentry names;source_downscored(new): a cell QA called supported, capped atleans-supportedbecause 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:
carriedanddropped: 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 producesheld_valueonly;range_flagandsource_refusedfollow.
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.conflictslists them; resend withon_conflict),409 knowledge_revision_conflict, and503 blueprint_busy(another write to the same Blueprint is in progress; retriable, honourRetry-After). - The Python SDK (
deepsieve) hasv1_knowledgefrom 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.
2026-10-19: the browser session cookie is __Host-ds_session
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 newfunding: {rail, credits_drawn, prepaid_cents}says what pays and how it splits (rail: "mixed"when both do).affordableandshortfall_centsnow count only the balance's part, so a run the credits partly fund can beaffordable: truewhere it wasfalse. Every other field keeps its meaning (plan_credits_coveris still "the credits alone").POST /v1/research/runs/{run_id}/retry:funding.covered_bycan bemixed, withfunding.prepaid_centsthe balance's part. Confirm it withexpected_funding: "mixed"ANDexpected_prepaid_centsset to that value; without it the confirmation is422 validation_failed(param: expected_prepaid_cents), and a different split is409 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-cli0.9.0 (--expected-prepaid) anddeepsieve-mcp0.9.0 (expected_prepaid_centsonretry_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:
| Org | Before | Now |
|---|---|---|
| no plan, free run used, balance covers the price | affordable: true, shortfall_cents: 0 | affordable: false, shortfall_cents: 0 |
| no plan, free run used, balance unknown | affordable: null | affordable: false, shortfall_cents: null |
| no plan, a Max run, or an ended Builders Trial, or a closed account, balance covers it | affordable: true | affordable: false |
| the same, balance unknown | affordable: null | affordable: 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.
| Package | Version | What it adds |
|---|---|---|
Python SDK (deepsieve) | 0.7.0 | v1_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.0 | the 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.0 | deepsieve 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,/v1calls 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 retriable500. - Approving such an id answers
409 no_active_blueprint, as/v1already did. 403 no_blueprint_accessnow also names "the organization has no Blueprint yet" among its reasons. The code and status are unchanged.- The web
POST /api/onboarding/sessionsnow declares its409in the contract. It was already served, with a plaindetailmessage, when the organization holds the maximum number of unfinished drafts. It is now also served, as a typedconflict, 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_blueprintas an id that exists nowhere. It was404, 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.
| Package | Version | What it adds |
|---|---|---|
Python SDK (deepsieve) | 0.6.0 | a 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.1 | get_run_status explains a re-check's results and its follow-up search |
CLI (deepsieve-cli) | 0.7.0 | deepsieve 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
403to409, and the code fromblueprint_forbiddentono_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_accesson 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_resultsentry readsstopped. The message says whether that value had already been checked. - A re-check that monitoring started cannot be interrupted. It finishes, then reads
cancelledwith every result. - A search stopped before it saved anything, or while it was saving, reads
update_result.outcomestopped, and the message says which. A search stopped after it saved a value keeps that value, and itsupdate_resultsays 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, withevidence_id,outcome(a stable key),message(the sentence to show your human) andfollowup_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
nullon every other run, until the run finishes, and on a run that ended before it could record a result (statusanderrorsay why).outcomeis an open, documented string: read a key you don't know as neutral. A chatreverifyaction'sreverify_run_idis 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:truewhen every call reported its usage;falsewhen at least one did not, so the counts are a floor;nullwhen unknown (a run from before this, one still running, or one executed across an API restart). Never readnullas complete.metrics.usage_unreported_stages: where it happened, such asinvestigate.- On
GET /v1/research/runs/{id}and every response carrying the run resource, MCPget_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_existingandtemporal_cutoff, the create's own: withreuse_existing, the seeds you already hold current data on are priced out and listed in the new response fieldreuse_predicted. - A known free run is priced on the entities it researches (
entities,interactionsandrun_price_centscount 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_toreport, or every seed predicted held underreuse_existing, is a422with 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}]onPOST /v1/research/runs/estimate, on the 202 fromPOST /v1/research/runs(and itsdry_run), and on the app's preview.kept_asis the spelling kept and researched;reasoniscase,same_record,legal_formoralias(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.seedson 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(onPOST /v1/research/runsand 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, inGET /v1/blueprints/{blueprint_id}/entities/{entity_key}and JSON exports) gainsrerun_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".
ChatWarningCodegainsconfirm_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_proposedstill marks the agent's own idea.initiated_byis the agent's claim about who asked. It is shown to help you decide and changes nothing about what happens.confirm_required, likemodel_proposedandno_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.
| Client | Version | Adds |
|---|---|---|
Python SDK (deepsieve) | 0.5.0 | run 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.0 | retry_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.0 | deepsieve 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
querygives 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, namedP01 (https://…). The run reportsseed_mode: "as_typed"("discover"is the old behaviour, unchanged for every other run, and always the mode of aseedsrequest), and so does the estimate. - Labels:
POST /v1/research/runstakesseed_labels({seed: label}, e.g."P01"beside a URL). With aquery, the parser reads labels you wrote (P01: https://…). The run resource returnsseed_labels, and a labelled target is named by its label in the run's records. - In
as_typedthe estimate no longer raisesallowed_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.previewis the merge as it stands now, with its ownpreview_digest. - Sending neither is
422 preview_required; sending both is a400. - The result's new
preview_checkedistruewhen the digest matched,falsefor 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: truereads each URL seed now and returnsurl_outcomes, one per URL, in the order sent:found(with a count),none_foundorunreadable. 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_hintand reads nothing. warnings[]gainsallowed_sources_seed_hosts_onlywhen 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: itsvalue,verdictandsource_urlsare that row's. - The rows beside the cell are listed in the citation's new
besidearray, each with itskind, its ownverdict,source_hostsandnote, itsvalue(never forwithheld), and fornot_appliedareasonin plain words. Read abesideverdict 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 inbeside.
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_appis 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/changesand MCPget_cell_changesshow it asobserved, not as achangedvalue.- 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.
2026-09-30 — remove a wrong link
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(MCPremove_link, CLIdeepsieve 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_removedinGET /v1/changes, undone withPOST /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}/rerunslists 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/approvepays for it. It needsexpected_total_centsandquote_id(both from the list your human approved) and anIdempotency-Key, and buys exactly those rows. If anything waiting changed, even at the same total, it is the new code409 rerun_price_changedwith the new total (read the list again for itsquote_id); too little credit is402 insufficient_credits. Nothing is charged on either. An agent asks its human first.PATCH /v1/research/runs/{run_id}/reruns/auto-approveturns automatic approval off. Turning it on is done in the app for now: an API key or OAuth app gets the new code403 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_rerunsandset_rerun_auto_approve(the browser-login server has them now; the stdiodeepsieve-mcpgets them in its next release), and the CLI commanddeepsieve runs reruns <id> [--approve --expected-total <cents> --quote-id <id>] [--auto-approve-off]in its next release.
2026-09-30 — merging two rows keeps both rows' links
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/previewandPOST /v1/mergesreturnlinks_combined[]: one entry per merged row that added links or changed a held list,{column, to_entity, to_label, added, from, changes_held}.to_labelis the Blueprint's name for the linked entity.changes_heldistruewhen a list a person set on purpose would change. Nothing sets a hold on these lists today, so it is currently alwaysfalse; read it for forward compatibility.- An owner's link columns no longer appear in
conflictsorunresolved_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, andrefundedreadsfalse. cancel.statusreadscancelling, and the run settlescancelled. Stopped between attempts with everything already delivered, it settlescompleted, with nothing refunded.- The advice to wait and send the Stop again is gone: one Stop is enough.
cancel.statusnever reads the internalawaiting_recoverystate 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_requireduntil it is sent withexpected_total_centsequal to the quote'stotal_price_cents. The quote is inerror.details, with the failed run'srefundand what is carried over or skipped at no charge. An agent asks its human before confirming. A moved price is409 retry_price_changedwith 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-Keyis honoured, and a refusal releases it for the confirmation. - New codes:
retry_confirmation_required,retry_price_changed,run_recoveringandrun_stopping(retriable).runs:writeand a member role. - The MCP tool is
retry_run(expected_total_cents, strictly an integer), and the CLI command isdeepsieve runs retry <id> [--expected-total <cents>]. The browser-login MCP server has it now. The stdiodeepsieve-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_requiredwitherror.details.locked_untiluntil it is sent withcommit_lock: true, and an agent asks its human before sending that. Sendexpected_locked_until(the date they agreed to) with it: if a UTC midnight passed meanwhile, the call is409 lock_quote_changedwith the new date and nothing is committed. The error envelope gains an optionaldetailsobject for facts like these. runs:writeand a member role. A run with no monitoring is404 not_found.- The MCP tool is
set_monitoring(commit_lock=True,expected_locked_until), and the CLI command isdeepsieve runs monitoring <id> --on|--off [--commit-lock --expected-locked-until <date>]. The browser-login MCP server has it now. The stdiodeepsieve-mcp, the SDK and the CLI get it in their next releases. GET /v1/research/monitoringreports gaingrace_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/monitoringrows gainunits_in_free_window,billing_units(what the report bills today) andadditive_windows[](run_id,units,free_until,in_free_window). The top-levelbilling_unitsis now the sum of the rows', so an additive run still inside its window is not in it.monitoredis ignored withadditive_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 yourallowed_sourcesapplied.reused_existingstays on the 202 with the same value, for compatibility. - The run resource (
GET /v1/research/runs/{run_id}) gainsreuse_carried(what reuse actually carried),being_refreshed([{name, run_id}]: entities skipped because a refresh of them was already running) andreuse_not_researched(see below). All three are null until the run decides, and on every run withoutreuse_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_existingrun whose every seed is predicted held is refused422even withguidance, 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_blueprintis 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'smessage_idto retry one that timed out without a second model call.start_researchtakesguidanceandmonitoredon both. On stdio this is new:monitoredwas not accepted, so every stdio run was monitored. Passfalsefor a one-off run.seedsandallowed_sourcesonstart_researchandestimate_researchtake a list or one comma-separated string on both.start_researchon stdio now sends an idempotency key on every call, as the browser-login server does. Pass your ownidempotency_keyto 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_unitspast the free 30 days;duplicated_units, the units paid for on reports another report already keeps fresh, with aduplicate_monitoringwarning;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.
| Client | Version | Adds |
|---|---|---|
Python SDK (deepsieve) | 0.4.0 | blueprint_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.0 | blueprint_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.0 | deepsieve 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), withRetry-Afterset to the reset. - A free organization's allowance is for its lifetime. At the cap:
402 plan_upgrade_requiredwithparam: "model_allowance", and noRetry-After: subscribe to continue. GET /v1/megainsmodel_allowance(used vs limit, input and output,resets_on; absent when it does not apply), and a counted call's response near either cap carriesX-DeepSieve-Warning. The CLI exits5onmodel_allowance_exhausted.
2026-09-28 — a merge re-points every link to a merged row: 409 merge_records_changed
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}/keepkeeps the value a person set and dismisses the held cell'spending_finding. The cell stays protected and its value is unchanged. The finding stays inGET /v1/cells/{cell_id}/changes, aschange: "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_idyou showed your human, as for apply: a newer one since is409 finding_changed, and nothing is dismissed. New error code:409 nothing_to_keep(nothing pending). Needsdata:writeand the member role; sendIdempotency-Key. - MCP:
keep_cell_valueon the browser-login server now, and in the nextdeepsieve-mcp. CLI:deepsieve holds keepin the nextdeepsieve-cli. SDK:v1_holds.v1_holds_keep_cell_valuein the nextdeepsieve.
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.
| Where | Was | Now |
|---|---|---|
GET /v1/export | ?workspace_id= | ?blueprint_id= |
POST /v1/keys body | workspace_id | blueprint_id |
ApiKey (list, create, rotate) | workspace_id | blueprint_id (and workspace_id, deprecated, for the window) |
GET /v1/members rows | workspace_ids | blueprint_ids (and workspace_ids, deprecated, for the window) |
If you change nothing:
- Until the sunset, your calls work. Each response carries
DeprecationandSunsetheaders (theSunsetdate is when the old name stops), aLinkto this changelog, anX-DeepSieve-Warningheader, and awarnings: [{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_idgets400 parameter_renamed(a new code), which namesblueprint_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.
| Call | Was | Now |
|---|---|---|
POST /v1/research/runs | blueprint_id optional; omitted = your active Blueprint | required, unless dry_run: true |
POST /v1/research/runs/estimate | optional; omitted = your active Blueprint | required |
POST /v1/webhooks | optional; omitted = your active Blueprint | required |
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_runmay 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
400carry the rule. - Reads, reports and the run list are unchanged:
blueprint_idis 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_runandv1_webhooks_create_webhooktakeblueprint_id. - MCP:
start_researchandestimate_researchtakeblueprint_idas a required argument (fromlist_blueprints). The browser-login MCP server has it now. - CLI:
deepsieve runs createneeds--blueprint <id>unless--dry-run, and stops before sending anything without it.deepsieve whoamishows 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 itsholdeven when no live evidence row stands behind it. That cell's citation hasvalue: null,cited: false,verdict: "not checked"andevidence_id: null:Citation.evidence_idis 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/changesitems gainby: "operator"for a merge DeepSieve's own duplicate cleanup made, and?by=operatorlists just those. They wereby: "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/datanow carrieshold(null, or why it is held and thepending_finding), andGET /v1/cells/{cell_id}/changeslists the finding aschange: "observed". GET /v1/holdslists those cells;pending=truenarrows to the ones with a newer finding.POST /v1/cells/{cell_id}/applytakes the finding (the cell stays protected).POST /v1/cells/{cell_id}/releasestops protecting the cell. Both needdata:writeand 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), andfinding_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_cellon the browser-login server now, and in the nextdeepsieve-mcp. CLI:deepsieve holds list|apply|releasein the nextdeepsieve-cli. SDK:v1_holdsin the nextdeepsieve.
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 conflictnaming them.POST /v1/changes/{change_id}/undotakes{"overwrite_changes": true}to undo anyway. MCPundo_change(overwrite_changes=True); CLIdeepsieve 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/changeswithby: "user", and undone there or by reverting the chat change.GET /v1/changesgains abyfilter (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, ornull;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.
| Client | Version | Adds |
|---|---|---|
Python SDK (deepsieve) | 0.3.5 | blueprint_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.0 | blueprint_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/previewshows what merging rows would do and writes nothing.POST /v1/mergesmerges them, as the app's "Mark as duplicate" does. The result'schange_idundoes it withPOST /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 inchoicesor you sendaccept_survivor_on_ties: true. choicesis strict: a key that matches no conflict, or a choice on a conflict the evidence already decided, is a400.- MCP:
preview_mergeandmerge_recordson the browser-login server now, and in the nextdeepsieve-mcp. CLI:deepsieve merge preview|applyin the nextdeepsieve-cli. SDK:v1_mergesin the nextdeepsieve.
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}/confirmand/revertneeddata:write. Without it they are refused403 insufficient_scope(orinsufficient_rolebelow member). Dismissing still needs onlyruns:write: it writes nothing.- A chat turn sent without
data:writestill answers. Every change it proposes waits (status: pending_confirmation, warningno_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_effectiveon an API key (GET /v1/keys, and the create and rotate responses):falsewhen the key'sworkspace_idnames 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).nullfor 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) andGET /v1/membersstill 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.statusgains two values,pending_confirmationanddismissed. 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.
2026-09-25 — list and undo merges, renames and successor links
GET /v1/changeslists the record-level changes in a Blueprint, newest first:merge(a person marked rows as duplicates),renamedandsucceeded_by(a rerun renamed a record, or linked it to the record that replaced it). Filter byentity,kindandlive.POST /v1/changes/{change_id}/undoundoes one, as the app's Undo does, and honoursIdempotency-Key.- A new scope,
data:write, for changing stored data outside a run. It is in theagent,cliandfullpresets and the MCP browser consent. It has a role floor: a caller below member is refused403 insufficient_rolewhatever its key or token carries. API keys store their scopes when they are minted, so an existingagentorclikey does not have it: mint a new key, or rundeepsieve loginagain. 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 hasdata:writefrom its next request. - MCP:
list_changesandundo_change; the browser-login MCP server has them now, anddeepsieve-mcpgains them in its next release. CLI:deepsieve changes listanddeepsieve changes undoin the nextdeepsieve-clirelease. SDK:v1_changesin the nextdeepsieverelease.
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:
{"length_limit": {"value": 1800, "unit": "in"}}The column in entity.columns carries type: "quantity" plus:
unit: the unit new values are converted into, ornullwhen the unit may differ per row. Each value carries its own unit, and a value stored before a unit change keeps its own, so readunitfrom 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
| Endpoint | Returns |
|---|---|
GET /v1/blueprints/{blueprint_id}/entities/{entity_key}/columns/{column}/changes | the log of one column across records, newest first |
GET /v1/cells/{cell_id}/changes | every 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:
| Client | Version |
|---|---|
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.
| Was | Now |
|---|---|
affordable: false, shortfall_cents: 1000 | affordable: true, shortfall_cents: 0 |
New on the estimate
uses_free_run — whether THIS request would consume the org's one free run.
run_price_cents still quotes the dollar price, exactly as it does for a
credit-covered run, so never infer funding from the price: uses_free_run is the
discriminator. null means eligibility could not be determined (billing inactive,
or the read failed) and never "not free" — when it is null, affordable and
shortfall_cents are null too, which is unknown, not unaffordable.
It is false for depth: max. The free run covers a Standard run; a Max
request is refused before the free run is ever considered, and allowed carries
that refusal.
free_run — present only when the free run's entity cap would trim THIS request.
A free run researches at most one interaction's worth of entities and drops the
rest rather than refusing, so the block names entity_cap, requested,
researched and dropped. The run response returns the same block and is
authoritative: dropped here is computed from the seeds as sent, while the run
narrows first (reuse_existing, additive_to).
New on GET /v1/me
free_run_available — is the org's one free run still unspent, so you can check
trial eligibility before committing to anything. Same nullability rule: null is
"could not determine", never "no free run".
2026-09-08 — breaking, before GA
Proposing a Blueprint change now names the Blueprint.
| Was | Now |
|---|---|
POST /v1/blueprints/propose | POST /v1/blueprints/{blueprint_id}/propose |
If you change nothing, this call returns 404. No alias is kept.
Get a blueprint_id from GET /v1/blueprints. The old path proposed
against whichever Blueprint happened to be active — a choice you never made
and the response never named. With two Blueprints in one account, it could
return a completely coherent proposal about the wrong one.
This is not only a path change. The id now reaches everything the answer depends
on: the Blueprint the proposal is built from, the Blueprint the tier is
classified against, and the live tables checked to decide whether a change would
lose data. A path that took an id and ignored it would have been worse than the
old one, because the id would have implied a control it did not have.
Also in the response
blueprint_id — the Blueprint this proposal is about, echoed in the form
GET /v1/blueprints uses. It is always present.
Name that Blueprint when you hand the change to a person. The
{app_url}/blueprint?ask=… link opens their active Blueprint, which is not
necessarily the one you addressed, and their chat re-derives the proposal from
your text alone. If those differ, the re-derivation succeeds against the wrong
Blueprint — no error. There is no flag to check instead: your active workspace
comes from your API key, theirs from their own switcher, and nothing on this API
reports the second.
Refusals
Addressing a Blueprint explicitly does not widen what you can reach. An id you
could not already read refuses exactly as the other by-id reads do: 404 for
unknown, malformed, or another organization's id (existence is never confirmed
across organizations), 403 for a scoped viewer without a grant, and 409 for
a Blueprint that is still a draft or has no tables yet. A refusal costs no model
call.
Applying is unchanged and still human-only. An agent may ask what a change would be; only a person may make it.
Clients
Upgrade to at least:
| Client | Version |
|---|---|
Python SDK (deepsieve) | 0.3.0 |
| MCP server — the stdio package and browser login alike | 0.3.0 |
The MCP tool propose_blueprint_change takes an optional blueprint_id; omit
it and it resolves the active Blueprint as before. The CLI does not call this
endpoint and is unaffected.
2026-09-07 — breaking, before GA
A dataset read now names the Blueprint it reads. Two endpoints are removed
and six are moved. This is a breaking change before GA, announced here as
Versioning & stability requires: /v1 becomes
additive-only at GA, and until then a break is allowed only when it is
deliberate and appears in this changelog.
If you change nothing, the calls below return 404. That is an ordinary
not-found, so a client that only checks for an error will look like it is
reading an empty dataset rather than calling a route that no longer exists.
Check the path before you check the data.
Removed
| Was | Now |
|---|---|
GET /v1/data | GET /v1/blueprints/{blueprint_id} |
GET /v1/data/{entity_key} | GET /v1/blueprints/{blueprint_id}/entities/{entity_key} |
Same response shape, same query parameters (limit, cursor, updated_since,
fields, receipts), same free-preview cap. One field differs: the catalog's
discriminator is now "object": "blueprint" rather than "object": "data_catalog".
Get a blueprint_id from GET /v1/blueprints, which lists the Blueprints
you can see and marks the active one. That listing is the reason for the change.
GET /v1/data answered for whichever Blueprint happened to be active — a
choice the caller never made and the response never named — so an account with
two Blueprints could receive a completely coherent answer to a question it had
not asked, with nothing in the payload to reveal which one it described. No
alias is kept: a shim with no consumer reads as a supported path.
GET /v1/export is unchanged, and still takes ?workspace_id=.
Moved
/v1/blueprints/onboarding/… → /v1/blueprints/drafts/… — all six: the
collection itself, {session_id}, and its /answers, /adjust, /chat and
/approve. Path only. Same handlers, same request and response bodies, and the
same operationIds, so generated SDK method names do not change.
Clients
Upgrade to at least:
| Client | Version |
|---|---|
Python SDK (deepsieve) | 0.2.0 |
CLI (deepsieve) | 0.2.0 |
| MCP server — the stdio package and browser login alike | 0.2.0 |
deepsieve data catalog and deepsieve data get resolve the active Blueprint
for you, and take --blueprint <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)
-
ApiKeygainsusageonGET /v1/keys—{runs, est_spent_cents}, ornullif 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_existingwas declaredstring; it is aboolean. A pasted property body had silently overwritten it in the contract, so SDK 0.1.1 types itOptional[str]andreuse_existing=Truedoes 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 tonumber) andStartRunResponse.reused_existing(carried the wrong description). -
Freshness overage wording: the per-day rate and
auto_topup_suggesteddescribed 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/codeand/v1/auth/device/token: the OAuth device flow behinddeepsieve 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_keyis now returned for a credential that was presented but not accepted — previously indistinguishable fromauthentication_required, which means no credential was sent at all. -
Citationgainsverdict,verdict_noteandcited. 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
/v1release: typed error envelope + request ids; scoped revocable API keys (/v1/keys); async research runs withIdempotency-Keyand query-first creation; outbound signed webhooks (run.*,dataset.updated); cursor-paginated dataset read (/v1/data) withupdated_sincesync and per-cell citations; NDJSON/JSON/CSV export; RateLimit headers + per-key budgets; MCP server;/llms.txt.