# Data API

> Read and write HTML and HTML Clay documents as structured JSON using CSS selector rules, with ETag and page revision protection.

Source: https://hyperclaylocal.com/docs/data-api/

Every document served by Hyperclay Local can be read and updated as structured JSON using CSS selector rules, either through a rules tag embedded in the document or through a `data` query parameter supplied by the caller. Every read and write carries an `ETag` (and, for page documents, page revision stamps) so stale writes are refused instead of overwriting newer edits.

For the browser plugin and hosted equivalents, see ClayJS [data: pages as JSON](https://clayjs.com/docs/plugins/data/), Hyperclay [Page data endpoints](https://hyperclay.com/docs/page-data/), and HTML Clay [JSON over HTTP](https://htmlclay.com/docs/json-http/).

## Read JSON with an embedded rules tag

Embed a JSON rules tag inside your `.html` or `.htmlclay` document:

```html
<script type="application/json" data-rules-name="api" data-rules-version="1">
{ "title": "h1", "items": ".item[]" }
</script>
```

Then read the extracted JSON from `/_/api/<file>`:

```bash
curl http://localhost:4321/_/api/index.html
# {"title":"Notes","items":["a","b"]}

curl http://localhost:4321/_/api            # same as /_/api/index.html
curl http://localhost:4321/_/api/blog/post.html
```

- **`data-rules-name`**: Parsed as a space-separated token list, so `data-rules-name="api cms collection"` matches the `api` token.
- **`data-rules-version`**: Must be `"1"`; any other value returns `400` with `{"error":"Unsupported rules version",...}`.
- **Missing tag**: Requesting `/_/api/<file>` without a `data` query parameter when the document has no matching `api` rules tag returns `400` with `{"error":"No api rules tag",...}`.

## Read or write with caller-supplied rules (`?data=`)

You can supply CSS selector rules directly on the request query string without adding a `<script>` tag to the document. Caller-supplied rules work on both the document's own URL and `/_/api/<file>`, for both `GET` and `POST`, on `.html` and `.htmlclay` files:

```bash
# Read with relaxed-JSON rules on the document URL
curl 'http://localhost:4321/index.html?data={title:"h1",xs:".item[]"}'
# {"title":"Notes","xs":["a","b"]}

# URL-encoded GET on a .htmlclay document
curl --get 'http://localhost:4321/soup.htmlclay' \
  --data-urlencode 'data={title:h1}'

# POST with caller-supplied rules
curl --request POST \
  'http://localhost:4321/soup.htmlclay?data=%7Btitle%3Ah1%7D' \
  --header 'Content-Type: application/json' \
  --data '{"title":"Lunch"}'
```

Replace `4321` with your folder's port if you are querying a [team folder](https://hyperclaylocal.com/docs/guide/team-folders/). The same `?data=` query works on `/_/api/soup.htmlclay?data=%7Btitle%3Ah1%7D`.

- On a document's own URL, `?data=` intercepts a `GET` or `POST` before the static file is served. Without `?data=`, a `GET` to `/index.html` returns the raw HTML document.
- Supplied rules replace the document's embedded `api` mapping for that single request, even if the embedded tag is malformed. They are never merged with the embedded tag and never saved into the file.
- An empty, malformed, or repeated `data` parameter returns `400` and never falls back to the embedded tag.

## Caching

- **Sidecar cache**: Results extracted using an embedded `api` tag are cached in `.hyperclay/api/` inside the served folder, rewritten on every save, and regenerated on a `GET` when the cache file is older than the source document. A freshly generated response includes the header `X-Served-By: app-generated`; a cache hit does not.
- **Per-filename isolation**: Each complete source filename keeps its own JSON cache, so `soup.html` and `soup.htmlclay` answer independently.
- **Caller rules skip cache**: Any request carrying a `?data=` parameter bypasses the embedded mapping's cache.
- **Legacy Tailwind URLs**: Existing legacy [Tailwind CSS](https://hyperclaylocal.com/docs/tailwind/) URLs still share a basename: cold generation prefers `.html`, then falls back to `.htmlclay` if the `.html` file is missing.

## Write JSON into a document (`POST`)

Send a `POST` request with `Content-Type: application/json` to `/_/api/<file>` (or `/_/api` to target `index.html`, or `/<file>?data=…` with caller-supplied rules) to update the document's DOM in place:

```bash
curl -X POST http://localhost:4321/_/api/index.html \
  -H 'Content-Type: application/json' \
  -d '{"title":"Groceries","items":["eggs","milk"]}'
# {"title":"Groceries","items":["eggs","milk"]}
```

`POST` bodies must be strict JSON with no rules envelope, using the same key structure that a `GET` with those rules returns. The response returns the fresh extraction under the same rules.

### Content-only safety and validation rules

- **Content only**: Text values are written as text and never parsed as HTML. Attributes that could execute script or alter page behavior are refused: `on*` event handlers, `style`, `srcdoc`, save and region markers (`no-save`, `no-data`, `contenteditable`, and related attributes), and `javascript:`, `vbscript:`, and `data:` URLs. Nothing inside `<script>`, `<style>`, `<template>`, `<iframe>`, `<object>`, `<noscript>`, or `<head>` metadata tags can be written, and neither can `@innerHTML` or `@outerHTML`. A refused target returns `400` with nothing written:
  ```json
  {"error":"Write refused","details":[{"target":"<a#home>","reason":"..."}]}
  ```
- **Unknown keys and unmatched selectors**: Keys not defined by the active rules, or rules whose CSS selector matches no element in the document, return `400` without writing anything:
  ```json
  {"error":"Write rejected","details":{"unknownKeys":[...],"unmatched":[...]}}
  ```
- **Partial writes and clearing values**: Any key you omit from the JSON body is left untouched. Passing `null` clears a text value.
- **Lists**: An array rule (`[]`) grows a list by cloning its first item, or by cloning its `cms-template` seed element when the list is currently empty.
- **Hidden regions**: Elements marked `no-data` or `editor-ui` are invisible to both reads and writes.
- **Minimal byte changes and UTF-8 check**: Only the targeted elements are rewritten; every other byte of the file stays as it was. Documents that are not valid UTF-8 are refused rather than corrupting bytes outside the edit. A `POST` body that changes nothing writes nothing to disk.
- **Body size and content type**: Request bodies must carry `Content-Type: application/json` (`415` otherwise) and be at most `1` MB (returning a JSON error when over the limit).
- **Origin rules**: Browser requests must come from the same origin; command-line and script requests do not need a browser token.
- **Save pipeline and live updates**: Every accepted write runs through the folder's write gate the same way as a browser save: it writes a backup to `.hyperclay/versions/`, updates the file atomically, refreshes the `.hyperclay/api/` cache, and broadcasts the change to open browser tabs as an external edit (protecting any region where someone is currently typing).

## Refuse stale writes (`If-Match` and page revisions)

Every `GET` and `POST` on the Data API reads the source file in a single pass and returns an `ETag` header that describes the exact bytes used for the extraction.

### File `ETag` and `If-Match`

Pass the `ETag` back in the `If-Match` header on your `POST` request to prevent overwriting concurrent edits:

```bash
curl -X POST http://localhost:4321/_/api/index.html \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "9f86d081884c7d65"' \
  -d '{"title":"Groceries","items":["eggs","milk"]}'
```

If the document changed on disk since you read it, Hyperclay Local refuses the stale write with `412 Precondition Failed` and leaves the file untouched.

### Page documents and page revisions

The Data API also reads and writes page documents whose editor merge state lives in `.hyperclay/page-state`:

- A write to a page document names both the file revision and the page revision.
- If the page revision is stale, the write is refused with the file on disk, the open live room, and `.hyperclay/versions/` backups left unchanged.
- When a Data API write based on an older file version can be merged into the stored page state, open windows merge the update instead of stopping.
