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):
- Direct the browser to
GET /oauth/authorizewithclient_id,redirect_uri,scope,state, and a PKCEcode_challenge(S256is required; this is a public-client flow). - The consent screen shows the client's registered name and the requested scopes; the human approves.
- The browser is redirected back with a one-time
code. - The client exchanges it at
POST /oauth/token(grant_type=authorization_code, with the PKCEcode_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.