Skip to content

Quickstart

Every /api/v1 call uses PAT (Personal Access Token) Bearer auth. Mint one from the console, or from the API with a logged-in session (JWT):

Terminal window
curl -X POST https://d2b.dev/api/v1/me/tokens \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"name": "my-agent", "scopes": ["workbooks:read", "workbooks:write"]}'
# → {"token": "d2b_pat_...", ...} (the token is shown in plaintext only in this response)
  • Scopes are resource×action: workbooks:read/write/delete, cloud-files:read (list / import from Drive / OneDrive — on by default for logins, explicit on PATs), governance:configure, workspaces:read/create/configure/delete, keys:read/mint/revoke, account:read, billing:manage. Implication stays within one resource (write ⊃ read); deleting a workbook, restoring a snapshot or reverting a version needs workbooks:delete
  • A token is always bound to exactly one account (one token = one account). Minting from a logged-in session targets your default account; pass account_id to target one of your developer accounts instead. GET /api/v1/me reports account_id / account_name
  • resource narrows the reach: workbook:<id> (that workbook only — the recommended shape when handing a token to an agent) or workspace:<id> (that workspace’s workbooks only; creates land there too). The default account is everything under the account. Reach is set by resource alone; scopes say what the key may do — a key without workspaces:read still creates in any workspace of its account when pinned to account. Naming a workspace the key does not reach in workspace_id is a 403 for listing and creation alike
  • GET /api/v1/me/workspaces lists the workspaces the token reaches, by name (is_default = where a create lands when workspace_id is omitted). /me and the workbook listing carry workspace_name as well
  • Workspace provisioning is workspaces:create, key minting is keys:mint, billing operations are billing:manage — permissions are independent resource×action scopes with no umbrella scope; mint keys carrying exactly what they need (GitHub fine-grained PAT style)
  • A create that omits workspace_id lands in the token’s account’s oldest workspace (the personal workspace for the default account). A developer account with no workspace returns 409 — create one first in the console (Workspaces) or with a workspaces:create key (POST /api/control/workspaces)
Terminal window
pip install d2b-sdk
from d2b import D2BClient
client = D2BClient(api_key="d2b_pat_...", base_url="https://d2b.dev")
# 1) A workbook is the working container
wb = client.workbooks.create(title="monthly-sales")["id"]
# 2) Throw the messy Excel at it. wait=True has the SDK babysit the 202+job.
# After extraction and structuring you get clean, typed tables
result = client.sources.upload(wb, "sales_2026-06.xlsx", wait=True)
# 3) See what landed (the basic agent move)
for t in client.tables.list(wb):
print(t["name"], t["row_count"])
schema = client.tables.schema(wb, "sales")
# 4) Analyse with SQL (governance applied, read-only)
out = client.query.sql(wb, 'SELECT product, sum(amount) FROM "sales" GROUP BY 1')
# 5) Derived tables are transforms (lineage is preserved)
client.transforms.create(
wb, name="agg/monthly", kind="sql",
template='CREATE OR REPLACE TABLE "{{ artifact_name }}" AS '
'SELECT product, sum(amount) AS revenue FROM "{{ src }}" GROUP BY 1',
artifact_name="product_sales", args={"src": "sales"},
)
# 6) Back to humans
xlsx = client.export.tables(wb, tables=["product_sales"]) # formatted xlsx
original = client.sources.render_template(wb, "sales_2026-06.xlsx") # original formatting, values refreshed
# 7) Pin a version
client.versions.commit(wb, "2026-06")
Terminal window
BASE=https://d2b.dev; H="Authorization: Bearer $D2B_PAT"
WB=$(curl -s -X POST $BASE/api/v1/workbooks -H "$H" -H "Content-Type: application/json" \
-d '{"title": "monthly"}' | jq -r .id)
curl -s -X POST $BASE/api/v1/workbooks/$WB/sources -H "$H" -F file=@sales.xlsx -F mode=auto -F async=true
# → {"job_id": ...} → poll GET $BASE/api/v1/jobs/{job_id}
curl -s $BASE/api/v1/workbooks/$WB/tables -H "$H"
curl -s -X POST $BASE/api/v1/workbooks/$WB/query -H "$H" -H "Content-Type: application/json" \
-d '{"sql": "SELECT count(*) FROM \"sales\""}'
Terminal window
pipx install d2b-sdk # or uvx --from d2b-sdk d2b …
d2b login
d2b workbooks create --title monthly
d2b upload sales.xlsx --workbook WB --wait
d2b query 'SELECT count(*) FROM "sales"' --workbook WB

Next: Core concepts, Connect over MCP, Your workbook in git.

Start with the default mode=auto (D2B cuts each sheet into its tables and reads merged headers, unit rows, subtotal rows and hierarchy to shape them). Drop to staged only when extraction or structuring missed:

  1. Re-upload the same file with mode=staged (bytes only, zero interpretation)
  2. POST .../sources/{name}/analyze → review and correct the returned parse spec (header row, data range, types)
  3. POST .../sources/{name}/materialize to commit

The decision flow is: auto → eyeball the result → if it missed, take control of the parse spec via staged. The auto result stays under its own name, so you can compare while fixing.

The upload response’s structuring field states the outcome: structured, skipped (you passed structuring=skip — the raw tables only), deferred (you passed structuring=defer — swapped in later), raw_fallback (structuring was requested but the raw tables came back; structuring_reason says why — no_credits, building (re-upload to retry), failed:… — or switch to staged), or off (structuring is disabled on this server). The CLI prints a stderr warning on raw_fallback.

Uploads through the API / CLI / SDK / MCP are structured by D2B by default (structuring=auto): each sheet is split into its tables and the titles and notes around them (<sheet>_補足情報), and the tables cut from it stay as they are laid out on the sheet. From those, tables with periods across the columns are reshaped to long form, and report tables with subtotals and totals are split into one table per level of their hierarchy (<table>_階層1, <table>_階層2 …; totals and differences computed from rows of the same level go to <table>_階層1_計算項目 and so on) and their account tree (<table>_科目). A sheet that is a single table with nothing around it is not split, and its shaped table takes over the file’s name. The original sheet stays in lineage. Structuring spends the workbook’s credits (uploading the same content again is served from the cache, with no structuring charge). To reshape the raw tables with your own LLM and add_transform instead, pass --no-structuring (API: structuring=skip): each sheet lands as it is, in ~1s, for free. --defer-structuring (structuring=defer) returns the raw tables now, structures in the background and swaps the structured tables in when ready (artifact.updated fires on the swap).