Emits

Docs

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).