Skip to main content

OAuth 2.1

Status: Public preview. Behaviour and endpoint URLs are stable for v1; new scopes land additively. Breaking changes only ship in a new dated API version with at least 90 days of overlap.
This is the integrator-facing reference for Scripe’s OAuth 2.1 authorization server. If you’re building your own product that calls the Scripe API on behalf of a Scripe user (a Zapier-style integration, an internal SaaS, an MCP host), this is the contract you code against. OAuth tokens are also the only credential the MCP transport accepts. If you only need a single-workspace key for your own scripts, mint an API key instead — it’s substantially simpler.

TL;DR

The grant path is authorization code with PKCE-S256, and v1 supports public clients only: token_endpoint_auth_method must be "none", no client secret is issued, and PKCE is the proof of client identity. We do not support the implicit grant, the password grant, client credentials, or unencrypted PKCE challenges.

1. Hosts and discovery

Fetch the discovery documents unauthenticated; they are cache-friendly and do not require a registered client. They are the source of truth for endpoint URLs — hard-code the discovery URLs below, not the endpoints they name:
The first is the RFC 8414 authorization-server metadata (endpoints, supported scopes, grant types, code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"]). The protected-resource documents are RFC 9728 metadata for the REST (https://api.scripe.io) and MCP (https://mcp.scripe.io) resource servers respectively. The stable public endpoint forms on the API host:

2. Dynamic Client Registration (RFC 7591)

You register your OAuth client by POSTing to the registration endpoint. The endpoint is public — anyone can register a client, and the resulting client_id grants no access to any user data until a user completes the consent flow. MCP hosts do this automatically.

2.1 Request

Field notes:
  • redirect_uris — must use HTTPS (or http://localhost for dev). Custom URI schemes (com.acme://…) are allowed for native and mobile clients.
  • token_endpoint_auth_method — must be "none" (or omitted; it is the default). v1 supports public clients only; any other value is rejected with invalid_client_metadata.
  • scope — space-separated list, validated against the closed scope list; unknown tokens fail with invalid_scope.
  • software_id / software_version — deduplication key. When both are sent, re-registering with identical values and the same redirect_uris returns the existing client_id instead of provisioning a new client. Omit them and every registration creates a fresh client row.

2.2 Response

No client_secret is issued — public clients prove themselves with PKCE. We treat each registered client as immutable once issued; register again if your metadata changes.

3. Authorization code with PKCE

3.1 Mint a PKCE pair

Store code_verifier somewhere server-side (or in a secure cookie). You present it later at the token endpoint.

3.2 Redirect the user to the authorization endpoint

The browser lands on the Scripe consent screen. If the user isn’t signed in, they authenticate first; once authenticated they see:
  • Your client’s name + logo, and its trust state: a Verified by Scripe pill for clients that passed review, or an “unverified” warning for everyone else. Well-known MCP hosts (Claude) are recognised by the redirect URIs they own and render verified with the publisher’s canonical name, logo and homepage without any review step — registrant-supplied branding never renders under the verified pill. If you want your client verified, contact us.
  • A workspace picker (which workspace the grant pins as its default) and a default project picker when the workspace has more than one project. The screen labels that picker Default profile: the Scripe UI calls a project a Profile, while the API keeps project as its published noun — see MCP § Workspaces and projects.
  • One access level choice — Read-only or Full access — with the permissions that choice grants listed underneath in plain English. Read-only grants the *:read scopes you requested; Full access grants everything you requested, including posts:publish, settings:write and the *:destroy scopes when you asked for them by name. There are no per-scope toggles. If you request no write-side scope the choice is locked to Read-only.
  • A “Cancel” / “Authorize access” choice.
If the user has already approved this exact client + scope set before and has not revoked, the screen short-circuits to a “Welcome back” view — still a visible page, never a silent flow.

3.3 Receive the code

Approved consent redirects back with ?code=…&state=…&iss=…. Always verify state matches what you sent. A declined consent redirects with ?error=access_denied&state=… — errors on this leg arrive as RFC 6749 redirect query parameters (error, error_description). Codes are single-use with a 10-minute TTL. Re-using a code raises invalid_grant.

3.4 Exchange at the token endpoint

The token endpoint accepts no client authentication — PKCE is the only proof. (application/json bodies are also accepted, for hosts that can’t send form encoding.)
The scope returned may be narrower than what you requested if the user chose Read-only on the consent screen (every write-side scope is dropped, offline_access is kept). It will never be wider. The returned form is always expanded and de-duplicated (aliases resolved, implied reads included), so your client never has to special-case aliases at runtime.

4. Refresh token rotation + reuse detection

Refresh tokens rotate on every use. A successful refresh issues:
  • A new access_token with a fresh 1-hour TTL.
  • A new refresh_token with a fresh sliding window.
  • A scope ⊆ the previous scope (you can narrow, never widen).

4.1 Reuse detection

Presenting an already-rotated refresh token is treated as a leaked-token signal:
  1. The entire token family (every access + refresh token chained back to the original consent) is revoked.
  2. The user has to consent again on the next /authorize.
  3. The revocation appears in the workspace audit log and the user’s email notifications.
There is one deliberate exception: replaying the same refresh token within a 15-second grace window (a network-glitch retry) returns the same new token pair instead of revoking the family. Outside that window, reuse is theft. If your client crashes between “server returned new refresh_token” and “I persisted it”, treat it as a fresh consent flow — the old token is dead.

4.2 Lifetimes

Long-lived integrations that need offline access must request the offline_access scope. Without it the consent does not mint refresh tokens and the user has to re-authorise after each access-token expiry.

5. Scopes

The closed list of scopes we honour at v1: The consent screen also accepts the aliases read and write, which expand to the union of the matching read or read+write scopes. Token responses always echo the expanded, deduplicated form. Never implied. posts:publish, settings:write, and the *:destroy family sit outside both aliases and outside every implication edge — write does not grant them, and neither does the matching *:write scope. A client that needs them must request each one explicitly; the consent screen then lists each under Full access with plain-English copy (and never under Read-only), and consents granted before these scopes existed never gain them.

5.1 Client accounts get the dashboard’s client view

A token issued to a user whose workspace role is Client (the brand owner an agency runs a profile for) reaches what that person reaches in the dashboard, whatever scopes it holds:
  • Reads that answer normally: the workspace, its projects, and analytics.
  • Reads that are narrowed: posts (GET /v1/posts, GET /v1/posts/:postId, list_posts, get_post), the status columns and their counts (GET /v1/post-statuses, list_post_statuses) and the calendar (GET /v1/calendar, list_calendar) return only posts in review, scheduled or published. Notes and ideas are left out of the calendar. A post outside that set answers not_found.
  • Everything else is refused with forbidden_project and details.reason: "client_role": every write, and the reads the dashboard withholds from a client (ideas, notes, sources, media, knowledge, positioning and brand settings, engagement, the team roster, usage, jobs, webhooks, community lists, viral posts).
The role is read live from the workspace membership (a change takes effect within a minute). API keys are unaffected: a key is issued by a workspace admin and acts as the workspace, not as a person.

5.2 Adding a scope later

  1. Send the user through /authorize again with the wider scope set (users who previously approved a strict subset land on an “additional permissions requested” branch of the consent screen).
  2. Exchange a new token. Existing tokens are not widened — an access token’s scope set is frozen at issuance.

6. Token introspection (RFC 7662)

Introspection in v1 is public: any caller holding a valid token can introspect it (no client authentication required). Inactive tokens always come back as { "active": false } — we do not leak the reason.

7. Revocation (RFC 7009)

client_id is required — the endpoint rejects a body without it (400 invalid_request) before it looks at the token at all, and an unknown id is invalid_client.
  • refresh_token revocation kills the entire token family (the refresh chain and every access token from the same consent).
  • access_token revocation kills only that access token — the refresh token survives, and the next refresh works as normal.
  • Clients revoke without authentication, but only their own tokens — we verify the token belongs to the presenting client_id.
  • Once client_id is present and known, the endpoint always returns 200 OK regardless of whether the token was active. RFC 7009 mandates this — it’s a privacy requirement.
The user can also revoke from the dashboard at Settings → Developer → Connected apps. That path triggers the same email notification and audit-log entry as a programmatic revoke.

8. Security headers and CORS

The consent screen sets Cache-Control: no-store and is intentionally not embeddable. If you need to pre-warm the consent flow, send the user there as a top-level navigation or in a popup window — just not in an iframe.

9. Errors

Two error shapes exist, matching the two legs of the flow: The authorize redirect leg uses RFC 6749 query parameters on the redirect back to your redirect_uri:
Every JSON endpoint (/oauth/register, /oauth/token, /oauth/revoke, /oauth/introspect) returns the same error envelope as the rest of the API, with the OAuth error name in error.code:
Switch on error.code. The codes you’ll see:

10. Worked example (Node + oauth4webapi)

This is the same flow every MCP host (Claude, ChatGPT, Cursor) runs automatically when you add the Scripe MCP server — see MCP.

11. Operational notes

  • Audit log — every authorize, token, and revoke action appears in the workspace’s audit log with the originating client.
  • Email notifications — the user receives an email when a new consent is granted, when reuse-detection revokes their tokens, and when they revoke from the dashboard.
  • Multi-workspace — one token can reach every workspace its user belongs to, plus every client workspace billed to an agency they own, via the Scripe-Workspace-Id header; see Workspaces § Multi-workspace access.
  • Status pagestatus.scripe.io.

12. Need help?