Quickstart
1. Authentication — mint a PAT
Section titled “1. Authentication — mint a PAT”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):
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 needsworkbooks: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_idto target one of your developer accounts instead.GET /api/v1/mereportsaccount_id/account_name resourcenarrows the reach:workbook:<id>(that workbook only — the recommended shape when handing a token to an agent) orworkspace:<id>(that workspace’s workbooks only; creates land there too). The defaultaccountis everything under the account. Reach is set byresourcealone; scopes say what the key may do — a key withoutworkspaces:readstill creates in any workspace of its account when pinned toaccount. Naming a workspace the key does not reach inworkspace_idis a 403 for listing and creation alikeGET /api/v1/me/workspaceslists the workspaces the token reaches, by name (is_default= where a create lands whenworkspace_idis omitted)./meand the workbook listing carryworkspace_nameas well- Workspace provisioning is
workspaces:create, key minting iskeys:mint, billing operations arebilling: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_idlands 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 aworkspaces:createkey (POST /api/control/workspaces)
2. Five minutes in Python
Section titled “2. Five minutes in Python”pip install d2b-sdkfrom d2b import D2BClient
client = D2BClient(api_key="d2b_pat_...", base_url="https://d2b.dev")
# 1) A workbook is the working containerwb = 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 tablesresult = 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 humansxlsx = client.export.tables(wb, tables=["product_sales"]) # formatted xlsxoriginal = client.sources.render_template(wb, "sales_2026-06.xlsx") # original formatting, values refreshed
# 7) Pin a versionclient.versions.commit(wb, "2026-06")3. The same thing in curl
Section titled “3. The same thing in curl”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\""}'4. Or the CLI
Section titled “4. Or the CLI”pipx install d2b-sdk # or uvx --from d2b-sdk d2b …d2b logind2b workbooks create --title monthlyd2b upload sales.xlsx --workbook WB --waitd2b query 'SELECT count(*) FROM "sales"' --workbook WBNext: Core concepts, Connect over MCP, Your workbook in git.
Messy Excel — when auto, when staged
Section titled “Messy Excel — when auto, when staged”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:
- Re-upload the same file with
mode=staged(bytes only, zero interpretation) POST .../sources/{name}/analyze→ review and correct the returned parse spec (header row, data range, types)POST .../sources/{name}/materializeto 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).