Skip to content

API reference

  • Auth: Authorization: Bearer <PAT> (/api/v1), Account API key (/api/control)
  • Errors: RFC7807-style problem+json (type / title / detail / suggested_fix)
  • Idempotency: mutations accept an Idempotency-Key header (same key + same body replays the first response)
  • Webhook signatures: X-D2B-Signature: sha256=<hex> = HMAC-SHA256(secret, raw_body)

Get Account

One Account (account_id picks among the caller’s; default = the oldest, lazily provisioned on first touch).

querytypedefaultdescription
account_id--

Account Billing

querytypedefaultdescription
account_id--

PATCH /api/control/account/billing/auto-topup

Section titled “PATCH /api/control/account/billing/auto-topup”

Account Billing Auto Topup

Adjust (or switch off) the Account’s low-balance auto-recharge.

Request body: AccountAutoTopupRequest (see openapi.json for the schema)

POST /api/control/account/billing/checkout

Section titled “POST /api/control/account/billing/checkout”

Account Billing Checkout

Start the Account’s subscription (Solo / Pro, monthly or annual) — the same Stripe prices the personal plan page sells.

Request body: DevCheckoutRequest (see openapi.json for the schema)

Account Billing Payg

Enable pay-as-you-go: a setup Checkout saves a card on the Account’s customer (no charge); the webhook then flips the free tier to “payg” with auto-recharge on. $0 monthly — you pay the pack rate for what you use.

Request body: PaygEnableRequest (see openapi.json for the schema)

Account Billing Portal

Request body: DevPortalRequest (see openapi.json for the schema)

POST /api/control/account/billing/reuse-card

Section titled “POST /api/control/account/billing/reuse-card”

Account Billing Reuse Card

Reuse the card already registered on the owner’s DEFAULT wallet for a developer Account (2026-09-01 onboarding: developer accounts skip the Free presentation and register a card up front).

Stripe PaymentMethods can’t hop between customers, so “reuse” means the developer Account SHARES the default wallet’s customer. Charges/ subscriptions stay attributable: everything we create carries metadata.account_id, and lifecycle events resolve by subscription id before any customer lookup. Card on file → the account flips straight to usage billing (payg + auto-recharge), same end-state as the setup Checkout.

Request body: ReuseCardRequest (see openapi.json for the schema)

Account Billing Topup

Buy a prepaid credit pack — raises the Account’s metering ceiling.

Request body: DevTopupRequest (see openapi.json for the schema)

Delete Account

Delete a developer Account and everything that dies with it.

Allowed only when ALL of these hold (each violation is its own error):

  • the caller is the owner’s session (JWT) — a key must not be able to erase its own principal;
  • kind == "developer" — the default account IS the user’s wallet;
  • no active/trialing subscription — cancel via the Stripe portal first (no silent cancellation side effects);
  • no settleable overage — postpaid liability must be billed before the debtor disappears. This counts the stored arrears bucket AND the current period’s derived ops overage (which the monthly settlement would snapshot and charge), measured against the same floor settlement uses: a balance too small for settlement to ever charge is written off rather than trapping the account forever;
  • confirm matches the account name exactly.

The cascade, in order: revoke every API key pinned to the account (cutting off concurrent access first), delete each funded workspace and its workbook rows (same metadata-delete semantics as the app’s workbook deletion), then the account row. Remaining credits — period and pack — are forfeited: prepaid balances don’t transfer between wallets (pack rates differ per tier; a transfer would be an arbitrage loop). The Stripe customer is never touched: it may be shared with the default wallet (reuse-card), and its invoice history stays valuable either way.

Request body: DeleteAccountRequest (see openapi.json for the schema)

List Account Keys

querytypedefaultdescription
account_id--

Create Account Key

Mint an account API key. The plaintext is returned exactly once.

A key minted WITH a key cannot exceed the issuer’s scopes, nor reach outside the issuer’s own resource (no privilege escalation through re-issuance, in either dimension); the owner’s JWT mints anything. workspace_id pins the key to that workspace (resource= "workspace:<id>"): its workbooks on the data plane, and that workspace alone on the control plane.

Request body: CreateKeyRequest (see openapi.json for the schema)

Revoke Account Key

Revoke one of the account’s API keys.

Reach follows the credential: the owner’s session and account-scoped keys manage every key on the account, a pinned key only keys pinned the same way — plus itself, always, so a suspect credential can burn itself without holding authority over any other. The key listing shows exactly what this reaches.

querytypedefaultdescription
account_id--

Account Usage Series

Daily usage buckets for the console’s trend charts.

Flows (requests / ops / credits) are that day’s totals; stocks (storage_bytes / tables) are the day’s last sample and may be null on days no sweep landed. Days with no activity are omitted — the client fills gaps (zeros for flows, carry-forward for stocks).

querytypedefaultdescription
daysinteger30
account_id--

List Accounts

Every Account the caller owns (User:Account = 1:N — the paved road is one Account per service). Account keys see only their own account.

2026-09-01 unification: the DEFAULT account (the wallet the app bills) is the console’s first-class account too — no lazy developer twin is minted any more. The listing is [default, …explicit developer accounts]; provision is accepted for compatibility but inert (the default account is app-plane and materializes on first billing touch regardless of who asks).

querytypedefaultdescription
provisionbooleanTrue

Create Account

Create another Account — “a new service = a new payment/contract subject”. Owner JWT only: an account KEY must not mint sibling payment subjects (its blast radius is its own account).

Request body: CreateAccountRequest (see openapi.json for the schema)

List Workspaces

querytypedefaultdescription
account_id--

Provision Workspace

workspace.provision — create an API-managed workspace under the account.

Unlike the SPA’s POST /api/workspaces this does NOT move the owner’s active SPA workspace (user.workspace_id stays untouched): a provisioned workspace is a workspace the developer backend manages, not a switch of the human owner’s working context. residency is recorded as a declaration; physical region enforcement is deferred (design doc §6).

Needs an account-scoped credential (or the owner’s session): a key pinned to one workspace does not create siblings for the account, the same way an account key cannot create sibling accounts.

Request body: ProvisionWorkspaceRequest (see openapi.json for the schema)

GET /api/control/workspaces/{workspace_id}

Section titled “GET /api/control/workspaces/{workspace_id}”

Get Workspace

PATCH /api/control/workspaces/{workspace_id}

Section titled “PATCH /api/control/workspaces/{workspace_id}”

Configure Workspace

workspace.configure — apply the provided fields, leave the rest.

idp_config is stored as declared linkage only; user/role sync from the IdP is deferred until a customer IdP exists (design doc §6).

Request body: ConfigureWorkspaceRequest (see openapi.json for the schema)

DELETE /api/control/workspaces/{workspace_id}

Section titled “DELETE /api/control/workspaces/{workspace_id}”

Delete Workspace

workspace.delete — remove an EMPTY managed workspace from the account.

Refused while workbooks remain (delete those first with a workbooks:delete credential) and for the built-in personal workspace. Keys pinned to the workspace are revoked as part of the deletion.

GET /api/control/workspaces/{workspace_id}/users

Section titled “GET /api/control/workspaces/{workspace_id}/users”

List Workspace Users

users.list — the workspace’s members with their EFFECTIVE roles (base role ∪ custom ABAC roles; the policy engine’s subject).

PUT /api/control/workspaces/{workspace_id}/users/{user_id}/roles

Section titled “PUT /api/control/workspaces/{workspace_id}/users/{user_id}/roles”

Set User Roles

Assign custom ABAC roles to a member (replaces the custom set; the base admin / editor / viewer role is membership state and stays). Built-in role names are reserved.

Request body: SetRolesRequest (see openapi.json for the schema)

GET /api/control/workspaces/{workspace_id}/workbooks

Section titled “GET /api/control/workspaces/{workspace_id}/workbooks”

List Workspace Workbooks

Read-only observability (2026-09-01): the workbooks living in one funded workspace, for the console’s data view. Visibility-filtered — the caller’s own workbooks plus the ones shared with them, the same rule the app applies, so another member’s private workbook is never listed. Metadata only — table listings and row previews go through the v1 data endpoints, which the owner’s session already passes.

List Cloud Files

Importable files on the caller’s linked drive. Folders are listed first for navigation (isFolder); pass their id as folder_id. SharePoint also searches by name with q.

querytypedefaultdescription
folder_idstringroot
q--Name search (SharePoint only)

List Jobs

Recent async jobs for the calling user, newest first.

Same registry as GET /jobs/{id}: process-local, ~1h retention after completion — a recent-activity view, not durable history. A workbook-scoped credential sees only its workbook’s jobs.

Get Job

Status + terminal result of an async operation.

status is one of pending | running | succeeded | failed. result (on success) carries the same body the synchronous form would have returned; error (on failure) a short message.

Get Me

Identity + plan summary. Mirrors the SPA’s /api/auth/me but adds the credential context — an agent inspecting principal learns which PAT it is and what scopes it carries.

Principal-aware (DX report 4 §5-1/§5-3): an account-pinned key sees ITS account’s wallet and reach — plan/credits_* are the pinned account’s buckets and workspace_id is the oldest workspace that account funds (null when it has none). The old behavior showed the owner’s app-side wallet and personal-workspace pointer, which the credential could not necessarily spend from or reach.

Cloud Authorization

Whether D2B may read the caller’s drive, and the one-page URL to authorize it when not: the user opens it in a browser once (Microsoft: connect the account; Google: connect and pick the files), then the agent retries its call.

querytypedefaultdescription
workbook_id--

Get My Data

List every artifact this credential can see, across every workbook. Refreshes the catalog from DuckDB before serving so the result reflects the current state of each workbook’s session DB.

The response shape matches the proposal’s §5 example: each row carries enough metadata (schema, row_count, source files, updated_at) for an agent to decide whether to drill down via GET /workbooks/{cid}/artifacts/{name}/....

querytypedefaultdescription
cursor--Opaque cursor from a prior call.
limitinteger50

List Access Tokens Endpoint

List the calling user’s PATs. Plaintext is never present — the response is the redacted to_api form.

Create Access Token Endpoint

Mint a new PAT. The plaintext is returned exactly once.

All scopes default to workbooks:read so an accidental click in the UI can’t issue a write-capable token without explicit intent.

Request body: CreateTokenRequest (see openapi.json for the schema)

Revoke Access Token Endpoint

Revoke a PAT. Idempotent — already-revoked tokens still return 204.

Reach follows the credential: the owner’s session and account-scoped keys manage every key on the account, a pinned key only keys pinned the same way — plus itself, always, so a suspect credential can burn itself without holding authority over any other.

Token Usage

Per-token daily request counts (trailing days, UTC days), aggregated from the token audit log. Same management gate as the token list — usage reveals which credentials exist and how hot they run.

querytypedefaultdescription
daysinteger30

List Webhooks

Create Webhook

Create a webhook subscription. Returns the signing secret once.

The owner is the calling user. Subscriptions are personal — there is no workspace-wide webhook in Phase 3; that lands with the wider workspace rollout in Phase 4.

Request body: CreateWebhookRequest (see openapi.json for the schema)

Delete Webhook Endpoint

GET /api/v1/me/webhooks/{webhook_id}/deliveries

Section titled “GET /api/v1/me/webhooks/{webhook_id}/deliveries”

List Deliveries

Recent deliveries for one webhook. Useful for debugging “why isn’t my endpoint receiving anything” — surfaces attempt count, last status, last error.

List My Workbooks

Paginated list of workbooks owned by the calling user.

For now this is straight enumeration from the store — no workspace-shared rows, no shared-with-me rows. Those become relevant in Phase 3 when the catalog grows; today the agent’s mental model is “I see the data I personally own”.

A workspace_id the credential cannot address is a 403, not an empty page: an empty page means “you own nothing there”, and a workspace outside the credential’s reach must not read the same way.

querytypedefaultdescription
cursor--Opaque pagination cursor returned by a prior call.
limitinteger50
workspace_id--Scope the listing to one workspace (a workspace id, or the self-relative alias ‘personal’ for the caller’s personal workspace) — the collection rule: collections take an explicit scope, resource-addressed calls scope themselves.

List My Workspaces

The workspaces this credential can create in or read from, by name.

is_default marks where POST /workbooks lands when workspace_id is omitted. Scoped to the credential’s reach: an account key lists its account’s workspaces the owner belongs to, a workspace:<id> key lists that one, a workbook:<id> key the workbook’s home.

Get Result

Read a materialised result by id. Supports JSON, NDJSON, Arrow.

querytypedefaultdescription
limitinteger1000
offsetinteger0

Search

Search across the user’s artifacts by name, description, column names, and source filenames.

Empty / whitespace-only queries return an empty result list instead of 400 — agents driving a partially-typed search box should not see error noise on every keystroke. Real failures (FTS5 syntax issues) are filtered at the store layer.

querytypedefaultdescription
qstring-Free-form keyword query.
limitinteger20

Get Upload Slot

Slot status: pending (waiting for bytes; upload_url is included), uploaded (ready to ingest) or ingested.

Put Upload Bytes

Receive the file for a slot. Authenticated by the signature in the URL alone (so a browser or a plain curl -T can send it); the body is the raw bytes.

querytypedefaultdescription
expiresinteger-
sigstring-

Create Workbook

Create a fresh workbook owned by the calling user (see :func:create_workbook_for_principal for the scope contract).

Get Workbook

Workbook metadata. Drops the full message history — agents that need it should pull from /api/workbooks/{id} for now (Phase 3 adds a dedicated /messages endpoint with windowing).

Delete Workbook

workbook.delete — owner-only, same semantics as the SPA’s delete. Without it the CLI could create workbooks but never clean them up (DX report §5).

POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns

Section titled “POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns”

Add Column

Add a column to an editable table (all NULL, or default everywhere). The column lands before __d2b_row_id so the row-id keeps riding last. Type from the create-table allow-list.

Request body: AddColumnRequest (see openapi.json for the schema)

PATCH /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}

Section titled “PATCH /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}”

Alter Column

Rename (new_name) OR retype (type) one column — exactly one per call. Retype casts existing values; a value that can’t cast cleanly is a 400 (clean the data or use a SQL transform).

Request body: AlterColumnRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}”

Drop Column

Drop a column from an editable table. The last data column can’t be dropped (delete the table instead). Any classification on the column is removed.

querytypedefaultdescription
actor--
expected_version--

POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}/tags

Section titled “POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}/tags”

Tag Column

Attach a sensitivity tag (pii / hr / …) to a column.

Request body: TagColumnRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}/tags/{tag}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}/tags/{tag}”

Untag Column

Remove a column’s tag (its policies stop applying to this column).

querytypedefaultdescription
actorstring-

GET /api/v1/workbooks/{workbook_id}/artifacts/{name}/schema

Section titled “GET /api/v1/workbooks/{workbook_id}/artifacts/{name}/schema”

Get Artifact Schema

Schema-only response: columns + types + row count.

Carries a 5-row sample so an agent can take a quick look without triggering a paginated rows call.

querytypedefaultdescription
formatstringcolumns

POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/sync-bindings

Section titled “POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/sync-bindings”

Bind Sync

Store the table ↔ external-sheet mapping (inert — D2B never executes the sync). One sheet maps to one table; binding a governed table returns explicit warnings instead of silently leaking.

Request body: BindRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/audit-log

Section titled “GET /api/v1/workbooks/{workbook_id}/audit-log”

Get Audit Log

The access-audit chain (newest first) + a fresh integrity check. chain_valid=false means an entry was altered, removed, or reordered after the fact.

querytypedefaultdescription
limitinteger200

GET /api/v1/workbooks/{workbook_id}/branches

Section titled “GET /api/v1/workbooks/{workbook_id}/branches”

List Branches

Open branch points (one row per branched table): what an edited export can still merge back against.

querytypedefaultdescription
limitinteger200

GET /api/v1/workbooks/{workbook_id}/charts

Section titled “GET /api/v1/workbooks/{workbook_id}/charts”

List Charts

Every chart in the workbook with its rendered config and the recipe (tool + params) it was generated from.

Read-only: charts are re-generated from their recipe, never edited as raw config, so this is history/inspection — what d2b pull writes to charts/*.json. On a governed workbook (any column policy) the config is omitted (config_omitted="governed"): a chart embeds the values it plots, which would bypass masking.

GET /api/v1/workbooks/{workbook_id}/classifications

Section titled “GET /api/v1/workbooks/{workbook_id}/classifications”

List Classifications

Every column tag in the workbook (the targets policies bind to).

querytypedefaultdescription
limitinteger500

GET /api/v1/workbooks/{workbook_id}/conflicts

Section titled “GET /api/v1/workbooks/{workbook_id}/conflicts”

List Conflicts

The workbook’s review queue. ?status=open filters to the items still awaiting a decision.

querytypedefaultdescription
status--
limitinteger200

POST /api/v1/workbooks/{workbook_id}/conflicts/{conflict_id}/resolve

Section titled “POST /api/v1/workbooks/{workbook_id}/conflicts/{conflict_id}/resolve”

Resolve Conflict

Close a review-queue item.

acknowledge records the decision; revert (overwrite conflicts) writes the prior value back as a fresh attributed edit; drop_override (stale_override conflicts) removes the override so the recomputed value shows.

Request body: ResolveConflictRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/export

Section titled “POST /api/v1/workbooks/{workbook_id}/export”

Export Workbook

Deliver tables as a file (xlsx: one sheet per table; csv: one file, or a zip for several tables).

formula_mode="preserve" (xlsx only) restores formula-provenance columns as live formulas translated to the file’s coordinates — the X-D2B-Preserved-Formulas header says which columns recalculate. generate needs the reference-map layer and is still refused explicitly. include_row_ids=True carries __d2b_row_id along (hidden column in xlsx) so an edited file can be merged back by row identity later; record_branch=True additionally freezes the exported rows as the merge base (export = branch). Masking/deny policies apply to the produced bytes; the X-D2B-Masked-Columns / X-D2B-Denied-Columns headers say what was enforced.

Request body: ExportRequest (see openapi.json for the schema)

Section titled “GET /api/v1/workbooks/{workbook_id}/file-links”

List File Links

The workbook’s tracked OneDrive / SharePoint files with their versions (newest last): n, kind (link / sync / revert), modified_by, modified, synced_at, snapshot_id.

POST /api/v1/workbooks/{workbook_id}/file-links

Section titled “POST /api/v1/workbooks/{workbook_id}/file-links”

Track Cloud File

Put an xlsx on OneDrive / SharePoint under D2B version control: its current bytes become version 1 and feed a workbook source of the same name; every later save becomes a version (see GET .../file-links).

Request body: TrackFileRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/file-links/local

Section titled “POST /api/v1/workbooks/{workbook_id}/file-links/local”

Track Local File

Track a file that lives on the user’s machine (d2b watch): the uploaded bytes become version 1 and feed a source of the same name; the client pushes later changes to …/file-links/{id}/versions. No drive API, no polling — the machine is the detector.

If the workbook already holds a source of that name: on_existing=auto keeps D2B’s copy as v1 when it looks like the same file (a sheet in common) and refuses otherwise; as_name tracks under another name; replace overwrites D2B’s copy; seed forces the v1 seed.

POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/sync

Section titled “POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/sync”

Sync File Link

Pull the drive’s copy now (even when its eTag did not move). A changed file becomes a new version and re-ingests the source.

Requires the workbooks:write and cloud-files:read scopes and edit rights on the workbook.

POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions

Section titled “POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions”

Push File Link Version

Push the file’s current bytes as the next version (d2b watch does this on every change). Identical bytes cut no version (changed: false).

Section titled “GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/cells”

File Link Version Cells

A version’s sheet as {cell, value} pairs (values only) — what an agent needs to write that version back into the spreadsheet itself.

Refused on governed workbooks because raw cell values cannot enforce column policies.

querytypedefaultdescription
sheet--
Section titled “GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/diff”

Diff File Link Version

Cell-by-cell diff from against (default: the previous version) to n: per sheet, {cell, old, new} (values only, capped).

Refused on governed workbooks because raw values cannot enforce column policies.

querytypedefaultdescription
against--
Section titled “GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/download”

Download File Link Version

The version’s original xlsx bytes.

Refused on governed workbooks because raw files cannot enforce column policies.

POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/revert

Section titled “POST /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/revert”

Revert File Link Version

Re-ingest version n on D2B’s side as a new version. The file on the drive is untouched — use cells (or download) to put the values back into the spreadsheet yourself.

List Ops

The workbook’s ordered mutation history, newest first.

Each op carries kind (rows.upsert / transform.run / formula.set / version.commit / rows.undo …), actor attribution, a replayable code reference, and undoable (true when this surface can apply its inverse via the undo endpoint).

querytypedefaultdescription
limitinteger50
before--Page: only ops with op_id < before.

POST /api/v1/workbooks/{workbook_id}/ops/{op_id}/undo

Section titled “POST /api/v1/workbooks/{workbook_id}/ops/{op_id}/undo”

Undo Op

Undo one row op (rows.upsert / rows.delete) by applying its inverse.

The undo is itself a normal write: it bumps the table version, lands in the edit log with attribution, marks downstream artifacts stale (auto-recompute settles them) and records a rows.undo op. An op can be undone once; undoing the undo is just another undo, targeting the rows.undo op’s own recorded edits.

GET /api/v1/workbooks/{workbook_id}/policies

Section titled “GET /api/v1/workbooks/{workbook_id}/policies”

List Policies

The workbook’s tag→effect rules.

querytypedefaultdescription
limitinteger500

PUT /api/v1/workbooks/{workbook_id}/policies/{tag}

Section titled “PUT /api/v1/workbooks/{workbook_id}/policies/{tag}”

Set Policy

Upsert the rule for (tag, role): mask (typed NULL + metadata), deny (column disappears), or allow (a role’s explicit exemption over the role="*" default).

Request body: SetPolicyRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/policies/{tag}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/policies/{tag}”

Delete Policy

Drop a tag’s rules (the tag itself stays on the columns). ?role= removes just that role’s row; without it the tag stops being governed entirely.

querytypedefaultdescription
actorstring-
role--

POST /api/v1/workbooks/{workbook_id}/query

Section titled “POST /api/v1/workbooks/{workbook_id}/query”

Post Query

Run a read-only SELECT and materialise the result.

Returns a result-set manifest plus the first page in the requested format. The manifest’s id is the durable handle; subsequent GET /api/v1/results/{id} calls read pages off the materialised Parquet snapshot without re-running the query.

Request body: QueryRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/query/validate

Section titled “POST /api/v1/workbooks/{workbook_id}/query/validate”

Validate Query

Static check + schema dry-run for a SELECT, no execution.

Returns { valid, errors, columns }. Use this when an agent wants to surface “this query won’t run” before committing to /query — the feedback loop is fast (no Parquet write, no result_id) and the column list it returns is exact (it goes through DuckDB’s planner, so a typo’d column name is caught here).

Request body: ValidateRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/recompute

Section titled “POST /api/v1/workbooks/{workbook_id}/recompute”

Recompute Stale

Re-run the transforms behind every stale table/view, dependency order, and clear their staleness.

Interactive writes (row edits, override drops, merges) mark their downstream artifacts stale instead of eagerly re-running them; this endpoint settles the whole backlog in one batch. Stale REPORTS are returned in skipped_reports — their regeneration is an LLM call the app’s regenerate flow owns. Artifacts whose transform fails stay stale and are listed in failed.

POST /api/v1/workbooks/{workbook_id}/review

Section titled “POST /api/v1/workbooks/{workbook_id}/review”

Review

Review a table (target) or the whole workbook and return evidence-backed findings. Read-only.

Checks: the source file’s formulas recomputed from its cells (overwritten values, broken fill-downs, ranges that stop short, subtotals summed twice); invariants of each table (mixed types or spellings in a column, gaps and repeats in an ordered key, values the input never had, totals a plain projection changed, rows that equal the sum of the rows above them); and, with judge, whether each computed column’s label matches its computation. Every finding carries the SQL or the cell that found it. coverage says what was and was not examined.

mode=lint answers 200 with the review when it finishes within about 45 seconds, otherwise 202 with a job. mode=agent (or async=true) answers 202 at once. The job’s result is the same body. The judgement model and the agent spend the workbook’s credits (credits); out of credits, lint runs without the judgement model and the agent is refused (402). Refused on governed workbooks (403).

Request body: ReviewRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/sheets

Section titled “GET /api/v1/workbooks/{workbook_id}/sheets”

List Sheets

GET /api/v1/workbooks/{workbook_id}/sheets/{name}

Section titled “GET /api/v1/workbooks/{workbook_id}/sheets/{name}”

Get Sheet

PUT /api/v1/workbooks/{workbook_id}/sheets/{name}

Section titled “PUT /api/v1/workbooks/{workbook_id}/sheets/{name}”

Put Sheet

Create or replace a sheet composition.

spec.blocks is an ordered vertical flow of {kind: heading|text|table_view|spacer, ...}; table_view blocks REFERENCE tables by name (n:m — the same table can sit on many sheets, and deleting a sheet deletes no data).

Request body: PutSheetRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/sheets/{name}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/sheets/{name}”

Delete Sheet

GET /api/v1/workbooks/{workbook_id}/sheets/{name}/render

Section titled “GET /api/v1/workbooks/{workbook_id}/sheets/{name}/render”

Render Sheet

Compose the sheet into an xlsx. Table blocks read through the governance projection — masked columns deliver as blanks, denied columns are absent — exactly like the rows surface.

GET /api/v1/workbooks/{workbook_id}/snapshots

Section titled “GET /api/v1/workbooks/{workbook_id}/snapshots”

List Snapshots

List committed snapshots for the workbook.

POST /api/v1/workbooks/{workbook_id}/snapshots

Section titled “POST /api/v1/workbooks/{workbook_id}/snapshots”

Commit Snapshot

Commit the current state as an immutable snapshot.

Subsequent edits land on a fresh editing snapshot; the snapshot just committed becomes the rollback target for POST .../snapshots/{id}/restore.

Request body: CommitSnapshotRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/snapshots/{snapshot_id}/restore

Section titled “POST /api/v1/workbooks/{workbook_id}/snapshots/{snapshot_id}/restore”

Restore Snapshot

Roll back to a prior snapshot. Destructive — requires workbooks:delete.

GET /api/v1/workbooks/{workbook_id}/sources

Section titled “GET /api/v1/workbooks/{workbook_id}/sources”

List Sources

The workbook’s sources with their lifecycle status.

POST /api/v1/workbooks/{workbook_id}/sources

Section titled “POST /api/v1/workbooks/{workbook_id}/sources”

Post Source

Upload a file into the workbook.

mode=auto (default) runs the one-shot extraction pipeline and the response carries the created source + every artifact it produced. mode=staged only lands the bytes (zero interpretation, status registered) — follow with POST .../sources/{name}/analyze, review/correct the parse spec, then POST .../sources/{name}/materialize.

async=true (auto mode only): the pipeline includes the structuring pass, so for big files the synchronous form can hold the socket for minutes. The async form returns 202 with a job_id immediately; poll GET /api/v1/jobs/{job_id} for the terminal result (the data events — source.materialized, artifact.updated — still arrive on webhooks as usual). Job state is process-local with a 1h retention; a deploy mid-job loses the handle but never the workbook’s durable state.

Idempotency: pass Idempotency-Key: <opaque> to make a retry safe — the cached response is returned for 24 h (for async calls the same job_id is replayed). Even without the header, identical bytes in the same workbook are de-duplicated: we return the existing source rather than re-ingesting.

POST /api/v1/workbooks/{workbook_id}/sources/cloud

Section titled “POST /api/v1/workbooks/{workbook_id}/sources/cloud”

Import Cloud File

Download a file from the caller’s Google Drive or OneDrive / SharePoint into the workbook and run extraction — the same ingest as a direct upload, with the file’s provenance stamped on the source so it can be refreshed later.

Request body: CloudImportRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/sources/from-url

Section titled “POST /api/v1/workbooks/{workbook_id}/sources/from-url”

Ingest From Url

Fetch a public URL server-side and ingest it (same response as POST /workbooks/{id}/sources). Private networks, localhost and redirects are refused; the size cap applies while streaming.

Request body: UrlIngestRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/sources/{source_name}

Section titled “GET /api/v1/workbooks/{workbook_id}/sources/{source_name}”

Get Source

One source + a summary of its parse spec (when analysed).

POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/analyze

Section titled “POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/analyze”

Analyze Source

Run structure detection and persist the ParseSpec proposal.

Pure inference — nothing is materialised. Re-running bumps the spec version and discards prior edits. Fires source.analyzed.

GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/download

Section titled “GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/download”

Download Source

The source’s ORIGINAL uploaded bytes, byte-identical (v0.5 §5.2 L0).

This is the honest answer to “give me the original file back”: no reconstruction, the file itself. Refused on governed workbooks for the same reason as preview — the original bytes bypass column policies.

POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/materialize

Section titled “POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/materialize”

Materialize Source

Faithfully load the selected regions into tables.

Default selection is every kind="data" region. target.mode="new" creates one table per region; "append" accumulates rows into the existing target.table (column-matched by name via target.column_mapping). Fires source.materialized + artifact.updated per produced artifact.

GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/parse-spec

Section titled “GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/parse-spec”

Get Parse Spec

The current ParseSpec document (regions + reader parameters).

PUT /api/v1/workbooks/{workbook_id}/sources/{source_name}/parse-spec

Section titled “PUT /api/v1/workbooks/{workbook_id}/sources/{source_name}/parse-spec”

Put Parse Spec

Correct the proposal: split/merge/exclude regions, fix ranges, kinds, or header flags. Bumps the spec version (status edited).

Request body: UpdateParseSpecRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/preview

Section titled “GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/preview”

Preview Source

A bounded look at the source’s RAW BYTES (spec §5 files.preview): the first rows of each sheet, before any analysis or materialisation — so an agent can decide what to do with a file it just landed (staged sources preview straight from registered).

Refused on governed workbooks: the original bytes would bypass column policies, so once any policy exists, reads go through the policy-applied rows/schema/profile surfaces instead.

querytypedefaultdescription
rowsinteger20

GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/render

Section titled “GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/render”

Render Source Template

Template write-back (v0.5 §5.2 L1): the source’s ORIGINAL xlsx with its data-region cell values replaced by the current tables.

Styles, merges, column widths, charts, images, macros and every formula OUTSIDE the data regions survive byte-identical — the original file is the style store; only the linked regions’ values change. xlsx + staged-materialised (region-linked) sources only; a table that grew past its template region is refused (409).

overrides (JSON {"<region table>": "<replacement table>"}) writes a derived/transform table into a region in place of its linked source table — faithful output with the computation kept server-side and lineage-traceable. The replacement must carry the region’s columns by name.

Refused on governed workbooks: the template’s non-data cells are original bytes, which bypass column policies.

querytypedefaultdescription
overrides--Optional JSON object mapping a region-linked table name → a replacement (e.g. transform) table name. That region is written from the replacement instead of the linked source table, so a SQL transform’s result renders into the template.

POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/revise

Section titled “POST /api/v1/workbooks/{workbook_id}/sources/{source_name}/revise”

Revise Source Endpoint

Faithful edit in one call (v0.5 §5.2 L1): analyze + materialize + transform + template write-back, folded.

Applies transform (a {{ artifact_name }} SQL template with {{ src }} bound to the target region’s table) to a region of the source, then returns the ORIGINAL xlsx with only that region’s values replaced — styles/charts/other sheets byte-identical. The transform persists (its DAG is queryable); positional write-back and in-region formula protection apply. Carry MIN("__d2b_row_id") in the SELECT to keep row positions on a folding merge.

Region: region.region_id (existing), region.sheet+range (reuse-or-add the rectangle — no separate update-parse-spec call), or omit for the source’s single data region. Refused on governed workbooks (the template’s non-data cells bypass column policies).

Request body: ReviseRequest (see openapi.json for the schema)

Sync Returned File

One-call sync of a returned export (design pillar 3, Tier 0 — the “agent mails a workbook, the end user edits it in Excel, the file comes back” loop).

Merges EVERY branched table whose sheet the file still carries, via the same per-table 3-way arbitration as .../merge (clean changes apply as attributed edits, both-sides changes queue kind="merge" conflicts). branch_id defaults to the newest recorded branch. Tables whose sheet is missing are reported in skipped; a sheet that fails to parse lands in errors without aborting the rest. Records one source.sync op summarizing the whole exchange.

GET /api/v1/workbooks/{workbook_id}/sync-bindings

Section titled “GET /api/v1/workbooks/{workbook_id}/sync-bindings”

List Sync Bindings

The workbook’s stored bindings (optionally one artifact’s).

querytypedefaultdescription
artifact--
limitinteger200

DELETE /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}”

Unbind Sync

Remove a stored binding (the external sheet itself is untouched).

querytypedefaultdescription
actorstring-

GET /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}/snapshot

Section titled “GET /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}/snapshot”

Binding Snapshot

The diff baseline for the agent’s sync cycle: governance-applied rows (row-id keyed when present) captured together with the table’s edit version.

querytypedefaultdescription
limitinteger10000

GET /api/v1/workbooks/{workbook_id}/tables

Section titled “GET /api/v1/workbooks/{workbook_id}/tables”

List Artifacts

List artifacts (tables + views) for one workbook, refreshing the catalog first so the response reflects DuckDB state.

Archived artifacts — raw shapes the structured tier superseded at ingest (promotion model) — are hidden by default; every row carries state. include_archived=true appends them (state: "archived", no schema/row_count — their table is not materialised) so an agent can see what unarchive would bring back.

querytypedefaultdescription
include_archivedbooleanFalse

POST /api/v1/workbooks/{workbook_id}/tables

Section titled “POST /api/v1/workbooks/{workbook_id}/tables”

Create Table

Create an empty table from a schema — no source file required.

The table registers as an editable raw artifact with row-ids assigned and edit_version seeded at 1, so rows can be upserted immediately (expected_version: 1). Emits artifact.updated.

Request body: CreateTableRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/tables/{name}

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}”

Get Artifact

Single-artifact metadata. Returns schema, row_count, source files, and freshness — whether the artifact still reflects its inputs (design §9: staleness is a first-class, introspectable state).

PATCH /api/v1/workbooks/{workbook_id}/tables/{name}

Section titled “PATCH /api/v1/workbooks/{workbook_id}/tables/{name}”

Rename Table

Rename an artifact’s display name (id-identity refactor P4): the immutable id and the physical table are untouched, so downstream transforms follow via the binding ids and reads keep working under the new name. Refuses a taken name (409) and, for now, a python-transform output.

Request body: RenameTableRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/tables/{name}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/tables/{name}”

Delete Artifact

Drop an artifact (table or view). Destructive — requires workbooks:delete.

GET /api/v1/workbooks/{workbook_id}/tables/{name}/a1

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/a1”

Read A1

A1 read facade (v0.5 §7): the coordinate system agents know best.

The table renders as a grid — row 1 is the header (column names), data starts at row 2, columns A.. follow the (policy-applied) column order. range is "B2:D10" or a single cell "C3". Returns {"range", "values": [[...]]} — values only, like the Sheets API. Reads ride the same governance projection as rows.

querytypedefaultdescription
rangestring-

PUT /api/v1/workbooks/{workbook_id}/tables/{name}/a1

Section titled “PUT /api/v1/workbooks/{workbook_id}/tables/{name}/a1”

Write A1

A1 write facade: set a rectangle of cells by spreadsheet coordinates (the partner to GET …/a1).

Grid row 1 is the header; data starts at row 2; columns A.. follow the data-column order. values is a row-major 2-D block matching the range shape. Grid rows that hit existing data rows update; rows past the bottom append (contiguous only). Maps onto row edits — same optimistic locking (expected_version), edit log, and conflict detection as rows.upsert. Writing the header row, a column past the table width, or leaving a gap is a 400.

Request body: A1WriteRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/tables/{name}/access

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/access”

Check Access

Dry-run (spec access.check): what would a read of this artifact return, per column — allow / mask / deny — without reading anything. Honors X-D2B-Acting-User, so a backend can preview a member’s effective access before running queries on their behalf.

POST /api/v1/workbooks/{workbook_id}/tables/{name}/columns

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/columns”

Add Column

Add a column to an editable table (all NULL, or default everywhere). The column lands before __d2b_row_id so the row-id keeps riding last. Type from the create-table allow-list.

Request body: AddColumnRequest (see openapi.json for the schema)

PATCH /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}

Section titled “PATCH /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}”

Alter Column

Rename (new_name) OR retype (type) one column — exactly one per call. Retype casts existing values; a value that can’t cast cleanly is a 400 (clean the data or use a SQL transform).

Request body: AlterColumnRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}”

Drop Column

Drop a column from an editable table. The last data column can’t be dropped (delete the table instead). Any classification on the column is removed.

querytypedefaultdescription
actor--
expected_version--

PUT /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/description

Section titled “PUT /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/description”

Set Column Description

Set or clear the description for one column on an artifact.

The column must exist in the artifact’s schema (we check the schema dictionary stored on the catalog row); a misspelled column name surfaces as 404 rather than silently creating a phantom description that never lights up in get_schema.

Request body: ColumnDescriptionBody (see openapi.json for the schema)

PUT /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/formula

Section titled “PUT /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/formula”

Set Formula

Deliver column as a per-row formula.

expr references columns as {column_name} placeholders (never A1 cell addresses) — e.g. "{running} / {count}". The stored value is untouched; the formula is applied when the table is exported to xlsx. The response carries a verification report — whether the formula reproduces the column’s current values within tolerance — so “replace this constant with a calc” can be confirmed, not hoped.

Request body: SetFormulaRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/formula

Section titled “DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/formula”

Clear Formula

Drop column’s formula — it delivers its stored value again.

POST /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/tags

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/tags”

Tag Column

Attach a sensitivity tag (pii / hr / …) to a column.

Request body: TagColumnRequest (see openapi.json for the schema)

DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/tags/{tag}

Section titled “DELETE /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/tags/{tag}”

Untag Column

Remove a column’s tag (its policies stop applying to this column).

querytypedefaultdescription
actorstring-

PUT /api/v1/workbooks/{workbook_id}/tables/{name}/description

Section titled “PUT /api/v1/workbooks/{workbook_id}/tables/{name}/description”

Set Artifact Description

Set or clear the description for one artifact.

The catalog must already contain a row for this artifact — i.e. the artifact was registered via a prior ingest or transform. We refresh the catalog on entry so a brand-new artifact created seconds ago is reachable.

Request body: ArtifactDescriptionBody (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/tables/{name}/formulas

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/formulas”

List Formulas

The table’s formula columns: {column: expr}.

GET /api/v1/workbooks/{workbook_id}/tables/{name}/lineage

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/lineage”

Get Artifact Lineage

Upstream DAG for one artifact (sources + transform-derived intermediates).

POST /api/v1/workbooks/{workbook_id}/tables/{name}/merge

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/merge”

Merge Export

3-way merge an edited export back into its table.

Cells changed only in the file apply as attributed edits; cells changed on both sides since the branch queue kind="merge" conflicts (D2B’s value stays until someone resolves — take_import applies the file’s value, acknowledge keeps ours). Rows the file deleted delete here when D2B left them untouched; rows D2B deleted are never silently resurrected.

GET /api/v1/workbooks/{workbook_id}/tables/{name}/overrides

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/overrides”

List Overrides

The artifact’s override layer: which cells are shadowed, by what value, over which computed value, by whom and when (doc §6 — the override story is always inspectable).

querytypedefaultdescription
limitinteger500

POST /api/v1/workbooks/{workbook_id}/tables/{name}/overrides/delete

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/overrides/delete”

Drop Override

Remove one cell’s override and restore the computed value it shadowed. The explicit exception ends explicitly.

Request body: DropOverrideRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/tables/{name}/profile

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/profile”

Get Artifact Profile

SUMMARIZE-style statistics for one table (spec §5 tables.profile): per-column null rate, distinct count, numeric ranges, top values and normalisation signals — the deep look schema’s 5-row sample can’t give. Governance applies: denied columns are absent, masked columns carry no statistics (their distribution is what the mask hides), and an enforced read is audited.

GET /api/v1/workbooks/{workbook_id}/tables/{name}/rows

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/rows”

Get Artifact Rows

Paginated rows in JSON shape: {columns: [...], rows: [[...]]}.

The row container is a list-of-lists (column-aligned) rather than list-of-dicts, so per-row payload size stays close to Arrow’s columnar footprint. Arrow + NDJSON content negotiation lands in Slice 3 (proposal §5 row payloads).

querytypedefaultdescription
limitinteger100
offsetinteger0

POST /api/v1/workbooks/{workbook_id}/tables/{name}/rows

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/rows”

Upsert Rows

Bulk upsert rows of a base table.

A row carrying __d2b_row_id updates that row; a row without it inserts (missing columns become NULL, the new __d2b_row_id comes back in row_ids). Atomic: one bad row rolls back the whole call. expected_version must match the table’s current edit version (pass null for a never-edited table); stale writes get 409 with the current version. Each change lands in the edit log with actor attribution.

Request body: UpsertRowsRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/tables/{name}/rows/delete

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/rows/delete”

Delete Rows

Bulk delete rows by __d2b_row_id.

Atomic: an unknown row-id aborts the whole call. Each deleted row’s full content is recorded in the edit log before removal, so the delete is logical-in-effect (nothing is lost) while reads stay clean. Same optimistic locking as upsert.

Request body: DeleteRowsRequest (see openapi.json for the schema)

GET /api/v1/workbooks/{workbook_id}/tables/{name}/schema

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/schema”

Get Artifact Schema

Schema-only response: columns + types + row count.

Carries a 5-row sample so an agent can take a quick look without triggering a paginated rows call.

querytypedefaultdescription
formatstringcolumns

GET /api/v1/workbooks/{workbook_id}/tables/{name}/style

Section titled “GET /api/v1/workbooks/{workbook_id}/tables/{name}/style”

Get Style

Current spec + annotations for one table.

PUT /api/v1/workbooks/{workbook_id}/tables/{name}/style

Section titled “PUT /api/v1/workbooks/{workbook_id}/tables/{name}/style”

Put Style

Replace the table’s style spec.

spec = {header?, columns?, rules?, column_widths?, freeze_header?, borders?}; styles are {bold, italic, underline, color, fill, number_format, align}. Rules carry a SQL predicate ("amount < 0") re-evaluated at delivery — the right tool for bulk styling (“negatives in red”), since one rule never drifts and never costs 10k annotations.

Request body: PutStyleRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/tables/{name}/style/annotations

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/style/annotations”

Annotate

Mark specific cells: {annotations: [{row_id, column, style}]}.

Anchored to __d2b_row_id × column — the same identity anchoring as value edits, so the mark follows the row through sorts and inserts (“THIS row”, not “the 5th row”). For bulk conditions use a spec rule instead.

Request body: AnnotateRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/tables/{name}/style/annotations/delete

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/style/annotations/delete”

Delete Annotations

Request body: DeleteAnnotationsRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/tables/{name}/sync-bindings

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/sync-bindings”

Bind Sync

Store the table ↔ external-sheet mapping (inert — D2B never executes the sync). One sheet maps to one table; binding a governed table returns explicit warnings instead of silently leaking.

Request body: BindRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/tables/{name}/unarchive

Section titled “POST /api/v1/workbooks/{workbook_id}/tables/{name}/unarchive”

Unarchive Artifact

Re-materialise an archived raw artifact from the shared cache (D3, v0.5 §4): the registry row and lineage never left; this brings the physical table back and flips state to active. Use it when an agent needs to descend from a structured table to the raw shape it was derived from (follow get_lineage to find the name).

GET /api/v1/workbooks/{workbook_id}/transforms

Section titled “GET /api/v1/workbooks/{workbook_id}/transforms”

List Transforms

Every authored transform that currently produces an artifact, with its template — the read half of POST /transforms.

One entry per transform → artifact edge (a template applied to two outputs appears twice, once per artifact_name). args carry the CURRENT display names of the bound inputs, so an entry round-trips through POST /transforms unchanged even after a rename. Built-in source extractors (auto_extractors/*) are not listed — they are not authored logic. Templates are code, not rows: column policies do not apply here, exactly as in the app’s Transform view.

This is what d2b pull materialises as transforms/*.sql|py files, so a workbook’s analytical logic can live in the caller’s own git repository and come back through d2b push.

POST /api/v1/workbooks/{workbook_id}/transforms

Section titled “POST /api/v1/workbooks/{workbook_id}/transforms”

Post Transform

Register a SQL or Python transform and materialise its output.

The transform runs synchronously; the response carries the resulting artifact metadata. Mirror of the SPA’s chat-agent path, so agents and humans co-author the same DAG.

Request body: TransformRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/uploads

Section titled “POST /api/v1/workbooks/{workbook_id}/uploads”

Create Upload Slot

Open an upload slot: the bytes go to upload_url (a signed PUT, no bearer needed, valid one hour) — from your own HTTP client, or by a human through upload_page — and POST .../uploads/{id}/ingest turns them into a source. Nothing about the file passes through the caller, which is the point for agents.

Request body: UploadSlotRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/uploads/{upload_id}/ingest

Section titled “POST /api/v1/workbooks/{workbook_id}/uploads/{upload_id}/ingest”

Ingest Upload Slot

Turn a received upload into a source (same response as POST /workbooks/{id}/sources, plus upload_id).

GET /api/v1/workbooks/{workbook_id}/versions

Section titled “GET /api/v1/workbooks/{workbook_id}/versions”

List Versions

POST /api/v1/workbooks/{workbook_id}/versions

Section titled “POST /api/v1/workbooks/{workbook_id}/versions”

Create Version

Commit the current state as a named version (label → snapshot).

Labels are immutable; committing an existing label is a 409.

Request body: CreateVersionRequest (see openapi.json for the schema)

POST /api/v1/workbooks/{workbook_id}/versions/{label}/revert

Section titled “POST /api/v1/workbooks/{workbook_id}/versions/{label}/revert”

Revert To Version

Restore the workbook to the named version’s snapshot — the same destructive machinery (and the same events) as snapshot restore.