# API and agent setup

Use the exact dashboard origin as `LIVEPAGES_ORIGIN`. Untrusted HTML is served only from slug subdomains. JSON requests support file text strings or `{ "content": "…", "encoding": "base64" }`. Uploads are capped at 100 files / 10 MB decoded / 15 MB JSON transport; `index.html` is required. No ZIP or folder path is interpreted as file contents.

## Publish first, claim later

POST a draft without a key or connector. The page is live right away at its `url`:

```sh
curl --fail-with-body https://peep.page/api/v1/drafts \
  -H 'Content-Type: application/json' \
  --data-binary @publish.json
```

```json
{"title":"My artifact","files":{"index.html":"<!doctype html><h1>Hello</h1>"}}
```

The response (`201`) includes `status: "live_unclaimed"`, `url`, `slug`, `claimUrl`, `claimToken`, `updateKey`, `expiresAt` and `approvalUrl` (the same link as `claimUrl`, kept for older clients). Unclaimed pages are unlisted (`X-Robots-Tag: noindex, nofollow` and a robots meta tag) and show a small banner: "Unclaimed page. Expires in HH:MM:SS. Claim it to keep it." with a Report abuse link. The banner never contains the claim link.

- **Give `claimUrl` to the user privately** (in your reply to them). Never put `claimUrl` or `claimToken` in page HTML, data or anywhere public: anyone holding it can take the page.
- **Expiry is 24 hours after the last publish.** To update an unclaimed page, POST to `/api/v1/drafts` again with `{"claimToken":"...","files":{...}}` (optional `title`). It keeps the same URL, returns `200` and restarts the 24-hour clock. A token for a claimed, expired or removed page returns `404`.
- **Claiming:** the user opens `claimUrl` and signs in. The page moves to their account, the banner and expiry go away, and `updateKey` starts working (scoped to `site:SLUG` and `data:write`). They can keep it public or make it private.
- **Expired pages** return `410` with a short explanation.
- **Limits:** 5 new drafts per IP per hour, 60 republishes per IP per hour, 20 new drafts per instance per day, 100 files / 10 MB. Forms cannot submit anywhere (`form-action 'none'`), and admins can take any page down.

Save `updateKey` from the draft response securely. Once the page is claimed, send raw JSON with `PUT /api/v1/sites/:slug/data` and `Authorization: Bearer <updateKey>`. No expectedVersion is needed; last successful write wins. Only data.json can change. Send Idempotency-Key for safe retries within 24 hours; the same key with different JSON returns 409. Success: `200 {ok,url,updatedAt,bytes}`. Before the page is claimed: `403` / `pending_approval`. Bad, revoked, wrong-page and expired-draft keys: `401`. JSON body maximum: 1 MiB (`413`). Revoke or rotate under page settings. Rotation invalidates the old key immediately and shows its replacement once. Configure your agent’s scheduler for recurring writes; claiming alone does not schedule it.

Pages should poll `/data.json` and `/__livepages/status` (updatedAt is Unix seconds). These respect the page's sharing policy. A configured email sender alerts the owner after 36 hours without agent updates, once per stale period; a successful update resets the alert. Email requires RESEND_API_KEY and EMAIL_FROM.

## One-call anonymous publishing (non-owner deployments only)

The owner-only peep.page pilot rejects anonymous `/publish` requests. Use `/drafts` above instead. The following flow applies only to deployments without an owner restriction.


```sh
export LIVEPAGES_ORIGIN=http://localhost:5173
node scripts/livepages.mjs publish ./my-page --title 'Sales pitch'
```

Equivalent raw API (write request JSON to a file; do not interpolate generated HTML into shell commands):

```sh
curl --fail-with-body "$LIVEPAGES_ORIGIN/api/v1/publish" \
  -H 'Content-Type: application/json' \
  --data-binary @publish.json
```

```json
{"title":"Sales pitch","files":{"index.html":"<!doctype html><h1>Hello</h1>","data.json":"{\"revenue\":100}"}}
```

Response: `url`, `slug`, `siteId`, `versionId`, `claimUrl`, `expiresAt`. Open the **private** claim link, sign in, select a personal/team space and confirm. Claim is atomic and single-use; a GET never consumes it. Do not paste claim links into public pages. Claiming a previously public page does not erase prior disclosure.

Optional `Idempotency-Key`: use a fresh random UUID for one logical publish and reuse it only for an exact retry. Treat it as private because it can retrieve that publish’s claim response. Results expire after 24 hours and are encrypted at rest. Changes to payload with the same key fail with 409. CLI generates a new key per invocation; retrying the entire CLI creates a new logical publish.

## Permanent/team pages and updates

Create a key in the dashboard. Copy it once into a secret store or environment, never the HTML or repository. Select a team (or personal space), optional site restriction, expiry, and `data:write` or `content:write` + `data:write`. Team membership is rechecked. A data-only key cannot change HTML, access policy or membership.

```sh
export LIVEPAGES_API_KEY='your-key-from-the-dashboard'
node scripts/livepages.mjs publish ./my-page --team TEAM_ID --title 'Weekly pulse'
node scripts/livepages.mjs update PAGE_SLUG ./my-page
node scripts/livepages.mjs data PAGE_SLUG ./metrics.json
node scripts/livepages.mjs list
```

The CLI reads the current version before updating. A concurrent edit returns 409; fetch the version and consciously retry. Full updates replace the file manifest. Data pushes replace only `data.json` and preserve HTML/assets. Version history rotates automatically. Team publishing requires `teamId` even with a team key and starts team-only.

- `GET /api/v1/sites/:slug` returns `site.live_version`, versions and source status for the authorized key.
- `PUT /api/v1/sites/:slug` accepts `{expectedVersion,files}`.
- `PUT /api/v1/sites/:slug/data` accepts `{expectedVersion,data}` where data is a JSON value, not encoded JSON text.
- Read site `GET /__livepages/status` for `updatedAt`, `lastSuccess`, `stale`, `error`; the same viewing gate applies as to all page files.

Have the generated page poll `/data.json` and `/__livepages/status`, preserve displayed values on failed fetch, and visibly mark stale state. Do not fetch secrets or upstream sources in the browser. Use the built-in “Try dashboard” starter as an example.

## Version history

Every HTML publish and every data.json write creates a version with `id`, `created_at`, `source` (`agent`, `dashboard` or `api`), `file_count`, `bytes` and an optional `label`. The last 50 versions per page are kept (`MAX_VERSIONS`).

- `GET /api/v1/sites/:slug/versions` lists versions newest first. Each has `live` and an owner-only, noindex `previewUrl` (`https://SLUG.peep.page/v/VERSION_ID/`).
- `POST /api/v1/sites/:slug/versions/:id/restore` publishes that version's files as a new version and returns `versionId` and `restoredFrom`. History is never deleted by a restore. Requires a key with `content:write`; data-only keys and page update keys are rejected.
- `PATCH /api/v1/sites/:slug/versions/:id` with `{"label":"..."}` renames a version (empty clears it).
- `GET /api/v1/sites/:slug/versions/:id/download` returns that version as a .zip.


## Pinned comments

Owners can turn on comments per page (off by default). Viewers click any element and leave a comment; on private pages only signed-in people with access can comment. Each comment is anchored by CSS selector, text snippet and an x/y fallback. Comments whose element no longer matches are flagged `orphaned` and kept.

- `GET /api/v1/sites/:slug/comments?status=open|resolved|all` needs `comments:read`. Each comment has `id`, `author`, `body`, `url`, `anchor` (`selector`, `snippet`, `relX`, `relY`, `pageX`, `pageY`), `status`, `orphaned`, `resolutionNote` and `replies`.
- `POST /api/v1/sites/:slug/comments/:id/resolve` with `{"note":"..."}` needs `comments:write`. Also `/reopen` and `/replies` (`{"body":"..."}`).
- `PUT /api/v1/sites/:slug/comments/settings` with `{"enabled":true,"slackWebhook":"https://hooks.slack.com/services/..."}` is for signed-in owners only. New comments are posted to that Slack incoming webhook.
- `comments:read` and `comments:write` are separate from `data:write`; page update keys cannot read comments. CLI logins and MCP connections created from now on include both.


## Hosted data

Configure in the page’s Live data tab; team owner authorization is required. Choose hourly/daily:

- JSON endpoint: operator-approved exact HTTPS hostname and optional permitted secret header. No redirects. 15-second request timeout; 2 MB response cap.
- Metabase: approved origin + numeric saved-question ID. Uses POST `/api/card/ID/query/json`, with API key or Metabase session credential. Parameterized questions are not supported in this first version.
- Google Sheets: spreadsheet ID, A1 range and a Google service-account JSON key. Enable Sheets API and share the spreadsheet with the service-account email as Viewer. Returns Sheets `values` data. This is service-account access, not a user Google OAuth Sheets connection.

Credentials are encrypted and never returned by the API. Blank keeps the existing credential; deleting/recreating the source clears it. Changing destination/header requires re-entry. Upstream failures preserve the last successful version.

## Claude Code / Codex / Cursor

Copy `skills/livepages/SKILL.md` into the tool’s skill directory (for example `.claude/skills/livepages/SKILL.md` or `.agents/skills/livepages/SKILL.md` in your project), or paste its instructions into the agent. Set the origin explicitly. Curl requires network permission in sandboxed agent environments. Do not claim automatic installation or that a terminal agent can bypass network restrictions.

The skill can submit a private draft without agent account setup; the owner confirms in the browser. For ongoing updates or team work, sign in via the dashboard and issue a scoped API key. Email OTP endpoints exist for interactive login, but automated key creation/login from an agent is intentionally not provided: keys require the dashboard’s authenticated consent flow.

## Remote MCP

The endpoint is `https://YOUR-DOMAIN/mcp`, using Streamable HTTP. The server implements discovery, public dynamic client registration, OAuth authorization-code + PKCE S256, expiring access/refresh tokens, and space-scoped consent. Tools: `publish_site`, `list_sites`, `update_site`, `set_data_source`. File arguments contain bytes/text, never client-local paths. Source secrets must be entered in the dashboard; MCP has a separate optional source-write consent.

Google authentication and MCP client authorization are separate steps. Register this as a custom remote connector where your Claude.ai/ChatGPT account and administrator permit it. Actual hosted client acceptance remains unverified; do not label this “one-click compatible” until tested on the chosen domain. Use skill/curl when a host does not expose custom MCP. Review/revoke connections from the Connect page. Account deletion and team removal also stop tool access.

Provider references: [Google service-account OAuth](https://developers.google.com/identity/protocols/oauth2/service-account), [Metabase API keys](https://www.metabase.com/docs/latest/people-and-groups/api-keys). Verify Metabase endpoint behavior against the API docs served by your installed version at `/api/docs`.
