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, 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 (the device grant),
or let an agent authorize itself with the authorization-code grant — both are
covered in authentication. 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
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).
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.
Domains
Account-level custom domains (Pro). See custom 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 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:
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).