FORMAT: 1A HOST: https://www.ethercalc.org/
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:
curl -X PUT -H 'Content-Type: text/x-socialcalc' \
--data-binary @example.sc http://127.0.0.1:8000/_/exampleTakes 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)
{ "room": "test" , "snapshot": "..." } -
Response 201
-
Headers
Location: /_/test
-
Takes a CSV structure that contains the new spreadsheet's content.
- Request (text/csv)
- Response 201
Takes a SocialCalc serialization format that contains the new spreadsheet's content.
- Request (text/x-socialcalc)
- Response 201
Takes a Excel XML file that contains the new spreadsheet's content.
- Request (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
- Response 201
Fetch the page as a serialization in SocialCalc save format.
- Response 200 (text/x-socialcalc)
Replace the page with a serialization in CSV format.
- Request (text/csv)
- Response 200
Replace the page with a serialization in SocialCalc save format.
- Request (text/x-socialcalc)
- Response 200
Replace the page with a serialization in Excel XML format.
- Request (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
- Response 200
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
{"command": "..."} -
Response 413 (text/plain)
command exceeds sheet limits
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
Deletes a room from the database
- Response 201 OK
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:
Authorization: Bearer <ETHERCALC_MIGRATE_TOKEN>
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)
{ "at": "2026-07-10T00:00:00.000Z", "dryRun": true } -
Response 200 (application/json)
{ "dryRun": true, "bookmark": "0000007b-..." }
Apply either the resolved bookmark or a previously returned undo bookmark:
-
Request (application/json)
{ "bookmark": "0000007b-..." } -
Response 200 (application/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)
{ "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.
Returns a JSON representation of all defined cells in the page.
- Response 200 (application/json)
Returns a JSON representation of a single cell in the page.
- Response 200 (application/json)
Returns a HTML rendering of the page. (GET /_/{id}/html also works.)
- Response 200 (text/html)
Returns a CSV rendering of the page. (GET /_/{id}/csv also works.)
- Response 200 (text/csv)
Returns a JSON array-of-array rendering of the page. (GET /_/{id}/csv.json also works.)
- Response 200 (application/json)
Returns a Excel XML rendering of the page. (GET /_/{id}/xlsx also works.)
- Response 200 (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
Returns a Excel XML rendering of the page. (GET /_/{id}/md also works.)
- Response 200 (text/x-markdown)
Fetch the sheet collection in Excel XML format. (GET /_/{id}/xlsx also works.)
- Response 200 (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
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 /_/<parent>/access
counted 1,883,069 rooms, 68,582 child-shaped names (<base>.<positive decimal>),
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.
Get index of rooms. Will fail with 403 if CORS is enabled.
- Response 200
Check if page exists
- Response 200 (application/json)
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.
Begin creating a passkey. Returns WebAuthn creation options plus the server-generated user id.
- Response 200 (application/json)
Takes {response, uid, challenge} from the browser ceremony. On
success sets the __Host-ec_sess cookie and returns {uid}.
- Response 200 (application/json)
Begin a usernameless (discoverable-credential) login.
- Response 200 (application/json)
Takes {response, challenge}. On success sets the __Host-ec_sess cookie and
returns {uid}.
- Response 200 (application/json)
Returns {uid, enabled} — uid is null for anonymous visitors and
enabled reports whether passkey auth is configured at all.
- Response 200 (application/json)
Clears the session cookie.
- Response 204
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
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