deed.page has one publish endpoint, PUT or POST to https://deed.page/v1/sites, and it accepts four kinds of body. They all produce the same result: a site at the slug you chose, with the files stored at exactly the paths you sent. Which one to use depends on where your files are right now. Maybe they're on disk, maybe they're strings in the model's context, maybe they're form fields in a tool that only speaks multipart.
Every example below was run against production on October 6, 2026, with X-Ttl: 900 so the test sites expired on their own, and they were deleted afterwards anyway. Drop the TTL header when you want a permanent site, which is the default.
1. A single HTML document
Use this when the whole page is one file. If the body starts with <!doctype html or <html, or the content type is text/html, deed.page stores it as index.html:
curl -sS -T index.html "https://deed.page/v1/sites/acme-status-note"That's the entire publish. On the first anonymous call the 201 response includes a deed_tok_… token once. Keep it (for example in DEEDPAGE_TOKEN) and send it as Authorization: Bearer on every later request.
The limit is that you get exactly one file. A stylesheet linked as /css/style.css will return 404 until you publish it some other way.
2. A gzip tarball (best when files are on disk)
If a build step wrote a folder, pack it and upload the archive:
tar -C ./site -czf /tmp/site.tar.gz .
curl -sS -T /tmp/site.tar.gz \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
"https://deed.page/v1/sites?slug=acme-weekly-report"The -C ./site … . part matters. deed.page keeps archive paths exactly as packed and does not strip a single top-level directory. If you run tar -czf site.tar.gz site instead, the page ends up at /site/ and the root returns 404. We checked both forms: with -C the root and /css/style.css served 200, and without it only /site/ did.
The server detects gzip from its magic bytes and also accepts plain .tar. It rejects symlinks, hard links, device entries, absolute paths, and .. segments with 400 invalid_archive. A tarball is the most compact choice for many files or binary assets, because nothing has to be base64-encoded.
3. A JSON file map (best when files are strings in memory)
Agents often hold the page as text they just generated. Writing it to disk only so you can tar it is wasted work, so send a map instead:
curl -sS -X POST https://deed.page/v1/sites \
-H 'content-type: application/json' \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-d '{
"slug": "acme-weekly-report",
"files": {
"index.html": "<!doctype html><link rel=stylesheet href=/css/style.css><h1>Report</h1>",
"css/style.css": "h1{color:teal}",
"favicon.ico": {"base64": "AAABAAEAAQEAAAEAGAAwAAAAFgAAACgAAAABAAAAAgAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAA"}
}
}'A string value is stored as UTF-8 text. For binary data use {"base64": "…"}, and {"content": "…"} works as an explicit text form. The slug can go in the body, in the URL path, in ?slug=, or in an X-Slug header. Base64 adds about a third to the size, so keep that in mind against the roughly 4 MB per-request cap.
The MCP server takes this same shape. A tools/call to https://deed.page/mcp looks like this:
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "publish_site",
"arguments": {
"slug": "acme-weekly-report",
"files": {
"index.html": "<!doctype html><h1>Report</h1>",
"css/style.css": "h1{color:teal}"
}
}
}
}You can pass the token as a Bearer header from the MCP client or as a token argument. Add ttl_seconds only if you want the site to expire.
4. Multipart form data (best for tools that already speak forms)
Some HTTP clients and no-code tools can only send multipart/form-data. deed.page takes the site path from the part's filename when it contains a directory. Otherwise it uses the field name if that contains a directory, and otherwise the filename:
curl -sS -X PUT \
-H "Authorization: Bearer $DEEDPAGE_TOKEN" \
-F "file=@site/index.html;filename=index.html" \
-F "file=@site/css/style.css;filename=css/style.css" \
"https://deed.page/v1/sites/acme-weekly-report"Naming the field after the path, as in -F "css/style.css=@site/css/style.css", works too. Generic field names such as file, files, upload, and site are never treated as paths. If you send a single part that is a tarball (-F "site=@site.tar.gz"), it is unpacked just like format 2.
Quick decision table
- One self-contained page: raw HTML.
- A build folder or binary assets: gzip tarball packed with
tar -C. - Text the agent just generated: JSON map, or MCP
publish_site. - A client that can only send forms: multipart.
Rules that apply to every format
- Same response. Each format returns
url,path_url,slug,version,permanent,expires_at,bytes, andfiles. - Same caps. A request is limited to about 4 MB. Above that, the API answers
413 request_too_large, and you send the remaining files withX-Merge: 1(see /blog/merge-publish-large-sites). - Same ownership. Without the token that owns the slug, a publish gets
409 slug_takenalong with asuggestion. - Empty bodies fail loudly. A plain-text body that is neither HTML, JSON, nor an archive returns
400 empty_site. Nothing gets published by accident. - Replacement by default. A publish without merge replaces the whole file set. Formats can be mixed: publish a tarball first, then merge in a JSON map.
Related
- Docs: /docs
- MCP walkthrough: /blog/mcp-publish-walkthrough
- Large sites with merge: /blog/merge-publish-large-sites
- OpenAPI: /openapi.json
- Agent skill: /skill.md
Questions or corrections: ops@avatar33.com.