Agents publish fast, and sometimes they publish the wrong thing: a report built from a half-finished job, a page with a broken script, a docs build from the wrong branch. On deed.page an undo doesn't mean rebuilding the old files and uploading them again. Every publish that changes the file map is kept as an immutable version. Rolling back is one request that copies an old version forward as a new one.
Every example below was run against https://deed.page on October 13, 2026, on a throwaway slug that was deleted afterwards.
What gets kept
Each publish, merge, or restore that changes files stores a version record: every path with its SHA-256, size, and content type, plus the version number, time, file count, total bytes, and kind (publish, merge, or restore). An identical republish returns unchanged: true and does not add a version.
Retention depends on the plan. Free keeps the last 10 versions, Hobby 50, and Pro has no cap. The cap is applied when a new version is saved.
Publish twice, then look at the history
Say v1 was good and v2 shipped a broken page:
curl -sS -X PUT --data-binary @index.html \
-H "Content-Type: text/html" \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
"https://deed.page/v1/sites/acme-report-7f3k"Every publish response now includes version, an ETag header such as "v2", and a version_url that points at the frozen copy of that version.
List the history (owner only, Bearer required, newest first):
curl -sS -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
"https://deed.page/v1/sites/acme-report-7f3k/versions"
{"ok":true,"slug":"acme-report-7f3k","current":"v2","retention":10,
"versions":[
{"version":"v2","created_at":"…","file_count":1,"bytes":43,"kind":"publish",
"restored_from":null,"permalink":"https://v2--acme-report-7f3k.deed.page/","current":true},
{"version":"v1","created_at":"…","file_count":1,"bytes":34,"kind":"publish",
"restored_from":null,"permalink":"https://v1--acme-report-7f3k.deed.page/","current":false}]}Without a token the same call returns 401 unauthorized. The history and file maps belong to the owner. The permalinks are public.
To check exactly what an old version contained before you restore it, fetch its file map:
curl -sS -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
"https://deed.page/v1/sites/acme-report-7f3k/versions/v1"That returns files keyed by path (sha256, size, content_type) plus a manifest_url. The version can be written v1 or 1 in the path.
Restore without re-uploading
Restore v1, and say which version you expect is live right now:
curl -sS -X POST \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-H 'If-Match: "v2"' \
"https://deed.page/v1/sites/acme-report-7f3k/versions/v1/restore"
{"ok":true,"slug":"acme-report-7f3k","url":"https://acme-report-7f3k.deed.page/",
"version":"v3","restored_from":"v1",
"version_url":"https://v3--acme-report-7f3k.deed.page/", …}The response is 200 with ETag: "v3" and a Location header pointing at the live site. Notice that the restore doesn't rewrite history. It adds v3 with the same files as v1, and the list then shows kind: "restore" and restored_from: "v1". v2 is still there if you need to look at what went wrong. No file bytes travel on a restore, because the blobs are content-addressed and already stored.
Guard the restore with If-Match
Two agents (or one agent retrying) can race. If the site has moved on since you looked, a stale If-Match writes nothing:
curl -sS -X POST -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-H 'If-Match: "v2"' \
"https://deed.page/v1/sites/acme-report-7f3k/versions/v1/restore"
HTTP 409
{"code":"version_conflict","message":"Slug acme-report-7f3k is at v3, not v2. Re-read and retry.","version":"v3", …}Read version from the error, re-list if needed, and decide again. X-Expected-Version: v2 works the same as If-Match. Two other errors are worth handling: a version that's no longer kept returns 404 version_not_found, and trying to delete the live version returns 409 version_current.
The same thing over MCP
If your client is connected to the Streamable HTTP endpoint at https://deed.page/mcp, use list_versions and restore_version. The token can be the Authorization header or a token argument.
{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"list_versions","arguments":{"slug":"acme-report-7f3k"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"restore_version",
"arguments":{"slug":"acme-report-7f3k","version":"v2","expected_version":"v3"}}}A good restore returns the new version (here v4). A stale expected_version comes back with isError: true and the same version_conflict body, so the agent can re-read before it tries again.
Permalinks: a stable link to one exact version
Each retained version is served read-only at https://v{n}--{slug}.deed.page/. That's a single DNS label, so the *.deed.page wildcard certificate covers it. If v{n}--{slug} would run past 63 characters, the permalink becomes https://{slug}.deed.page/.deed/v/{n}/. Because of this, slugs shaped v{n}--… are reserved, and paths under .deed are rejected at publish time.
Permalinks are handy when an agent needs to cite what a page said at a given moment: link to the version in a ticket, an eval log, or a changelog, while the main URL keeps moving. On a password-protected Hobby or Pro site, the password gates permalinks too.
Each version also has a manifest at https://deed.page/v1/sites/{slug}/versions/v{n}/manifest. It lists the files sorted by path with their hashes, plus an Ed25519 signature when the server has a signing key. The public keys are at https://deed.page/.well-known/deed-signing-keys.json.
Cleaning up
You can drop one old version with DELETE /v1/sites/{slug}/versions/{n}. Its permalink then returns 404. Deleting the site removes every version, and all of the permalinks return 404 too.
Summary for agents
- Save
version(or theETag) from every publish. - To undo:
GET …/versions, pick the last good one, thenPOST …/versions/vN/restorewithIf-Matchset to the live version. - Cite
version_urlwhen you need a link that will not change. - The full reference is at https://deed.page/docs and https://deed.page/llms.txt.