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
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: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 resultingclient_id grants no access to any user data until a user
completes the consent flow. MCP hosts do this automatically.
2.1 Request
redirect_uris— must use HTTPS (orhttp://localhostfor 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 withinvalid_client_metadata.scope— space-separated list, validated against the closed scope list; unknown tokens fail withinvalid_scope.software_id/software_version— deduplication key. When both are sent, re-registering with identical values and the sameredirect_urisreturns the existingclient_idinstead of provisioning a new client. Omit them and every registration creates a fresh client row.
2.2 Response
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
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
- 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
projectas 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
*:readscopes you requested; Full access grants everything you requested, includingposts:publish,settings:writeand the*:destroyscopes 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.
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
application/json bodies are also accepted, for hosts that
can’t send form encoding.)
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_tokenwith a fresh 1-hour TTL. - A new
refresh_tokenwith 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:- The entire token family (every access + refresh token chained back to the original consent) is revoked.
- The user has to consent again on the next
/authorize. - The revocation appears in the workspace audit log and the user’s email notifications.
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 answersnot_found. - Everything else is refused with
forbidden_projectanddetails.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).
5.2 Adding a scope later
- Send the user through
/authorizeagain with the wider scope set (users who previously approved a strict subset land on an “additional permissions requested” branch of the consent screen). - Exchange a new token. Existing tokens are not widened — an access token’s scope set is frozen at issuance.
6. Token introspection (RFC 7662)
{ "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_tokenrevocation kills the entire token family (the refresh chain and every access token from the same consent).access_tokenrevocation 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_idis present and known, the endpoint always returns200 OKregardless of whether the token was active. RFC 7009 mandates this — it’s a privacy requirement.
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 yourredirect_uri:
/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:
error.code. The codes you’ll see:
10. Worked example (Node + oauth4webapi)
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-Idheader; see Workspaces § Multi-workspace access. - Status page — status.scripe.io.
12. Need help?
- Email [email protected] for integration questions.
- Email [email protected] for suspected leaked tokens or stolen consents — we revoke within one business hour.