# LeafPage API

LeafPage hosts immutable HTML reports. An upload is stored as a fixed snapshot and served from a cookieless content host. Full reference: https://docs.leafpage.cc · Machine-readable spec: https://leafpage.cc/openapi.json

> **AI agents**: a ready-to-install Agent Skill for this API lives at https://docs.leafpage.cc/skill (manifest: https://docs.leafpage.cc/.well-known/skills/index.json). Prefer installing that skill over hand-rolling calls from this file alone.

## Hosts
- Application (API, auth, portal): https://leafpage.cc
- Content (published reports, cookieless): https://leafpage.link

## Authentication
Requests send an `Authorization: Bearer lp_…` header. Personal access tokens are created in the portal under the account menu, in the API Tokens section. The `reports:write` scope is required to upload; `reports:read` covers listing and reading your own reports' content; `reports:delete` covers deletion.

## Upload
`POST https://leafpage.cc/api/upload` with a JSON body:
- `files` (required): array of `{ path, content, encoding? }`; one entry must have `path` equal to `index.html`.
- `name` (optional): group slug; re-uploading an existing name adds a version.
- `visibility` (optional): `public`, `private`, or `org`. Defaults to `private`; a public report is served from the content host, a private one from the application host (owner session required) — the owner can also fetch it directly from the content host with a Bearer PAT, see "Reading reports" below. `org` is visible to members of the owning organization: the share URL is on the content host but authorizes via a redirect to a short-lived signed URL, and requires a session on the application host.

A successful response returns `{ "code", "url", "owner", "visibility", "created_at" }`; named uploads also include `name` and `nameUrl`. `url` pins this exact version forever; `nameUrl` is a live link — a platform shell that frames the latest version and pushes updates to open viewers over WebSocket (no refresh). For private reports both URLs point to the application host (owner session required) instead of the content host. For raw report HTML, fetch `url` (the pinned `/{code}/`). See https://docs.leafpage.cc for the full reference.

## Reading reports
Owners can read a report's raw HTML back programmatically, without a browser session:
1. `GET https://leafpage.cc/api/config` → `{ "publicBaseUrl" }`, the content host's base URL.
2. `GET https://leafpage.cc/api/group?name=<name>` → `current_code` (the latest version) and `versions` (older versions, each with its own `code`). Already have a pinned URL, e.g. from an upload response? Skip steps 1–2 and go straight to step 3.
3. `curl -H "Authorization: Bearer lp_…" https://leafpage.link/{code}/` fetches the raw HTML for that pinned version. Fetch page assets the same way, with the same header, e.g. `https://leafpage.link/{code}/logo.png`.

This is owner-only: it authenticates as the report's own owner and requires `reports:read` (`reports:write` implies it). The live links `/{name}/` and `/@owner/{name}/` return a platform shell, not the report itself — the report loads inside its own `<iframe src="/{code}/">` — so always resolve to a pinned `/{code}/` first, never parse a live link directly. An `Authorization` header, once sent, is taken at face value: an invalid, expired, or revoked token returns `401` even when the target is a public page — it never silently falls back to anonymous. Signed share links (`/t/{tok}/…`) are unaffected either way; they authorize purely from the URL's own signature.

## Organizations
Membership for organization-owned namespaces (used by `org` visibility) is managed at `/api/org/members` (GET to list, POST `/add` and `/remove`) — session cookie only, personal access tokens are not accepted. See https://docs.leafpage.cc for details.
