# a4list API

The API represents each sheet and its configuration as one JSON document.
Content is edited with an atomic, whole-document `POST`; creation, duplication,
and metadata remain separate operations.

Machine-readable contract: [`/openapi.json`](/openapi.json). The a4list plain
text representation and its exact editor mappings are documented in
[`/plain_text_format.md`](/plain_text_format.md).

## Capabilities

Every sheet is created with two independent capability URLs:

```text
/sheet/re_<read_secret>
/sheet/wr_<write_secret>
```

New secrets contain 80 cryptographically random bits encoded as 20 lowercase
hexadecimal characters. Existing 128-bit, 32-character secrets remain valid.

A capability beginning with `re_` can read and duplicate. It cannot edit or
discover the source write capability. A capability beginning with `wr_` can
also replace the document. Do not list, log, or publish write URLs unless
editable access is intended.

## Create

```text
POST /api/sheet
```

No request body is required. To create a localized default sheet with an initial
title, send `{"title":"..."}` as UTF-8 JSON and optionally set
`X-A4List-Locale`. To create with complete content, send either a complete
document with `Content-Type: application/json` or [a4list plain text](/plain_text_format.md)
with `Content-Type: text/plain`. The `201` response returns both capabilities for
the newly created sheet:

```json
{
  "read_url": "/sheet/re_0123456789abcdef0123",
  "write_url": "/sheet/wr_fedcba9876543210fedc",
  "revision": 0
}
```

The legacy `X-A4List-Title` header is accepted for an empty request, but JSON is
preferred because HTTP headers cannot reliably carry Unicode titles.

```sh
curl -X POST "$a4list_origin/api/sheet"

curl -X POST "$a4list_origin/api/sheet" \
  -H 'Content-Type: text/plain' \
  --data-binary @sheet.txt
```

Production creation and duplication are rate limited. A request over the limit
returns `429` with a `Retry-After` header. Localhost and loopback development
requests bypass this tracking and limit.

Successful production creates, duplicates, document writes, WebSocket saves,
and recovery-email updates retain internal IP, network, and client metadata for
abuse investigation. This audit data is not returned by any capability API.

## Read the complete document

```text
GET /api/sheet/{re_or_wr_capability}
```

A read-capability response deliberately omits all write-key fields:

```json
{
  "access": "read",
  "revision": 18,
  "document": {}
}
```

A write-capability response additionally supplies the shareable read URL:

```json
{
  "access": "write",
  "read_url": "/sheet/re_0123456789abcdef0123",
  "recovery_email": "",
  "revision": 18,
  "document": {}
}
```

The empty `document` objects above abbreviate the structure below.

```sh
curl "$a4list_origin/api/sheet/$read_capability"
```

## Read a4list plain text

```text
GET /api/sheet/{re_or_wr_capability}/txt
```

This returns `text/plain` without an API envelope. The same representation is
available from the main `GET` endpoint with `Accept: text/plain`.
Recovery email and write capability data are never included.

```sh
curl "$a4list_origin/api/sheet/$read_capability/txt"
```

## Replace the complete document

```text
POST /api/sheet/{wr_capability}
Content-Type: application/json or text/plain
If-Match: "revision-18"
```

The body may be either the complete JSON document itself—not a command or patch
envelope—or an a4list plain text document sent as `text/plain`. Plain-text
input preserves the current sheet title unless its first line is `# Sheet title`.
QR/URL settings are preserved while panel rows are replaced. The following
structural example is abbreviated and is not directly postable:

```json
{
  "schema_version": 2,
  "title": "Next two weeks",
  "paper": {
    "key": "a4",
    "label": "A4 / four A6 panels",
    "width_mm": 210,
    "height_mm": 297,
    "panel_width_mm": 105,
    "panel_height_mm": 148.5,
    "grid_mm": 5
  },
  "print": {
    "show_qr": true,
    "show_sheet_url": true,
    "qr_links_to_read_only": false,
    "checklist_box_count": 14
  },
  "panels": {
    "pn_0123456789abcdef": {
      "position": 0,
      "is_print_enabled": true,
      "mirror_source_panel_id": null,
      "rows": {
        "rw_0123456789abcdef": {
          "sort_key": "0010",
          "row_kind": "checklist",
          "has_line": false,
          "label": "Drink water"
        }
      }
    }
  }
}
```

Real documents contain exactly four uniquely positioned panels and 28 rows per
panel. Panel and row objects are addressed by stable IDs rather than array
indexes. `row_kind` is `gap` or `checklist`. `print.checklist_box_count` is `7`
for a one-week row or `14` for a two-week row and applies to every checklist
on the sheet. Fetch the canonical document,
modify it while preserving its IDs, then post the complete result.

Success returns a compact acknowledgement and an updated `ETag`:

```json
{ "revision": 19 }
```

A stale `If-Match` returns `412` and does not overwrite newer work:

```json
{ "error": "revision_conflict", "revision": 20 }
```

The caller must fetch revision 20, intentionally reapply or merge its changes,
and submit again. Posting with a read capability returns `403`.

```sh
curl -X POST "$a4list_origin/api/sheet/$write_capability" \
  -H 'Content-Type: application/json' \
  -H 'If-Match: "revision-18"' \
  --data-binary @document.json
```

## Clickable creation links

When an LLM chat or another environment cannot make HTTP `POST` requests, it
can construct a link for the user to click:

```text
https://a4list.enzom.dev/#create_from_json=<base64-json-document>
https://a4list.enzom.dev/#create_from_txt=<base64-plain-text>
```

Encode the UTF-8 bytes using standard or URL-safe base64. The client reads the
fragment, posts the decoded body to the creation API, and redirects to the new
write URL. The JSON value is a complete document, not a read/write response
envelope. Keep these links compact because browsers and chat clients impose
varying URL-length limits.

## Generate an A4 PDF

```text
GET /api/sheet/{re_or_wr_capability}/pdf?revision=18&disposition=inline
```

The PDF is pinned to the current committed revision. `inline` opens the PDF in
the browser; `attachment` downloads it. PDF requests and uncached browser
renders are subject to rate and resource limits.

```sh
curl "$a4list_origin/api/sheet/$read_capability/pdf?revision=18&disposition=attachment" --output sheet.pdf
```

## Duplicate

```text
POST /api/sheet/{re_or_wr_capability}/duplicate
```

The latest committed document is copied into an independent sheet. Both read
and write source capabilities are accepted. The response contains the new
target's read and write URLs but never the source's write capability.

An optional JSON body can set the duplicate's title. Without it, the server
appends `COPY` to the source title.

```json
{ "title": "My sheet COPY 2" }
```

```sh
curl -X POST "$a4list_origin/api/sheet/$read_capability/duplicate"
```

## Metadata

```text
GET /api/sheet/{re_or_wr_capability}/meta
```

This validates a capability without returning the document:

```json
{
  "access": "read",
  "revision": 19,
  "is_locked": true
}
```

A write-authorized metadata response may also contain `read_url`. It never
returns a write key.

```sh
curl "$a4list_origin/api/sheet/$read_capability/meta"
```

## Errors and limits

Errors have an `error` string. Documents are strictly parsed and normalized.
Oversized request bodies return `413`, and body streams are stopped once their
limit is exceeded. There is deliberately no API for listing or
searching capabilities, and `GET /api/sheet` returns `404`.
