# REST API

The Luuphub REST API exposes everything the dashboard does — sites, boards,
posts, comments, votes, statuses, changelog, and cross-site insights — over stable,
versioned `/v1` routes.

> **Interactive reference:** the full request/response schemas render at
> [/api-reference](/api-reference) (Scalar), backed by the machine-readable
> [OpenAPI 3.1 document](/api/openapi.json). That spec is generated from the same
> Zod schemas the routes parse and return, so it can never drift from runtime.

## Authentication

Send an API key as a bearer token:

```http
GET /v1/sites HTTP/1.1
Host: app.luuphub.com
Authorization: Bearer lh_live_...
```

Create keys in the dashboard under **Developers**. API access requires a Pro plan
or higher; a key on a plan without API access gets a typed `402`, never a silent
drop.

## Scopes

Every key carries a set of scopes; a request missing the operation's scope gets a
`403` naming the missing scope. New keys are **read-only** by default. A key may
also be **restricted to one site** — a request for another site is a `403` (IDOR
protection), never silently re-scoped.

- `sites:read` — granted on a default read-only key
- `boards:read` — granted on a default read-only key
- `boards:write` — write scope, opt-in
- `posts:read` — granted on a default read-only key
- `posts:write` — write scope, opt-in
- `comments:read` — granted on a default read-only key
- `comments:write` — write scope, opt-in
- `votes:write` — write scope, opt-in
- `statuses:read` — granted on a default read-only key
- `statuses:write` — write scope, opt-in
- `changelog:read` — granted on a default read-only key
- `search:read` — granted on a default read-only key
- `insights:read` — granted on a default read-only key
- `webhooks:write` — write scope, opt-in
- `automation:read` — granted on a default read-only key
- `automation:write` — write scope, opt-in

## Idempotency

Send an `Idempotency-Key` header on writes. A replay with the same key returns the
**stored** response (not a duplicate); the same key with a different body is a
`422`; a concurrent duplicate while the original is in flight is a `409`.

## Operations

| Method | Path | Summary | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/sites` | List sites | `sites:read` |
| `GET` | `/v1/sites/{siteId}` | Get a site | `sites:read` |
| `GET` | `/v1/sites/{siteId}/boards` | List boards | `boards:read` |
| `POST` | `/v1/sites/{siteId}/boards` | Create a board | `boards:write` |
| `GET` | `/v1/sites/{siteId}/posts` | List posts | `posts:read` |
| `POST` | `/v1/sites/{siteId}/posts` | Create a post | `posts:write` |
| `GET` | `/v1/sites/{siteId}/posts/{postId}` | Get a post | `posts:read` |
| `PUT` | `/v1/sites/{siteId}/posts/{postId}/status` | Change a post status | `statuses:write` |
| `POST` | `/v1/sites/{siteId}/posts/{postId}/merge` | Merge a duplicate post into a survivor | `posts:write` |
| `GET` | `/v1/sites/{siteId}/posts/{postId}/comments` | List a post comments | `comments:read` |
| `POST` | `/v1/sites/{siteId}/posts/{postId}/comments` | Comment on a post | `comments:write` |
| `POST` | `/v1/sites/{siteId}/posts/{postId}/votes` | Vote on a post | `votes:write` |
| `GET` | `/v1/sites/{siteId}/statuses` | List statuses | `statuses:read` |
| `POST` | `/v1/sites/{siteId}/statuses` | Create a status | `statuses:write` |
| `GET` | `/v1/sites/{siteId}/changelog` | List changelog entries | `changelog:read` |
| `GET` | `/v1/sites/{siteId}/search` | Search posts | `search:read` |
| `GET` | `/v1/sites/{siteId}/insights` | Get a site AI-clustered feedback themes | `insights:read` |
| `GET` | `/v1/automation/queue` | List the autopilot queue for a site | `automation:read` |
| `POST` | `/v1/automation/runs` | Start an agent run | `automation:write` |
| `POST` | `/v1/automation/runs/{runId}/steps` | Append a step to an agent run | `automation:write` |
| `POST` | `/v1/automation/runs/{runId}/complete` | Complete an agent run | `automation:write` |

## Errors & limits

- Typed JSON error bodies on `400` / `401` / `402` / `403` / `404` / `409` / `422` / `429`.
- Per-key rate limiting returns `429` with retry guidance.
- Cursor pagination on list endpoints (`limit` 1–100, opaque `cursor`).

## Next

- [Webhooks](/developers/docs/webhooks) — get pushed the same events.
- [MCP server](/developers/docs/mcp) — the API as agent tools.
