FORMAT: 1A HOST: https://www.ethercalc.org/ # EtherCalc API for real-time collaborative spreadsheets. * Overview: http://ethercalc.net/ * 中文版: http://ethercalc.tw/ Note: When using `curl`, please make sure to `--data-binary` instead of `--data`. For example: ```bash curl -X PUT -H 'Content-Type: text/x-socialcalc' \ --data-binary @example.sc http://127.0.0.1:8000/_/example ``` # Index [/_] ## Create Page [POST] Takes a JSON structure with `room` and `snapshot` fields. Replaces the page with a serialization in Socialtext save format. If `room` is not specified, returns a new page. + Request (application/json) ```json { "room": "test" , "snapshot": "..." } ``` + Response 201 + Headers ``` Location: /_/test ``` ## Create from CSV [POST] Takes a CSV structure that contains the new spreadsheet's content. + Request (text/csv) + Response 201 ## Create from SocialCalc [POST] Takes a SocialCalc serialization format that contains the new spreadsheet's content. + Request (text/x-socialcalc) + Response 201 ## Create from Excel XML [POST] Takes a Excel XML file that contains the new spreadsheet's content. + Request (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) + Response 201 # Page [/_/{id}] ## Page Content [GET] Fetch the page as a serialization in SocialCalc save format. + Response 200 (text/x-socialcalc) ## Overwrite with CSV [PUT] Replace the page with a serialization in CSV format. + Request (text/csv) + Response 200 ## Overwrite with SocialCalc [PUT] Replace the page with a serialization in SocialCalc save format. + Request (text/x-socialcalc) + Response 200 ## Overwrite with Excel XML [PUT] Replace the page with a serialization in Excel XML format. + Request (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) + Response 200 ## Post Commands [POST] Takes a JSON structure with a `command` field (either as a string or an array of strings), or a plain-text command string. Runs one or more commands specified in the `command` field. To find out which command corresponds to which spreadsheet actions, perform the actions on the web interface and check the _Audit_ tab for the recorded commands. Command batches that would push the sheet's declared area past the 200,000-cell limit are rejected with `413`. The same limit applies over WebSocket, where the connection closes with code `1008` and reason `Command exceeds sheet limits`. + Request (application/json) + Response 202 ```json {"command": "..."} ``` + Response 413 (text/plain) ```text command exceeds sheet limits ``` ## Append Rows [POST] Takes a CSV structure that contains fields to be appended to the first column after the last row. + Parameters + row (optional, integer) ... If specified, insert and paste on the specified row instead of the last. + Request (text/csv) + Response 200 ## Delete Room [DELETE] Deletes a room from the database + Response 201 OK # Point-in-Time Restore [/_/{id}/pitr-restore] ## Restore a hosted room [POST] Restores the complete SQLite-backed Durable Object for one room to a Cloudflare PITR bookmark from approximately the previous 30 days. This recovers the snapshot, commands, audit/chat state, cell metadata, and alarms together. Local Miniflare and standalone workerd do not retain PITR history and return `501`. The route requires the deployment operator token: ```text Authorization: Bearer ``` If `ETHERCALC_MIGRATE_TOKEN` is unset, the route is hidden with `404`. A missing or incorrect bearer returns `401`. Supply exactly one target. `at` accepts a positive millisecond epoch or an ISO-8601 timestamp. `dryRun` resolves and returns the target bookmark without scheduling a restore: + Request (application/json) ```json { "at": "2026-07-10T00:00:00.000Z", "dryRun": true } ``` + Response 200 (application/json) ```json { "dryRun": true, "bookmark": "0000007b-..." } ``` Apply either the resolved bookmark or a previously returned undo bookmark: + Request (application/json) ```json { "bookmark": "0000007b-..." } ``` + Response 200 (application/json) ```json { "restored": true, "bookmark": "0000007b-...", "undoBookmark": "0000009d-...", "exists": true, "updatedAt": 1783641600000 } ``` `exists` is `false` when the restored point predates room creation; `updatedAt` is then omitted. To undo a restore, submit the returned `undoBookmark` as `bookmark`. A freshly created room has no PITR history until Cloudflare's change log catches up (about a minute in practice). Until then, timestamp dry-runs return `400 PITR target is unavailable`; poll the dry-run before scheduling a real restore. Invalid requests or unavailable/expired targets return `400`. An unsupported deployment returns `501`. A dispatch failure before the restore is accepted returns `502` as plain text — nothing was scheduled. Once the restore is accepted, the rewind is already armed, so later failures return JSON that keeps the reverse handle: + Response 500 (application/json) ```json { "accepted": true, "bookmark": "0000007b-...", "undoBookmark": "0000009d-...", "error": "PITR restore did not restart the room" } ``` Finalization failures after a confirmed restart use the same shape with status `502` and `"error": "PITR restore finalization failed"`. In both cases retain `undoBookmark`: the restore may have applied (or still apply), and submitting `undoBookmark` reverses it. # Page Cells [/_/{id}/cells] ## GET Returns a JSON representation of all defined cells in the page. + Response 200 (application/json) # Cell Value [/_/{id}/cells/{coord}] ## GET Returns a JSON representation of a single cell in the page. + Response 200 (application/json) # HTML Export [/{id}.html] ## GET Returns a HTML rendering of the page. (GET `/_/{id}/html` also works.) + Response 200 (text/html) # CSV Export [/{id}.csv] ## GET Returns a CSV rendering of the page. (GET `/_/{id}/csv` also works.) + Response 200 (text/csv) # JSON Export [/{id}.csv.json] ## GET Returns a JSON array-of-array rendering of the page. (GET `/_/{id}/csv.json` also works.) + Response 200 (application/json) # Excel XML Export [/{id}.xlsx] ## GET Returns a Excel XML rendering of the page. (GET `/_/{id}/xlsx` also works.) + Response 200 (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) # Markdown Export [/{id}.md] ## GET Returns a Excel XML rendering of the page. (GET `/_/{id}/md` also works.) + Response 200 (text/x-markdown) # Multi-sheet Excel XML [/={id}.xlsx] ## Export [GET] Fetch the sheet collection in Excel XML format. (GET `/_/{id}/xlsx` also works.) + Response 200 (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) ## Import [PUT] Replace the page with a serialization in Excel XML format. + Request (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) + Response 200 Private parent rooms are supported. Each imported sub-sheet is initialized as a workbook child with immutable parent delegation; it does not receive an independent ACL. Reads, writes, exports, formulas, and WebSocket mutations are authorized against the parent room on every request. A pre-existing unmarked occupied room is never auto-adopted as a child. On 2026-08-11 a read-only D1 `rooms` index query plus anonymous probes of gate-exempt `GET /_//access` counted 1,883,069 rooms, 68,582 child-shaped names (`.`), and 13,875 distinct candidate parents — 13,873 public, 2 not-found, 0 private. Pre-fix children had no access metadata, so `#mirrorIndex` treated them as public and wrote them into the index; post-fix parented children are never indexed. No remediation is required in hosted production: the fix is prospective and closed the hole before anyone was exposed by it. The invokability facts remain true and explain why a sweep was never needed (and would still block anyone who later wants one): `/_do/backfill-children` and `/_do/set-parent` exist only on the internal Durable Object dispatch surface (`packages/worker/src/room.ts:371,374`) with no Worker route proxying them; `ETHERCALC_MIGRATE_TOKEN` is unset in production so operator-gated paths return 404 (`packages/worker/src/lib/migrate-auth.ts:40`); and `#postBackfillChildren` requires `local.acl.owner === uid` (`packages/worker/src/room.ts:916-928`), so an operator cannot remediate rooms they do not own. A hosted sweep would still require shipping a new Worker route, provisioning the token secret, and deciding whether to keep owner-only auth or permit operator overrides. The D1 count is a lower bound — a child whose mirror write failed, or any self-hosted deployment, is not represented — but zero is plausible rather than lucky: exposure required a private room (passkeys reached production only around `d2afa90`, 2026-07-18) and multi-sheet use and a child that received content. New children are safe on creation, and pristine pre-fix children are adopted on the next TOC write (`packages/worker/src/routes/multi-import.ts:415`). Multi-sheet imports use idempotent parent-side reservations and monotonic child indices. A retry with the same request identifier reuses its reservation; abandoned reservations expire without allowing a later import to reuse any possibly orphaned child name. # Rooms [/_rooms] ## Index of rooms [GET] Get index of rooms. Will fail with 403 if CORS is enabled. + Response 200 # Page [/_exists/{id}] ## Page Exists [GET] Check if page exists + Response 200 (application/json) # Passkey Auth [/_auth] Available when the deployment sets `ETHERCALC_AUTH` plus the WebAuthn trust anchors (`ETHERCALC_RP_ID`, `ETHERCALC_ORIGIN`); otherwise every ceremony route responds 404. Sessions use the HttpOnly, host-only `__Host-ec_sess` cookie — tokens never appear in response bodies. ## Register Init [POST /_auth/register-init] Begin creating a passkey. Returns WebAuthn creation options plus the server-generated user id. + Response 200 (application/json) ## Register Complete [POST /_auth/register-complete] Takes `{response, uid, challenge}` from the browser ceremony. On success sets the `__Host-ec_sess` cookie and returns `{uid}`. + Response 200 (application/json) ## Login Init [POST /_auth/login-init] Begin a usernameless (discoverable-credential) login. + Response 200 (application/json) ## Login Complete [POST /_auth/login-complete] Takes `{response, challenge}`. On success sets the `__Host-ec_sess` cookie and returns `{uid}`. + Response 200 (application/json) ## Who Am I [GET /_auth/whoami] Returns `{uid, enabled}` — `uid` is null for anonymous visitors and `enabled` reports whether passkey auth is configured at all. + Response 200 (application/json) ## Logout [POST /_auth/logout] Clears the session cookie. + Response 204 # Private Page [/_/private] ## Create Private Page [POST] Requires a passkey session. Creates a fresh room readable and writable only by the owner, and returns `{room}` with a Location header. Private rooms never appear in `/_rooms` listings, and every read, write, export, and WebSocket path answers 403 for non-members. + Response 201 (application/json) + Response 401 — no passkey session # Private Copy [/_from/{id}/private] ## Copy to Private [POST] Requires a passkey session. Copies a readable page into a fresh private room owned by the caller and redirects to its edit view. + Response 302 + Response 401 — no passkey session + Response 403 — source page not readable by the caller