Emits

Docs

Authentication

Every client (the CLI, an agent over MCP, or code calling the REST API) authenticates the same way: an OAuth grant a human approves once, which issues a short-lived access token plus a refresh token to renew it with. No surface mints a long-lived key on demand, and nothing here requires pasting a secret into a config file.

Two grants exist, one per kind of client:

  • The device grant (RFC 8628), for the CLI or anything else running in a terminal. The client shows a code; you approve it in a browser.
  • The authorization-code grant with PKCE, for agents and MCP clients. The client sends its human to a consent screen and receives the tokens on redirect.

Scopes

Scope Unlocks
pages:read Listing and reading your pages (find, get, the list tool)
pages:write Publishing, updating, visibility, delete
domains:bind Binding a custom domain (Pro)

emit login requests pages:read and pages:write. domains:bind is a step-up: emit bind-domain re-runs the device flow to request it for that one operation, and the elevated scope is not persisted into your stored credential. Scopes are fixed at approval; nothing widens a credential's scopes after the fact.

Token lifetimes

An approval issues a pair:

  • An access token, valid for one hour, sent as the bearer on every request.
  • A refresh token, valid for 90 days, used to mint the next pair.

Refresh tokens are single-use and rotating: each renewal spends the old refresh token and returns a new one, so a leaked refresh token that has already been spent is dead on arrival (invalid_grant). A client used at least once every 90 days stays signed in indefinitely.

The device grant

Method Path Body Returns
POST /oauth/device/code form: client_id, scope { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }
POST /oauth/token form: grant_type=urn:ietf:params:oauth:grant-type:device_code, device_code, client_id the token pair, or 400 { error } while pending

The verification_uri is emits.app/device: the human opens it in the browser where they are signed in, enters the code, and approves the scopes. The client polls /oauth/token at the returned interval until approval.

The authorization-code grant

For clients with a browser available (or able to send their human to one):

  1. Direct the browser to GET /oauth/authorize with client_id, redirect_uri, scope, state, and a PKCE code_challenge (S256 is required; this is a public-client flow).
  2. The consent screen shows the client's registered name and the requested scopes; the human approves.
  3. The browser is redirected back with a one-time code.
  4. The client exchanges it at POST /oauth/token (grant_type=authorization_code, with the PKCE code_verifier) for the token pair.

Renewal is POST /oauth/token with grant_type=refresh_token.

Dynamic client registration

Agents do not pre-register by hand. POST /oauth/register (RFC 7591) accepts a client's name and redirect URIs and returns a client_id on the spot. This is how an MCP client bootstraps itself from the 401 handshake with no human setup. The registered name is what the consent screen (and the console) shows, and registration is rate-limited.

Discovery

Everything above is discoverable, so a conforming client needs no configuration beyond the API origin:

Document Path
Authorization server metadata (RFC 8414) /.well-known/oauth-authorization-server
Protected-resource metadata (RFC 9728) /.well-known/oauth-protected-resource (and …/mcp for the MCP endpoint)
API catalog (RFC 9727) /.well-known/api-catalog

GET /v1/token introspects the token you hold: 200 { scopes } when live, a uniform 401 when not (expiry, revocation, and never-existed are not distinguished).

Revocation

The console lists every approved client under Settings → API access — one entry per client, named as it appeared on the consent screen, with its scopes and when its access stops. Revoking an entry ends the whole authorization: the access token stops working and the refresh token can no longer renew (invalid_grant). Signing in again is the only way back. Published pages stay up; revocation severs the client, not the content.