Worksheets Developer Platform
Build on Worksheets
Read and write spreadsheets from your own code. Push data from your database, pull reports into your app, build dashboards that update themselves, and react to changes the moment someone edits a cell.
Quickstart
Make your first API call in about two minutes.
- Create an access token. Sign in to Worksheets, open the account menu (your avatar, top right) and choose AI connections (MCP) → Create token. Copy the
wst_…token. It's shown only once. - Install an SDK, or skip this and use cURL.
npm install /developers/downloads/beezifi-worksheets-1.0.0.tgzpip install /developers/downloads/beezifi-worksheets-python-1.0.0.zip - Set your credentials as environment variables.
export WORKSHEET_URL= export WORKSHEET_TOKEN=wst_your_token_here - Create a workbook, write data and read the result.
import { Worksheet } from '@beezifi/worksheets'; const ws = new Worksheet(); // reads WORKSHEET_URL and WORKSHEET_TOKEN const wb = await ws.workbooks.create({ title: 'My first API workbook' }); await wb.write('A1', [ ['Month', 'Revenue'], ['Oct', 42000], ['Nov', 45500], ['Total', '=SUM(B2:B3)'], ]); await wb.batch().bold('A1:B1').numberFormat('B2:B4', '$#,##0').addChart('A1:B3', { title: 'Revenue' }).commit(); console.log(await wb.get('B4')); // 87500 console.log(wb.url); // open it in the appfrom worksheet import Worksheet ws = Worksheet() # reads WORKSHEET_URL and WORKSHEET_TOKEN wb = ws.workbooks.create(title="My first API workbook") wb.write("A1", [ ["Month", "Revenue"], ["Oct", 42000], ["Nov", 45500], ["Total", "=SUM(B2:B3)"], ]) wb.batch().bold("A1:B1").number_format("B2:B4", "$#,##0").add_chart("A1:B3", title="Revenue").commit() print(wb.get("B4")) # 87500 print(wb.url) # open it in the app# create curl -s -X POST "$WORKSHEET_URL/api/workbooks" \ -H "Authorization: Bearer $WORKSHEET_TOKEN" -H "Content-Type: application/json" \ -d '{"title":"My first API workbook"}' # → {"id":"5d7c…","title":"My first API workbook"} # write curl -s -X POST "$WORKSHEET_URL/api/workbooks/5d7c…/commands" \ -H "Authorization: Bearer $WORKSHEET_TOKEN" -H "Content-Type: application/json" \ -d '{"commands":[{"id":"range.setInputs","params":{"range":"A1","rows":[["Month","Revenue"],["Oct","42000"],["Nov","45500"],["Total","=SUM(B2:B3)"]]}}]}' # read curl -s "$WORKSHEET_URL/api/workbooks/5d7c…/cells?range=B4" -H "Authorization: Bearer $WORKSHEET_TOKEN" # → {"sheet":"Sheet1","range":"B4","values":[[87500]]}
Authentication
Every request carries a personal access token in the Authorization header:
Authorization: Bearer wst_3hObNTld…- Tokens act as you. They can open every workbook you can, with your role in each: viewers can read, editors can edit, owners can also share and delete.
- Shown once, stored hashed. Worksheets keeps only a SHA-256 hash. If you lose a token, revoke it and create a new one.
- Revoke any time in AI connections. Revocation takes effect on the next request. The list shows when each token was last used.
- Tokens can't create tokens. Managing tokens requires signing in to the app, so a leaked token can't create more.
- Keep tokens server-side. Put them in environment variables or a secrets manager, never in front-end code or source control. Use one token per integration so you can revoke them separately.
Core concepts
- Workbook
- A spreadsheet file, identified by a UUID such as
5d7c2e1a-…. You see it in the app URL:/app.html?id=…. - Sheet
- A tab inside a workbook. Pass
sheetby name (or numeric id). When omitted, the first/active sheet is used. - Range
- A1 notation:
A1,B2:D10, whole columnsA:C, whole rows3:5. Whole rows/columns are clipped to the used area when reading. - Input
- Text exactly as you would type it into a cell.
=SUM(A1:A3)becomes a formula,12%a percentage,2026-01-31a date,$1,200a currency number,TRUEa boolean. - Value
- What a cell evaluates to: number, string, boolean or
null. Errors are objects like{"e": "#DIV/0!"}; dates are serial numbers (days since 1899-12-30). Read withmode=displayto get formatted text instead. - Command
- One editing action, such as
format.set,rows.insertorchart.add. Everything that changes a workbook is a command; the SDK helpers build them for you. See the command reference. - Live document
- Your commands are applied to the same live copy people have open in their browsers: they see each change immediately, formulas recalculate at once, and the workbook autosaves within about 1.5 seconds. Every change is recorded in the activity log, and versions are snapshotted automatically every 30 minutes during editing.
Errors
Errors use standard HTTP status codes and a consistent JSON body. The requestId (also in the X-Request-Id header) identifies the request in server logs; include it when contacting support.
{
"error": "You have view-only access to this workbook.",
"code": "forbidden",
"requestId": "db6fb32a-63a7-4cd6-a65c-8d91444e3815"
}| Status | code | Meaning | SDK exception |
|---|---|---|---|
| 400 | invalid_request | Invalid parameters, range or command. | ValidationError / CommandError |
| 401 | unauthorized | Missing, invalid or revoked token. | AuthenticationError |
| 403 | forbidden | Not allowed: view-only access, or an owner-only action. | PermissionError |
| 404 | not_found | Doesn't exist, or isn't shared with you (we don't reveal which). | NotFoundError |
| 409 | conflict | Conflicts with existing state. | ConflictError |
| 413 | payload_too_large | Upload or workbook over the size limit. | ValidationError |
| 429 | rate_limited | Too many requests; see rate limits. | RateLimitError |
| 5xx | server_error | Temporary problem on our side; retry with backoff. | ServerError |
Command failures
POST /workbooks/{id}/commands runs commands in order and stops at the first failure. The response is still 200; check each result's ok. Commands before the failure stay applied. The SDKs turn this into a CommandError carrying the failing index and command.
{ "results": [ { "ok": true, "seq": 41 }, { "ok": false, "error": "Invalid range: ZZZ" } ] }Rate limits and sizes
| Requests | 600 per minute per token (configurable by your administrator with API_RATE_LIMIT) |
|---|---|
| Commands per request | 500 (the SDKs split longer lists automatically) |
| Upload size | 25 MB by default (MAX_UPLOAD_MB) |
| Workbook size | 40 MB of stored data by default (MAX_WORKBOOK_MB) |
| Sheet size | 1,048,576 rows × 16,384 columns |
| MCP reads | 20,000 cells per read_range call |
Every API response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets). A 429 is rejected before any work is done, so it is always safe to retry after RateLimit-Reset seconds. Both SDKs do this automatically.
JavaScript & TypeScript SDK @beezifi/worksheets 1.0.0
Zero dependencies. Runs on Node.js 18+, Deno, Bun, Cloudflare Workers and modern browsers, with full TypeScript definitions. ES module.
npm install /developers/downloads/beezifi-worksheets-1.0.0.tgzimport { Worksheet, CommandError } from '@beezifi/worksheets';
const ws = new Worksheet({
baseUrl: '',
token: process.env.WORKSHEET_TOKEN,
timeout: 30_000, // per request (ms)
maxRetries: 2, // network errors, 429 and 5xx on safe requests
});
// workbooks
const list = await ws.workbooks.list({ search: 'budget' });
const wb = await ws.workbooks.create({ template: 'sales', title: 'Sales FY27' });
const same = ws.workbook(wb.id); // handle by id
const { sheets } = await wb.summary(); // names, used ranges, tables, charts
// read
await wb.read('B4:G10'); // values
await wb.read('C5', { mode: 'display' }); // [['$48,200']]
await wb.readRecords('B4:G10'); // [{ Region: 'North', Q1: 48200, … }]
// write and format, one request
await wb.batch({ sheet: 'Sales' })
.write('K1', [['Target'], [250000]])
.setCell('K3', '=G11/K2')
.numberFormat('K3', '0.0%')
.highlightCells('H5:H10', 'lessThan', 0.2)
.commit();
// rows from your app
await wb.append('B4:G4', [{ Region: 'APAC', Q1: 21000, Q2: 26000, Q3: 30500, Q4: 34100 }]);
// sharing, versions, files
await wb.shares.add('maya@example.com', 'editor');
await wb.versions.save('Before import');
const xlsx = await wb.export('xlsx'); // ArrayBufferThe package includes a detailed README.md, CHANGELOG.md, typings and runnable examples: quick start, append records, watch changes, nightly backup and CSV-to-report.
Python SDK beezifi-worksheets 1.0.0
Standard library only, Python 3.9+. The same features as the JavaScript SDK in snake_case, typed with py.typed. (Realtime events are JavaScript-only; in Python, poll wb.activity().)
pip install /developers/downloads/beezifi-worksheets-python-1.0.0.zipfrom worksheet import Worksheet, CommandError
ws = Worksheet(base_url="", token="wst_...", timeout=30, max_retries=2)
wb = ws.workbooks.create(template="sales", title="Sales FY27")
print(wb.summary()["sheets"][0]["usedRange"]) # 'A1:I17'
rows = wb.read_records("B4:G10") # list of dicts
(wb.batch(sheet="Sales")
.write("K1", [["Target"], [250000]])
.set_cell("K3", "=G11/K2")
.number_format("K3", "0.0%")
.highlight_cells("H5:H10", "lessThan", 0.2)
.commit())
imported = ws.workbooks.import_file("report.xlsx")
open("copy.xlsx", "wb").write(wb.export("xlsx"))Command helpers
Both SDKs wrap every command in a helper. Call it on the workbook for a single change, or chain helpers on batch() and send them together with commit(). JavaScript names are shown; Python uses snake_case (numberFormat → number_format). The last argument accepts { sheet }.
| Area | Helpers |
|---|---|
| Contents | setCell write setValues clear fill move |
| Formatting | format numberFormat bold italic toggle borders cellStyle copyFormat merge unmerge |
| Rows & columns | insertRows deleteRows hideRows insertColumns deleteColumns hideColumns insertCells deleteCells columnWidth rowHeight |
| Sheets | addSheet renameSheet deleteSheet duplicateSheet moveSheet tabColor hideSheet gridlines freeze |
| Data | sort filter filterColumn reapplyFilter removeDuplicates replaceAll validation defineName deleteName |
| Conditional formats | highlightCells colorScale dataBars iconSet conditionalFormat clearConditionalFormats |
| Tables & charts | addTable updateTable convertTableToRange addChart updateChart deleteChart addSparklines clearSparklines |
| Other | note link setTitle and command(id, params) for anything else |
Style properties
format(range, style) merges these keys; set one to null to remove it.
| Key | Meaning | Example |
|---|---|---|
b i u st | Bold, italic, underline, strikethrough | true |
ff fs | Font family, size in points | "Aptos", 12 |
fc bg | Font color, fill color | "#1F4E79" |
ha va | Horizontal / vertical alignment | "center", "middle" |
wrap indent rot | Wrap text, indent level, rotation in degrees | true, 2, 45 |
nf | Number format (Excel syntax) | "$#,##0.00", "0.0%", "yyyy-mm-dd" |
bd | Borders per side t b l r | {"b": "double #000000"} |
Other languages
The API is plain HTTPS and JSON, so any language works. Generate a typed client from the OpenAPI 3.1 spec:
# Go, Java, C#, Ruby, PHP, Kotlin, Swift, Rust …
npx @openapitools/openapi-generator-cli generate \
-i /developers/openapi.json -g go -o ./worksheet-goYou can also import the spec into Postman or Insomnia to try every endpoint.
Guide: read data
Start with the summary to find sheet names and where the data is, then read just that range. readRecords turns a table with a header row into objects.
const { sheets } = await wb.summary();
const orders = sheets.find((s) => s.name === 'Orders'); // usedRange e.g. "A1:F842"
const rows = await wb.readRecords(orders.usedRange, { sheet: 'Orders' });
const late = rows.filter((r) => r.Status === 'Late');
sheets = wb.summary()["sheets"]
orders = next(s for s in sheets if s["name"] == "Orders")
rows = wb.read_records(orders["usedRange"], sheet="Orders")
late = [r for r in rows if r["Status"] == "Late"]
Need the text exactly as people see it (currency, dates, percentages)? Use mode: 'display'. Need the formulas? Use mode: 'formulas'.
Guide: write and format
Write values and formulas as rows, then style them, all in one batch. Formulas recalculate immediately and anyone viewing sees the change live.
await wb.batch({ sheet: 'Invoice' })
.write('A1', [
['Item', 'Qty', 'Price', 'Total'],
['Design', 12, 95, '=B2*C2'],
['Hosting', 1, 240, '=B3*C3'],
['', '', 'Subtotal', '=SUM(D2:D3)'],
['', '', 'Tax', '=D4*TaxRate'],
['', '', 'Total', '=D4+D5'],
])
.defineName('TaxRate', '=Settings!$B$1')
.bold('A1:D1').cellStyle('C6:D6', 'Total')
.numberFormat('C2:D6', '$#,##0.00')
.borders('A1:D3', 'all')
.columnWidth('A:D', [160, 60, 100, 110])
.commit();
(wb.batch(sheet="Invoice")
.write("A1", [
["Item", "Qty", "Price", "Total"],
["Design", 12, 95, "=B2*C2"],
["Hosting", 1, 240, "=B3*C3"],
["", "", "Subtotal", "=SUM(D2:D3)"],
["", "", "Tax", "=D4*TaxRate"],
["", "", "Total", "=D4+D5"],
])
.define_name("TaxRate", "=Settings!$B$1")
.bold("A1:D1").cell_style("C6:D6", "Total")
.number_format("C2:D6", "$#,##0.00")
.borders("A1:D3", "all")
.column_width("A:D", [160, 60, 100, 110])
.commit())
Guide: build a report
Turn raw rows into a structured table, then add a summary that uses table formulas, plus a chart. Because the summary uses formulas, it stays correct when rows are added later.
await wb.batch()
.addTable('A1:E500', { sheet: 'Orders', name: 'Orders', style: 'TableStyleMedium2' })
.addSheet('Summary')
.write('A1', [
['Region', 'Revenue'],
['North', '=SUMIFS(Orders[Amount], Orders[Region], A2)'],
['South', '=SUMIFS(Orders[Amount], Orders[Region], A3)'],
['Online', '=SUMIFS(Orders[Amount], Orders[Region], A4)'],
], { sheet: 'Summary' })
.numberFormat('B2:B4', '$#,##0', { sheet: 'Summary' })
.dataBars('B2:B4', '#147a40', { sheet: 'Summary' })
.addChart('A1:B4', { sheet: 'Summary', type: 'pie', title: 'Revenue by region', at: 'D2' })
.commit();Guide: sync safely
For scheduled jobs that replace data, save a version first and restore it if anything fails. People keep a clean workbook even if your job crashes halfway.
await wb.versions.save(`Before sync ${new Date().toISOString().slice(0, 10)}`);
const [checkpoint] = await wb.versions.list();
try {
await wb.batch({ sheet: 'Data' }).clear('A2:F100000').write('A2', rowsFromDb).commit();
} catch (err) {
await wb.versions.restore(checkpoint.id);
throw err;
}
wb.versions.save(f"Before sync {datetime.date.today()}")
checkpoint = wb.versions.list()[0]
try:
wb.batch(sheet="Data").clear("A2:F100000").write("A2", rows_from_db).commit()
except WorksheetError:
wb.versions.restore(checkpoint["id"])
raise
429, which is always rejected before any work. If you build your own retries, follow the same rule.Guide: import and export
import { readFile, writeFile } from 'node:fs/promises';
const wb = await ws.workbooks.import(await readFile('q3.xlsx'), { filename: 'q3.xlsx' });
console.log(wb.warnings); // anything that couldn't be imported
await writeFile('q3-updated.xlsx', Buffer.from(await wb.export('xlsx')));
const csv = await wb.export('csv', { sheet: 'Summary' }); // one sheet as text
wb = ws.workbooks.import_file("q3.xlsx")
print(wb.warnings)
open("q3-updated.xlsx", "wb").write(wb.export("xlsx"))
csv_text = wb.export("csv", sheet="Summary")
curl -s -X POST "$WORKSHEET_URL/api/workbooks/import" \
-H "Authorization: Bearer $WORKSHEET_TOKEN" -F "file=@q3.xlsx"
curl -s -o q3.xlsx "$WORKSHEET_URL/api/workbooks/$ID/export?format=xlsx" \
-H "Authorization: Bearer $WORKSHEET_TOKEN"
Imports accept .xlsx, .xlsm, .csv, .tsv and .txt. The .xlsx round trip keeps values, formulas, number formats, fonts, fills, borders, alignment, merges, sizes, frozen panes, notes, links and names. Charts, tables, sparklines and conditional formats live inside Worksheets but aren't written to .xlsx yet.
REST API reference
Base URL: /api. Generated from the OpenAPI 3.1 spec, so it always matches the server.
Loading reference…
Realtime events
Subscribe to a workbook to receive every change as it happens, whether made by people in the app, your integrations or AI assistants. Use it to keep caches fresh, trigger workflows when a status cell changes, or mirror edits into another system.
const sub = wb.subscribe({
ready: ({ seq, role }) => console.log('listening from change', seq),
change: ({ seq, user, command }) => {
if (command.id === 'cell.set' && command.params.cell === 'F2') notifyTeam(`${user.name} changed the status`);
},
presence: (people) => console.log('viewing:', people.map((p) => p.name)),
reconnect: ({ missedChanges }) => { if (missedChanges) refreshFromApi(); },
error: (err) => console.error(err),
});
await sub.ready;
// later
sub.close();| Event | Payload | When |
|---|---|---|
ready | { seq, role, snapshot } | Connected; current document received. |
change | { seq, user: { id, name }, command: { id, params } } | Any edit, in server order. seq increases by one per change. |
saved | { seq, at } | Changes up to seq are saved. |
reload | { seq, by, snapshot } | The workbook was replaced wholesale (e.g. a version restore). |
presence | [{ id, name, color, role }] | Someone opened or closed the workbook. |
disconnect / reconnect | { code } / { seq, missedChanges, snapshot } | The connection dropped and was restored automatically with backoff. |
error / close | Error / { code, reason } | Unauthorized, not found, or closed. |
Protocol
Without the SDK, connect a WebSocket to wss://host/ws?id=workbookId&token=wst_…. Messages are JSON with a type of init, op, saved, reload, join, leave or error. Close codes 4001 (unauthorized) and 4004 (not found) are final; reconnect on any other close. A subscriber appears in other people's presence list as the token's user.
Command reference
Every command accepted by POST /workbooks/{id}/commands and by run() in the SDKs. Most take sheet (default: first/active) and a range in A1 notation. Loaded live from GET /api/commands.
Loading…
Function reference
Functions you can use in formulas written through the API. Loaded live from GET /api/functions.
MCP for AI assistants
Worksheets hosts a remote Model Context Protocol server at /mcp (Streamable HTTP). AI assistants can list, read, write, format and chart workbooks with the same access as the token's owner, and every edit appears live in the app.
claude mcp add --transport http worksheets /mcp \
--header "Authorization: Bearer wst_your_token"
{
"mcpServers": {
"worksheets": {
"type": "http",
"url": "/mcp",
"headers": { "Authorization": "Bearer wst_your_token" }
}
}
}
| Tool | What it does |
|---|---|
list_workbooks | Find workbooks by title. |
get_workbook | Sheets, used ranges, tables, charts and names. |
read_range | Values, displayed text or formulas (up to 20,000 cells). |
write_cells | Write values and formulas as if typed. |
run_commands | Any of the commands above, in a batch. |
list_commands, search_functions | Discover commands and functions. |
create_workbook, export_sheet, save_version | Create, export as CSV, checkpoint. |
The MCP endpoint currently uses access tokens. OAuth sign-in, needed for claude.ai and Claude Desktop custom connectors, is planned.
Changelog
1.0.0 · November 13, 2026
- JavaScript/TypeScript and Python SDKs.
- Personal access tokens; realtime WebSocket feed accepts tokens.
GET /workbooks/{id}/summary; error bodies includecodeandrequestId;X-Request-IdandRateLimit-*headers on every response.- OpenAPI 3.1 spec and MCP server.
New fields and endpoints may be added at any time, so ignore fields you don't recognise. Breaking changes are announced here in advance.
Support
Email support@beezifi.com and include the requestId from the error. Report security issues privately to the same address with “Security” in the subject. By using the API you agree to the Terms of Service and Privacy Policy.