<!-- Host it yourself · Docs · clayjs · https://clayjs.com/docs/endpoint · generated from the page; every page has one at its path plus .md -->

[Docs](https://clayjs.com/docs) · Host it yourself

# The endpoint spec

The one route a host implements, and a complete server in a couple dozen lines of Express.

A clayjs server implements one route:

| Part | Value |
| --- | --- |
| Route | `POST /_/save` |
| Body | The file's full HTML, as plain text. Always: this route has exactly one body shape, on every host. |
| Header | `Document-URL`: the full URL of the page being saved. |
| Success | `200` with JSON `{ "msg": "Saved" }` |
| Failure | Any non-2xx. The status is authoritative; host-generated failures use JSON `{ "msg": "why" }`, but clients tolerate non-JSON intermediary errors. |

A complete server in a couple dozen lines of Express:

```javascript
import express from "express";
import fs from "node:fs/promises";
import path from "node:path";

const root = process.cwd();
const app = express();
app.use(express.static(root));
app.use(express.text({ type: "*/*", limit: "10mb" }));

app.post("/_/save", async (req, res) => {
  try {
    const origin = req.get("Origin");
    if (origin && new URL(origin).host !== req.get("Host"))
      return res.status(403).json({ msg: "Cross-origin save refused", code: "forbidden" });
    const page = decodeURIComponent(new URL(req.get("Document-URL")).pathname);
    const target = path.join(root, path.normalize(page.endsWith("/") ? page + "index.html" : page));
    if (!target.startsWith(root + path.sep))
      return res.status(403).json({ msg: "Outside the folder", code: "forbidden" });
    if (!/^\s*<!doctype html>/i.test(req.body))
      return res.status(422).json({ msg: "Not a complete HTML document", code: "invalid-document" });
    await fs.writeFile(target, req.body);
    res.json({ msg: "Saved" });
  } catch {
    res.status(500).json({ msg: "Couldn't save" });
  }
});

app.listen(4600);
```

The server half is only half of it: on your own host nothing arms [edit mode](https://clayjs.com/docs/edit-mode) for you, so a page served this way is read-only until you set `window.clayEditMode` before the script tag. A real deployment adds authorization and atomic writes. The same contract powers hyperclay.com, Hyperclay Local, and HTML Clay. Two details you may ignore: every save also carries a `Save-Trigger: user|auto` header (whether a person triggered it), and HTML Clay uses a token variant, `POST /_/save/{token}`, read from `<html savetoken>`.
