# Changelog

> API changes, newest first


## 2026-09-16 — the cost estimate now accounts for the free run

**`affordable` changes value for trial accounts.** If you branch on it, read this.

`POST /v1/research/runs/estimate` priced a run against plan credits and prepaid
dollars only. An unsubscribed org with its one free run unspent was therefore
quoted `affordable: false` with a `shortfall_cents` — and then
`POST /v1/research/runs` succeeded and charged nothing, because the free run
funds it. An autonomous caller obeying the estimate halted before reaching a run
it could in fact have made.

| Was | Now |
|---|---|
| `affordable: false`, `shortfall_cents: 1000` | `affordable: true`, `shortfall_cents: 0` |

### New on the estimate

`uses_free_run` — whether THIS request would consume the org's one free run.

**`run_price_cents` still quotes the dollar price**, exactly as it does for a
credit-covered run, so never infer funding from the price: `uses_free_run` is the
discriminator. `null` means eligibility could not be determined (billing inactive,
or the read failed) and never "not free" — when it is null, `affordable` and
`shortfall_cents` are null too, which is unknown, not unaffordable.

It is `false` for `depth: max`. The free run covers a **Standard** run; a Max
request is refused before the free run is ever considered, and `allowed` carries
that refusal.

`free_run` — present only when the free run's entity cap would trim THIS request.
A free run researches at most one interaction's worth of entities and **drops the
rest** rather than refusing, so the block names `entity_cap`, `requested`,
`researched` and `dropped`. The run response returns the same block and is
authoritative: `dropped` here is computed from the seeds as sent, while the run
narrows first (`reuse_existing`, `additive_to`).

### New on `GET /v1/me`

`free_run_available` — is the org's one free run still unspent, so you can check
trial eligibility before committing to anything. Same nullability rule: `null` is
"could not determine", never "no free run".

## 2026-09-08 — breaking, before GA

**Proposing a Blueprint change now names the Blueprint.**

| Was | Now |
|---|---|
| `POST /v1/blueprints/propose` | `POST /v1/blueprints/{blueprint_id}/propose` |

**If you change nothing, this call returns `404`.** No alias is kept.

Get a `blueprint_id` from **`GET /v1/blueprints`**. The old path proposed
against whichever Blueprint happened to be *active* — a choice you never made
and the response never named. With two Blueprints in one account, it could
return a completely coherent proposal about the wrong one.

This is not only a path change. The id now reaches everything the answer depends
on: the Blueprint the proposal is built from, the Blueprint the `tier` is
classified against, and the live tables checked to decide whether a change would
lose data. A path that took an id and ignored it would have been worse than the
old one, because the id would have implied a control it did not have.

### Also in the response

`blueprint_id` — the Blueprint this proposal is about, echoed in the form
`GET /v1/blueprints` uses. It is always present.

**Name that Blueprint when you hand the change to a person.** The
`{app_url}/blueprint?ask=…` link opens *their* active Blueprint, which is not
necessarily the one you addressed, and their chat re-derives the proposal from
your text alone. If those differ, the re-derivation succeeds against the wrong
Blueprint — no error. There is no flag to check instead: your active workspace
comes from your API key, theirs from their own switcher, and nothing on this API
reports the second.

### Refusals

Addressing a Blueprint explicitly does not widen what you can reach. An id you
could not already read refuses exactly as the other by-id reads do: `404` for
unknown, malformed, or another organization's id (existence is never confirmed
across organizations), `403` for a scoped viewer without a grant, and `409` for
a Blueprint that is still a draft or has no tables yet. A refusal costs no model
call.

Applying is unchanged and still human-only. An agent may ask what a change would
be; only a person may make it.

### Clients

Upgrade to at least:

| Client | Version |
|---|---|
| Python SDK (`deepsieve`) | 0.3.0 |
| MCP server — the stdio package and browser login alike | 0.3.0 |

The MCP tool `propose_blueprint_change` takes an optional `blueprint_id`; omit
it and it resolves the active Blueprint as before. The CLI does not call this
endpoint and is unaffected.

## 2026-09-07 — breaking, before GA

**A dataset read now names the Blueprint it reads.** Two endpoints are removed
and six are moved. This is a **breaking change before GA**, announced here as
[Versioning & stability](/developers/versioning) requires: `/v1` becomes
additive-only at GA, and until then a break is allowed only when it is
deliberate and appears in this changelog.

**If you change nothing, the calls below return `404`.** That is an ordinary
not-found, so a client that only checks for an error will look like it is
reading an empty dataset rather than calling a route that no longer exists.
Check the path before you check the data.

### Removed

| Was | Now |
|---|---|
| `GET /v1/data` | `GET /v1/blueprints/{blueprint_id}` |
| `GET /v1/data/{entity_key}` | `GET /v1/blueprints/{blueprint_id}/entities/{entity_key}` |

Same response shape, same query parameters (`limit`, `cursor`, `updated_since`,
`fields`, `receipts`), same free-preview cap. One field differs: the catalog's
discriminator is now `"object": "blueprint"` rather than `"object":
"data_catalog"`.

Get a `blueprint_id` from **`GET /v1/blueprints`**, which lists the Blueprints
you can see and marks the active one. That listing is the reason for the change.
`GET /v1/data` answered for whichever Blueprint happened to be *active* — a
choice the caller never made and the response never named — so an account with
two Blueprints could receive a completely coherent answer to a question it had
not asked, with nothing in the payload to reveal which one it described. No
alias is kept: a shim with no consumer reads as a supported path.

`GET /v1/export` is unchanged, and still takes `?workspace_id=`.

### Moved

`/v1/blueprints/onboarding/…` → `/v1/blueprints/drafts/…` — all six: the
collection itself, `{session_id}`, and its `/answers`, `/adjust`, `/chat` and
`/approve`. Path only. Same handlers, same request and response bodies, and the
same `operationId`s, so **generated SDK method names do not change**.

### Clients

Upgrade to at least:

| Client | Version |
|---|---|
| Python SDK (`deepsieve`) | 0.2.0 |
| CLI (`deepsieve`) | 0.2.0 |
| MCP server — the stdio package and browser login alike | 0.2.0 |

`deepsieve data catalog` and `deepsieve data get` resolve the active Blueprint
for you, and take `--blueprint <id>` to name one instead. The MCP tools
`get_blueprint` and `query_entities` take an optional `blueprint_id`, and a new
`list_blueprints` tool supplies the ids — on both transports.

## 2026-08 (later)

- **`ApiKey` gains `usage` on `GET /v1/keys`** — `{runs, est_spent_cents}`, or
  `null` if the aggregation could not be completed. The API has returned it
  since the per-key usage work; the contract never declared it, so generated
  clients could not read it. Additive — existing fields unchanged. **Scope
  worth knowing: it counts runs in your ACTIVE WORKSPACE.** Keys are
  workspace-bound at mint time and the list is org-wide, so a key pinned to a
  different workspace reports zero rather than its real totals.
- **`CreateRunRequest.reuse_existing` was declared `string`; it is a `boolean`.**
  A pasted property body had silently overwritten it in the contract, so SDK
  0.1.1 types it `Optional[str]` and `reuse_existing=True` does not type-check.
  Corrected — the next SDK release carries the right type. Two other properties
  had the same defect without reaching the SDK: `MonitoringView.freshness`
  (object, was resolving to `number`) and `StartRunResponse.reused_existing`
  (carried the wrong description).
- **Freshness overage wording**: the per-day rate and `auto_topup_suggested`
  described a *report*; both are metered per **monitored run** — one per Deep
  Research dossier, so a report assembled from several dossiers accrues
  several. Wording only; no behaviour or price changed.

- **CLI sign-in** — `POST /v1/auth/device/code` and `/v1/auth/device/token`:
  the OAuth device flow behind `deepsieve login`. Unauthenticated by design;
  uses RFC 8628's own vocabulary (`authorization_pending`, `slow_down`,
  `expired_token`, `access_denied`), and the two retriable codes are the normal
  path, not failures.
- **New error codes**: the four above. `invalid_api_key` is now returned for a
  credential that was presented but not accepted — previously indistinguishable
  from `authentication_required`, which means no credential was sent at all.
- **`Citation` gains `verdict`, `verdict_note` and `cited`.** The API already
  returned them; the contract did not declare them, so generated clients could
  not read a verdict. Additive — existing fields unchanged. SDK 0.1.1 carries
  them.
- **MCP over browser login reaches tool parity with the stdio package** (19
  tools). No tool was removed.

## 2026-08

- **Initial `/v1` release**: typed error envelope + request ids; scoped
  revocable API keys (`/v1/keys`); async research runs with `Idempotency-Key`
  and query-first creation; outbound signed webhooks (`run.*`,
  `dataset.updated`); cursor-paginated dataset read (`/v1/data`) with
  `updated_since` sync and per-cell citations; NDJSON/JSON/CSV export;
  RateLimit headers + per-key budgets; MCP server; `/llms.txt`.
