Free plan

OAuth apps and oEmbed

Build integrations with EveryPage's OAuth 2.0 provider (dynamic client registration, resource indicators, iss) and universal oEmbed embedding.

EveryPage provides two complementary APIs for integrating with documents: OAuth 2.0 for granting third-party apps controlled access to user accounts, and oEmbed for turning any share link into an embedded viewer.

OAuth 2.0

EveryPage acts as an OAuth 2.0 provider, allowing third-party applications to access a user's EveryPage account without handling their credentials. This is fundamentally different from API keys, which carry full account access and cannot be scoped—an OAuth token's permissions are narrowed to exactly what a user grants.

Endpoints and flow

You implement the authorization-code flow with PKCE (RFC 7636). The endpoints sit at the site root, not under /api, and are advertised in the RFC 8414 metadata:

  • GET /oauth/authorize — User consent screen. Redirect users here with client_id, redirect_uri, response_type=code, code_challenge, code_challenge_method=S256, scope, and optionally state and resource. response_type=code and code_challenge_method=S256 are the only accepted values; the redirect_uri must match one registered for the client (see Redirect URI matching), and an unregistered one is rejected with a rendered error rather than a redirect.
  • POST /oauth/token — Exchange an authorization code for tokens, or refresh an access token. Form-encoded. Submit grant_type (one of authorization_code or refresh_token), then either code + redirect_uri + code_verifier, or refresh_token, plus resource if the authorization request carried one. Client credentials go in HTTP Basic auth, or as client_id/client_secret in the body; a confidential client must present a matching secret, and a public client sends client_id alone. The code_verifier must be 43–128 characters.
  • POST /oauth/revoke — Revoke an access or refresh token per RFC 7009. Submit the token and client credentials.
  • POST /oauth/register — Dynamic client registration (RFC 7591). See Client registration.

/oauth/authorize requires a signed-in EveryPage session. A signed-out user is redirected to the login page and returned to the same authorize request afterwards, so your app does not need to handle that case. Once signed in, the browser lands on the consent screen; approving redirects back to your redirect_uri with code, state and iss, and denying redirects back with error=access_denied (and state and iss).

The iss parameter

Every authorization response, success or error, carries iss=https://everypage.co (RFC 9207), and the server metadata says so with authorization_response_iss_parameter_supported: true. A client that talks to more than one authorization server should check that iss matches the issuer it sent the user to before redeeming the code; this is the defence against mix-up attacks.

Resource indicators

resource (RFC 8707) names the API a token is for. EveryPage accepts three values on /oauth/authorize:

  • https://everypage.co/mcp — the hosted MCP server
  • https://everypage.co/api/v1 — the platform API
  • https://everypage.co — the site as a whole

Anything else is refused with invalid_target. The code remembers the value, a resource sent to /oauth/token must match it (otherwise invalid_target), and a refreshed token keeps it. A token bound to https://everypage.co/mcp is the only kind /mcp accepts. /api/v1 accepts any live token whatever its resource, so a token minted for /mcp can call the API the MCP tools are built on. A dynamically registered client that sends no resource is bound to https://everypage.co/mcp, because that is what registration exists for.

The authorization code expires in 5 minutes and is single-use — it is consumed before the client and PKCE checks run, so a failed exchange cannot be retried. Access tokens expire in 1 hour; refresh tokens in 60 days.

Refresh tokens rotate: each refresh returns a new refresh token and retires the old one. Presenting an already-used refresh token revokes the whole grant and returns invalid_grant. A refresh may pass scope to narrow the grant, never to widen it.

Scopes

The consent screen lists every scope the app requested; the user approves or denies the request as a whole, not scope by scope. The available scopes are:

  • files:read — Read file list and share links.
  • files:write — Upload files, modify file settings, delete files.
  • readership:read — Read readership analytics and viewer sessions.
  • profile — Read the user's email address and subscription plan.
  • webhooks:manage — List, create, delete, and test webhook endpoints.

A client can only request scopes it was registered for; anything outside that set fails with invalid_scope.

An OAuth token holds only the scopes the user granted. By contrast, an API key holds all scopes—so OAuth is the preferred surface for third-party integrations that need only a subset of capabilities.

Client registration

There are two ways to get a client_id.

Registered by EveryPage. For an integration you publish (a marketplace app, a confidential server-side client), contact EveryPage with your app's name, its redirect URIs and the scopes it needs. EveryPage issues a client_id and, for a confidential client, a client_secret.

Dynamic registration (RFC 7591). Clients that register themselves at runtime, MCP clients such as Claude and Claude Code above all, POST JSON to /oauth/register:

{
  "client_name": "Claude",
  "redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "scope": "files:read files:write readership:read profile webhooks:manage"
}
  • token_endpoint_auth_method is none (the default: a public client), client_secret_basic or client_secret_post. For either secret method the response carries a client_secret (with client_secret_expires_at: 0, it does not expire), and the token endpoint then requires it. PKCE is required either way.
  • grant_types may only contain authorization_code and refresh_token; response_types must be code.
  • redirect_uris holds 1–5 URIs, each https or an http loopback address (localhost, 127.0.0.1, [::1]), without a fragment, up to 2000 characters.
  • client_name is trimmed and capped at 100 characters. It is the client's own claim, so the consent screen shows the redirect host next to it, and warns when the redirect is a loopback address ("an app on this computer is asking for access").
  • scope must be a subset of the five scopes; when omitted the client may request all five.

The response is 201 with the RFC 7591 client information (client_id, client_id_issued_at, the registered metadata, and client_secret for a secret method). Invalid input is a 400 with invalid_redirect_uri or invalid_client_metadata. The endpoint has its own per-IP rate limit. A dynamically registered client that requests no scope at authorization is treated as requesting every scope it registered for, and the consent screen lists them. Registrations that never obtain a token within 24 hours, or that have not obtained one for 30 days, are removed automatically; the client simply registers again.

Redirect URI matching

Registered URIs are matched by exact string comparison, with one exception: per RFC 8252 §7.3, when both the registered and the presented URI are http on the same loopback host (localhost, 127.0.0.1 or [::1], not interchangeably) with the same path, the port is ignored. Native apps such as Claude Code listen on whatever port is free, so http://localhost/callback registered matches http://localhost:53122/callback presented. Every other URI, https ones included, must match exactly — no prefixes, no wildcards — so register each one verbatim.

User revocation

Users see and revoke active OAuth grants under "Connected apps" on their account settings page, where each row shows the app's redirect host and when it was allowed (two "Claude" rows are two separate connections). Revoking immediately invalidates all tokens for that app.

oEmbed

The GET /oembed endpoint is a public, unauthenticated, per-IP rate-limited oEmbed provider (type rich). It sits at the site root and also answers on owners' custom domains. When a user pastes an EveryPage share link into a consumer that supports oEmbed—WordPress, Notion, or other tools—the consumer queries this endpoint to turn the link into an embedded viewer.

Endpoint

GET /oembed?url=https://everypage.co/{id}&format=json

The url parameter must be an absolute http or https URL pointing to any of EveryPage's identifier forms: the canonical UUID, a 12-character short ID, a variant short ID, or a vanity slug (Pro, on the owner's custom domain). The identifier is read from the first path segment, and an /embed/{id} link is accepted too, so a copied embed src resolves like a share link. A missing or unparseable url is a 400. The endpoint responds with JSON:

{
  "version": "1.0",
  "type": "rich",
  "provider_name": "EveryPage",
  "provider_url": "https://everypage.co",
  "title": "Document title",
  "html": "<iframe src=\"https://everypage.co/embed/{shortId}\" width=\"800\" height=\"600\" frameborder=\"0\" allowfullscreen title=\"Document title\"></iframe>",
  "width": 800,
  "height": 600
}

The default iframe dimensions are 800×600; consumers may restyle them.

Identifier resolution

The oEmbed endpoint resolves every identifier form:

  • UUID — Resolves on everypage.co and on the owner's own custom domain.
  • Short ID — Same as the UUID.
  • Variant short ID — Resolves to the parent file; the emitted embed points to the parent, not the variant.
  • Vanity slug — Resolves only on the owner's active custom domain (or everypage.co if no domain is set).

Every form is checked against the host in the pasted url, not just slugs. If that host is a custom domain registered to a different account, the lookup fails — a branded domain can never vouch for a file it does not own.

Dead links of every shape return a uniform 404:

  • Expired files
  • Burned (revoked) files
  • Non-existent identifiers
  • Unclaimed anonymous uploads (legacy)
  • Revoked link variants
  • An identifier on a foreign custom domain

Returning an identical 404 for all dead states prevents third-party consumers from probing whether a link exists or has been claimed.

Frozen artifact rule

The html field in the response always embeds via the durable /embed/{shortId} URL, never a vanity slug and never a variant identifier. This ensures that the frozen HTML outlives slug renames, domain changes, and variant revocations. Once pasted into a foreign page, the embed remains valid even if the owner changes the link's settings. See embedding for more detail.

Consumer sandboxing

Some oEmbed consumers (like WordPress) treat EveryPage as an untrusted provider and render the embed in a sandboxed iframe with an opaque origin. This prevents the viewer from booting because requests for /assets and session-based public endpoints fail the same-origin check.

The WordPress plugin resolves this by registering EveryPage as a trusted provider on plugin-installed sites, so embeds render without sandboxing. Without the plugin the iframe is inserted but the viewer does not load — there is no partial rendering, and no configuration on your side changes that. Making plugin-less pastes render is a known outstanding platform change (cross-origin headers on assets and public read endpoints, plus cookieless viewing), not something implemented today. See WordPress integration for setup.

Format parameter

Only format=json is supported (XML is deliberately not implemented). Omitting the parameter is equivalent to format=json. Any other value returns 501 Not Implemented.

See also