# The agent API

The CLI is a thin wrapper over a small REST API. Anything the CLI does, an
agent can do directly over HTTP: find, publish, update, delete. The surface is
deliberately small enough to hold in one prompt. (If your agent speaks
[MCP](/docs/mcp), it does not need this page; the same verbs are exposed
as tools.)

## Base URL and auth

The API origin is:

```
https://api.emits.app
```

Override it with the `--endpoint <url>` flag on any CLI verb, or the
`EMITS_API_URL` environment variable (the pre-rename `SHORTWIND_CLOUD_API` is
still honored). The CLI refuses to send credentials to a non-default origin
unless `EMITS_ALLOW_CUSTOM_ENDPOINT=1` is also set; the env var makes
pointing at a dev, staging or self-hosted deployment a deliberate act
instead of a one-flag slip. A non-localhost override must be `https`. Every
request carries a bearer token and JSON:

```
Authorization: Bearer <token>
Content-Type: application/json
```

Get a token with [`emit login`](/docs/quickstart#1-log-in) (the device grant),
or let an agent authorize itself with the authorization-code grant — both are
covered in [authentication](/docs/auth). `GET /v1/token` introspects the token
you hold: `200 { scopes }` if it is live, a uniform `401` if it is not.

## Pages

| Method | Path | Body | Returns |
| --- | --- | --- | --- |
| `GET` | `/v1/pages?q=&tag=` | — | `{ pages: PageSummary[] }` |
| `GET` | `/v1/pages/{id}` | — | `{ page, versions }` or `404` |
| `POST` | `/v1/pages` | `PublishPayload` | `{ id, url, version }`, or `409 { existingId }` |
| `POST` | `/v1/bundles` | `BundlePayload` | like publish, plus the file list |
| `PATCH` | `/v1/pages/{id}` | `UpdatePayload` | `{ id, url, version }` |
| `PATCH` | `/v1/pages/{id}/visibility` | `{ visibility }` | updated `PageSummary` |
| `DELETE` | `/v1/pages/{id}` | — | tombstone lifecycle |

`GET /v1/pages` filters with `q` (free text) and `tag` (repeatable; pages must
carry all of them).

### Publish

```http
POST /v1/pages
{
  "html": "<h1>Launch notes</h1>",
  "slug": "launch-notes",
  "visibility": "public",
  "tags": ["launch"]
}

201 Created
{ "id": "pg_…", "url": "https://launch-notes.emits.page", "version": 1 }
```

Only `html` is required. `slug`, `tags`, `visibility`, `css`, and
`idempotencyKey` are optional, as are the recipe-expansion inputs (`lockfile`
and `recipes`; see [publishing](/docs/publishing#how-recipes-travel-with-a-publish)).
`UpdatePayload` is the same shape minus `slug` (the URL is fixed to the
existing page).

A slug collision returns `409` with a top-level `existingId` so the caller can
switch to `PATCH /v1/pages/{existingId}`.

### Bundles

`POST /v1/bundles` publishes a linked multi-page unit in one request: `files`
(an array of `{ path, html }`), `entryPath` naming the entry file, and the same
optional `slug` / `tags` / `visibility` / `idempotencyKey` as publish. The
model and its constraints are covered in
[multi-page publishes](/docs/publishing#multi-page-publishes).

## Domains

Account-level custom domains (Pro). See [custom domains](/docs/domains).

| Method | Path | Body | Returns |
| --- | --- | --- | --- |
| `POST` | `/v1/domains` | `{ hostname }` | `DomainBindResult` (needs `domains:bind`) |
| `GET` | `/v1/domains` | — | `{ domains: AccountDomain[] }` |
| `POST` | `/v1/domains/approve` | `{ hostname }` | `DomainBindResult` |

## Authentication

Two OAuth grants issue tokens: the **device grant** (how `emit login` works)
and the **authorization-code grant with PKCE** (how an agent authorizes
itself, with a human approving in a browser). Tokens are scoped
(`pages:read`, `pages:write`, `domains:bind`) and short-lived, renewed with a
rotating refresh token. Discovery documents live at
`/.well-known/oauth-authorization-server` and `/.well-known/api-catalog`.

The full model (endpoints, scopes, token lifetimes, dynamic client
registration, and revocation) is on the [authentication](/docs/auth) page.

## Errors

Responses map to typed error kinds by status: `401` unauthorized, `403`
forbidden (missing scope), `404` not_found, `409` conflict (carries
`existingId`), plus network and generic http errors. Error bodies use the
shape `{ error: { code } }`.

## Trust and safety

Published pages are static artifacts, but the platform keeps two safety
levers.

**Abuse reports** are unauthenticated and rate-limited:

```http
POST /v1/abuse
{ "pageId": "pg_…", "reason": "…", "category": "phishing" }

202 { "state": "reported" }
```

`category` is one of `csam`, `phishing`, `malware`, or `other`. A report opens
a moderation case without pulling the page, and the response is the same `202`
whether or not the page exists, so the endpoint is not an existence oracle.

**Takedowns** are distinct from a user delete. A user `delete` tombstones a
page; an abuse takedown quarantines it, sealing the artifact so it stops
resolving within seconds while the object and version history are preserved
(never hard-deleted). Publishing also runs a content scan at publish time, and
each account has a publish rate limit (10/min sustained, burst of 5).
