# Upload large sites in parts with X-Merge

By Avatar 8 (Avatar 8 LLC) · Published September 30, 2026 · https://deed.page/blog/merge-publish-large-sites

A single publish request to deed.page is capped at about 4 MB. That limit comes from the serverless platform that runs the API, not from your plan quota. Free still allows 25 MB per site; Hobby and Pro go higher. When a folder, asset pack, or generated report exceeds one request, you do not need a different host — you publish in parts with `X-Merge: 1`.

This post shows the exact REST and MCP shapes that work against production today.

## Why the body is capped, not the site

Plans set how large a finished site may be. The request body is a separate ceiling: roughly 4 MB per call. If you send more in one shot, the API returns `413` with code `request_too_large` and tells you to split the site and continue with `X-Merge: 1`.

Merge means: keep the files already on the slug, then add or overwrite only the paths in this request. A publish without merge replaces the whole site. That difference matters for agents that build assets over several steps or stream a directory that does not fit one PUT.

## Step 1 — publish the first chunk

Start with the files you need for a working URL — usually `index.html` plus anything the first paint requires. Pick a slug. On the first anonymous publish the response includes a `deed_tok_…` token once. Store it; later merges require it.

JSON map (handy when the agent already has strings in memory):

    curl -sS -X POST https://deed.page/v1/sites \
      -H 'content-type: application/json' \
      -d '{
        "slug": "my-page-<random>",
        "files": {
          "index.html": "<!doctype html><h1>gallery</h1><img src=\"/assets/hero.jpg\" alt=\"\">"
        }
      }'

Or a gzip tarball of a partial tree:

    tar -C ./site-part1 -czf /tmp/part1.tar.gz .
    curl -sS -T /tmp/part1.tar.gz \
      "https://deed.page/v1/sites?slug=my-page-<random>"

Typical response fields: `ok`, `url` (for example `https://my-page-<random>.deed.page/`), `path_url`, `version` (`v1`), `permanent`, `bytes`, `files`, and — only this once — `token`. Replace `<random>` before you send it. `gallery-demo` and other docs examples are reserved.

## Step 2 — merge the rest with the token

Send the remaining paths with `Authorization: Bearer` and `X-Merge: 1`. Paths you omit stay; paths you include are added or overwritten at those exact paths. A tarball directory is not peeled off, so pack with `tar -C` when those files should land at the site root. Each successful merge bumps `version`.

    tar -C ./site-part2 -czf /tmp/part2.tar.gz .
    curl -sS -T /tmp/part2.tar.gz \
      "https://deed.page/v1/sites?slug=my-page-<random>" \
      -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
      -H "X-Merge: 1"

JSON equivalent for a few large-ish text or base64 assets:

    curl -sS -X POST https://deed.page/v1/sites \
      -H 'content-type: application/json' \
      -H "Authorization: Bearer $DEEDPAGE_TOKEN" \
      -H "X-Merge: 1" \
      -d '{
        "slug": "my-page-<random>",
        "files": {
          "assets/hero.jpg": {"base64": "<base64 bytes>"},
          "assets/caption.txt": "hero caption"
        }
      }'

You can repeat merge publishes until the site is complete, as long as the total size stays under your plan's per-site limit (25 MB on Free, 100 MB on Hobby, 500 MB on Pro). Query param `merge=1` works the same as the header.

## MCP: merge=true on update_site

If your client speaks Streamable HTTP MCP at `https://deed.page/mcp`, use `publish_site` for the first chunk, then `update_site` with `merge: true`:

    {
      "name": "update_site",
      "arguments": {
        "slug": "my-page-<random>",
        "token": "deed_tok_…",
        "merge": true,
        "files": {
          "assets/hero.jpg": {"base64": "<base64 bytes>"}
        }
      }
    }

Same storage and ownership rules as REST. Prefer `update_site` when you already own the slug; `publish_site` with a token and `merge: true` is also accepted by the server.

## Patterns that work well

- **CI / cron:** Build step one uploads HTML and CSS; step two uploads hashed bundles under `/assets/` with merge. The public URL never changes.
- **SPA plus assets:** Set `X-Spa: 1` (or `spa: true` in MCP) on the first publish so client routes keep serving `index.html`, then merge the JS chunks.
- **Idempotent retries:** Send `Idempotency-Key` on each chunk if your harness may retry the same body after a timeout.
- **Do not merge secrets:** Every path on the slug is public. Merge only files you intend to serve.

## What merge does not do

- It does not raise the per-request cap. Each call still must stay under ~4 MB.
- It does not delete omitted paths. To remove a file, replace the site without merge (full file set) or delete the slug and republish.
- It does not move paths. A tarball entry `globex-example.com/index.html` updates that path only. It does not replace `/index.html`. Pack with `tar -C` when files should land at the site root.
- It does not bypass plan quotas. `whoami` (MCP) or your Bearer principal's plan still limits total sites and bytes.
- A stranger without your token still gets `409 slug_taken` with a `suggestion` — merge does not open the slug to the world.

## Related

- Docs (large sites section): [/docs](/docs)
- MCP walkthrough: [/blog/mcp-publish-walkthrough](/blog/mcp-publish-walkthrough)
- Why permanent anonymous publish exists: [/blog/publish-without-a-human](/blog/publish-without-a-human)
- OpenAPI: [/openapi.json](/openapi.json)
- Skill for agents: [/skill.md](/skill.md)

Questions or corrections: ops@avatar33.com.
