Recommended implementation paths
Before getting to the integration, decide between registering the client dynamically or registering it beforehand and reusing a fixedclient_id.
Dynamic registration is for clients that can’t predict their own deployment details ahead of time, like an MCP server. The standard case is registering beforehand, and the rest of this guide assumes that path.
If you’re registering beforehand, ask us to mark the client manual_verified. That gets it a verified badge on the consent screen. It’s the only effect it has today. It doesn’t change scopes, rate limits, or anything else.
Scopes
Use the smallest scope that works for your integration:emails:sendis enough for send-only routes, such asPOST /emails,POST /email,POST /emails/sending,POST /email/sending, andPOST /broadcasts/:broadcastId/send.full_accessis required for other API routes.
scope, we register it with every supported scope. Pass scope explicitly during registration and authorization instead of relying on that default.
Request encoding and resource
Dynamic client registration uses JSON. The token and revocation endpoints accept both JSON and application/x-www-form-urlencoded, but prefer form encoding for /oauth/token and /oauth/revoke, since that’s what most OAuth libraries send by default.
We do not support RFC 8707 resource indicators yet. Don’t send or rely on resource in authorization or token requests. It’s accepted but ignored.
Generating PKCE values and state
Before starting the authorization request, generate three values:code_verifier: a high-entropy random string kept only by the client.code_challenge: the base64url-encoded SHA-256 hash of thecode_verifier.state: a high-entropy random string used to bind the callback to the request that started the flow.
code_verifier is sent only during the token exchange. The code_challenge is sent during authorization. The state is sent during authorization and must come back unchanged on the callback.
state and codeVerifier server-side before redirecting the user to us. For a local client, keep them in memory while the temporary callback server is running.
Pre-registered remote client
A remote client should use an HTTPS redirect URI owned by the app, for examplehttps://example.com/oauth/callback.
For remote apps, you can still use POST /oauth/register manually while we don’t have a central place in the app to create clients.
Generate PKCE and state
code_verifier, code_challenge, and state. See
above.Reject invalid callbacks
A missing
code, mismatched or missing state, or an error query
parameter should all be treated as failures.Local client
A local client must use a loopback redirect URI. Prefer127.0.0.1 with a random local port, for example http://127.0.0.1:49152/oauth/callback.
Registered loopback redirect URIs allow port variance. The host, path, and query string must still match. For example, a client can register http://127.0.0.1/oauth/callback and later authorize with http://127.0.0.1:49152/oauth/callback.
Bind a temporary callback server
Pick a random high port and bind it to
127.0.0.1 or [::1], never
0.0.0.0.Generate PKCE and state
code_verifier, code_challenge, and state. See
above.Register the client
Dynamically register with a loopback redirect URI and the minimum required
scope. See Register Client.
Open the authorize URL in the user's browser
Don’t prefetch
/oauth/authorize from your process and follow the redirect
yourself. The user needs to see the consent screen.Reject invalid callbacks
A missing
code, mismatched or missing state, or an error query
parameter should all be treated as failures.Exchange the code
Use the original
code_verifier. See Token.Dynamic client registration
Passscope explicitly. If DCR omits scope, we register the client with every supported scope by default.
client_id:
Authorization request
Do not have a backend or CLI process prefetch/oauth/authorize and then open the returned dashboard URL. Instead, open the authorization URL in the user’s browser and let the browser follow the redirect to the dashboard.
redirect_uri used in the authorization request:
state matches the original state before exchanging the code.
Authorization code exchange
Theredirect_uri in the token request must match the redirect_uri from the authorization request.