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-Keyheader (same key + same body replays the first response) - Webhook signatures:
X-D2B-Signature: sha256=<hex>= HMAC-SHA256(secret, raw_body)
GET /api/control/account
Section intitulée « GET /api/control/account »Get Account
One Account (account_id picks among the caller’s; default = the
oldest, lazily provisioned on first touch).
| query | type | default | description |
|---|---|---|---|
account_id | - | - |
GET /api/control/account/billing
Section intitulée « GET /api/control/account/billing »Account Billing
| query | type | default | description |
|---|---|---|---|
account_id | - | - |
PATCH /api/control/account/billing/auto-topup
Section intitulée « 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 intitulée « 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)
POST /api/control/account/billing/payg
Section intitulée « POST /api/control/account/billing/payg »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)
POST /api/control/account/billing/portal
Section intitulée « POST /api/control/account/billing/portal »Account Billing Portal
Request body: DevPortalRequest (see openapi.json for the schema)
POST /api/control/account/billing/reuse-card
Section intitulée « 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)
POST /api/control/account/billing/topup
Section intitulée « POST /api/control/account/billing/topup »Account Billing Topup
Buy a prepaid credit pack — raises the Account’s metering ceiling.
Request body: DevTopupRequest (see openapi.json for the schema)
POST /api/control/account/delete
Section intitulée « POST /api/control/account/delete »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;
confirmmatches 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)
GET /api/control/account/keys
Section intitulée « GET /api/control/account/keys »List Account Keys
| query | type | default | description |
|---|---|---|---|
account_id | - | - |
POST /api/control/account/keys
Section intitulée « POST /api/control/account/keys »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)
DELETE /api/control/account/keys/{key_id}
Section intitulée « DELETE /api/control/account/keys/{key_id} »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.
| query | type | default | description |
|---|---|---|---|
account_id | - | - |
GET /api/control/account/usage/series
Section intitulée « GET /api/control/account/usage/series »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).
| query | type | default | description |
|---|---|---|---|
days | integer | 30 | |
account_id | - | - |
GET /api/control/accounts
Section intitulée « GET /api/control/accounts »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).
| query | type | default | description |
|---|---|---|---|
provision | boolean | True |
POST /api/control/accounts
Section intitulée « POST /api/control/accounts »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)
GET /api/control/workspaces
Section intitulée « GET /api/control/workspaces »List Workspaces
| query | type | default | description |
|---|---|---|---|
account_id | - | - |
POST /api/control/workspaces
Section intitulée « POST /api/control/workspaces »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 intitulée « GET /api/control/workspaces/{workspace_id} »Get Workspace
PATCH /api/control/workspaces/{workspace_id}
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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.
GET /api/v1/cloud-files/{provider}
Section intitulée « GET /api/v1/cloud-files/{provider} »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.
| query | type | default | description |
|---|---|---|---|
folder_id | string | root | |
q | - | - | Name search (SharePoint only) |
GET /api/v1/jobs
Section intitulée « GET /api/v1/jobs »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 /api/v1/jobs/{job_id}
Section intitulée « GET /api/v1/jobs/{job_id} »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 /api/v1/me
Section intitulée « GET /api/v1/me »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.
GET /api/v1/me/authorize/{provider}
Section intitulée « GET /api/v1/me/authorize/{provider} »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.
| query | type | default | description |
|---|---|---|---|
workbook_id | - | - |
GET /api/v1/me/data
Section intitulée « GET /api/v1/me/data »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}/....
| query | type | default | description |
|---|---|---|---|
cursor | - | - | Opaque cursor from a prior call. |
limit | integer | 50 |
GET /api/v1/me/tokens
Section intitulée « GET /api/v1/me/tokens »List Access Tokens Endpoint
List the calling user’s PATs. Plaintext is never present —
the response is the redacted to_api form.
POST /api/v1/me/tokens
Section intitulée « POST /api/v1/me/tokens »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)
DELETE /api/v1/me/tokens/{token_id}
Section intitulée « DELETE /api/v1/me/tokens/{token_id} »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.
GET /api/v1/me/usage
Section intitulée « GET /api/v1/me/usage »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.
| query | type | default | description |
|---|---|---|---|
days | integer | 30 |
GET /api/v1/me/webhooks
Section intitulée « GET /api/v1/me/webhooks »List Webhooks
POST /api/v1/me/webhooks
Section intitulée « POST /api/v1/me/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 /api/v1/me/webhooks/{webhook_id}
Section intitulée « DELETE /api/v1/me/webhooks/{webhook_id} »Delete Webhook Endpoint
GET /api/v1/me/webhooks/{webhook_id}/deliveries
Section intitulée « 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.
GET /api/v1/me/workbooks
Section intitulée « GET /api/v1/me/workbooks »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.
| query | type | default | description |
|---|---|---|---|
cursor | - | - | Opaque pagination cursor returned by a prior call. |
limit | integer | 50 | |
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. |
GET /api/v1/me/workspaces
Section intitulée « GET /api/v1/me/workspaces »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 /api/v1/results/{result_id}
Section intitulée « GET /api/v1/results/{result_id} »Get Result
Read a materialised result by id. Supports JSON, NDJSON, Arrow.
| query | type | default | description |
|---|---|---|---|
limit | integer | 1000 | |
offset | integer | 0 |
GET /api/v1/search
Section intitulée « GET /api/v1/search »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.
| query | type | default | description |
|---|---|---|---|
q | string | - | Free-form keyword query. |
limit | integer | 20 |
GET /api/v1/uploads/{upload_id}
Section intitulée « GET /api/v1/uploads/{upload_id} »Get Upload Slot
Slot status: pending (waiting for bytes; upload_url is
included), uploaded (ready to ingest) or ingested.
PUT /api/v1/uploads/{upload_id}
Section intitulée « PUT /api/v1/uploads/{upload_id} »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.
| query | type | default | description |
|---|---|---|---|
expires | integer | - | |
sig | string | - |
POST /api/v1/workbooks
Section intitulée « POST /api/v1/workbooks »Create Workbook
Create a fresh workbook owned by the calling user (see
:func:create_workbook_for_principal for the scope contract).
GET /api/v1/workbooks/{workbook_id}
Section intitulée « GET /api/v1/workbooks/{workbook_id} »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 /api/v1/workbooks/{workbook_id}
Section intitulée « DELETE /api/v1/workbooks/{workbook_id} »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 intitulée « 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 intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
actor | - | - | |
expected_version | - | - |
POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/columns/{column}/tags
Section intitulée « 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 intitulée « 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).
| query | type | default | description |
|---|---|---|---|
actor | string | - |
GET /api/v1/workbooks/{workbook_id}/artifacts/{name}/schema
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
format | string | columns |
POST /api/v1/workbooks/{workbook_id}/artifacts/{name}/sync-bindings
Section intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
limit | integer | 200 |
GET /api/v1/workbooks/{workbook_id}/branches
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
limit | integer | 200 |
GET /api/v1/workbooks/{workbook_id}/charts
Section intitulée « 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 intitulée « GET /api/v1/workbooks/{workbook_id}/classifications »List Classifications
Every column tag in the workbook (the targets policies bind to).
| query | type | default | description |
|---|---|---|---|
limit | integer | 500 |
GET /api/v1/workbooks/{workbook_id}/conflicts
Section intitulée « GET /api/v1/workbooks/{workbook_id}/conflicts »List Conflicts
The workbook’s review queue. ?status=open filters to the items
still awaiting a decision.
| query | type | default | description |
|---|---|---|---|
status | - | - | |
limit | integer | 200 |
POST /api/v1/workbooks/{workbook_id}/conflicts/{conflict_id}/resolve
Section intitulée « 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 intitulée « 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)
GET /api/v1/workbooks/{workbook_id}/file-links
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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).
GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/cells
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
sheet | - | - |
GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/diff
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
against | - | - |
GET /api/v1/workbooks/{workbook_id}/file-links/{link_id}/versions/{n}/download
Section intitulée « 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 intitulée « 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.
GET /api/v1/workbooks/{workbook_id}/ops
Section intitulée « GET /api/v1/workbooks/{workbook_id}/ops »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).
| query | type | default | description |
|---|---|---|---|
limit | integer | 50 | |
before | - | - | Page: only ops with op_id < before. |
POST /api/v1/workbooks/{workbook_id}/ops/{op_id}/undo
Section intitulée « 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 intitulée « GET /api/v1/workbooks/{workbook_id}/policies »List Policies
The workbook’s tag→effect rules.
| query | type | default | description |
|---|---|---|---|
limit | integer | 500 |
PUT /api/v1/workbooks/{workbook_id}/policies/{tag}
Section intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
actor | string | - | |
role | - | - |
POST /api/v1/workbooks/{workbook_id}/query
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « GET /api/v1/workbooks/{workbook_id}/sheets »List Sheets
GET /api/v1/workbooks/{workbook_id}/sheets/{name}
Section intitulée « GET /api/v1/workbooks/{workbook_id}/sheets/{name} »Get Sheet
PUT /api/v1/workbooks/{workbook_id}/sheets/{name}
Section intitulée « 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 intitulée « DELETE /api/v1/workbooks/{workbook_id}/sheets/{name} »Delete Sheet
GET /api/v1/workbooks/{workbook_id}/sheets/{name}/render
Section intitulée « 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 intitulée « GET /api/v1/workbooks/{workbook_id}/snapshots »List Snapshots
List committed snapshots for the workbook.
POST /api/v1/workbooks/{workbook_id}/snapshots
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
rows | integer | 20 |
GET /api/v1/workbooks/{workbook_id}/sources/{source_name}/render
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
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 intitulée « 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)
POST /api/v1/workbooks/{workbook_id}/sync
Section intitulée « POST /api/v1/workbooks/{workbook_id}/sync »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 intitulée « GET /api/v1/workbooks/{workbook_id}/sync-bindings »List Sync Bindings
The workbook’s stored bindings (optionally one artifact’s).
| query | type | default | description |
|---|---|---|---|
artifact | - | - | |
limit | integer | 200 |
DELETE /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}
Section intitulée « DELETE /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id} »Unbind Sync
Remove a stored binding (the external sheet itself is untouched).
| query | type | default | description |
|---|---|---|---|
actor | string | - |
GET /api/v1/workbooks/{workbook_id}/sync-bindings/{binding_id}/snapshot
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
limit | integer | 10000 |
GET /api/v1/workbooks/{workbook_id}/tables
Section intitulée « 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.
| query | type | default | description |
|---|---|---|---|
include_archived | boolean | False |
POST /api/v1/workbooks/{workbook_id}/tables
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
range | string | - |
PUT /api/v1/workbooks/{workbook_id}/tables/{name}/a1
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
actor | - | - | |
expected_version | - | - |
PUT /api/v1/workbooks/{workbook_id}/tables/{name}/columns/{column}/description
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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).
| query | type | default | description |
|---|---|---|---|
actor | string | - |
PUT /api/v1/workbooks/{workbook_id}/tables/{name}/description
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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).
| query | type | default | description |
|---|---|---|---|
limit | integer | 500 |
POST /api/v1/workbooks/{workbook_id}/tables/{name}/overrides/delete
Section intitulée « 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 intitulée « 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 intitulée « 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).
| query | type | default | description |
|---|---|---|---|
limit | integer | 100 | |
offset | integer | 0 |
POST /api/v1/workbooks/{workbook_id}/tables/{name}/rows
Section intitulée « 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 intitulée « 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 intitulée « 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.
| query | type | default | description |
|---|---|---|---|
format | string | columns |
GET /api/v1/workbooks/{workbook_id}/tables/{name}/style
Section intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « 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 intitulée « GET /api/v1/workbooks/{workbook_id}/versions »List Versions
POST /api/v1/workbooks/{workbook_id}/versions
Section intitulée « 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 intitulée « 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.