OAuth Field Guide
Researched guide · 2026-10-28

Notion

www.notion.com · Productivity / Docs & wikis · for: AI agent reads and writes pages and databases in the user's Notion workspace.

ModerateSelf-serve, review to scalewhy this rating →
Do this next

Open https://app.notion.com/developers/connections, create a public connection with installation scope 'Any workspace', and note which fields the form requires. That settles two open questions while you get your client ID and secret.

Your path at a glance

Prepare
Register
Configure OAuth
Test
Review
Launch
Customer
Agent
You
Provider
Customer admin
Milestones
◆ Works for you
30 min – 1.5 h work
no wait
◆ Any customer
4 h – 16 h work
no wait
agent can do you agent prepares, you finish assumedClick a step to jump to it.

Watch out for

  • BlockerThe agent only sees pages the user pickedwhy & fix →
  • WarningRotating refresh tokens: one race and the user is disconnectedwhy & fix →
  • WarningNew token pair on every (re)authorization since 2026-06-08why & fix →
  • WarningInstallation scope is permanentwhy & fix →
  • WarningChanging capabilities forces re-authorizationwhy & fix →
  • WarningLow rate limits for agent workloadswhy & fix →
  • InfoEnterprise workspaces can block your connectionwhy & fix →

Before you start

Steps

Prepare

  1. 1Choose installation scope and capabilitiesYouopen ↗details →

Register the app

  1. 2Create the public connection in the Developer portalYouopen ↗details →

Configure OAuth

  1. 3Store client ID/secret and authorization URLAgent prepares, you finishopen ↗details →
  2. 4Implement authorize redirect, callback and code exchangeAgent can doopen ↗details →
  3. 5Implement refresh with rotation-safe storageAgent can doopen ↗details →
  4. 6Add rate-limit and retry handlingAgent can doopen ↗details →

Test with your account

  1. 7Connect your own workspace and verify page accessAgent prepares, you finishopen ↗details →

Review & verification

  1. 8(Optional) Submit a Marketplace listingProvider reviewsopen ↗details →

Launch to customers

  1. 9Build page-picker onboarding for the agentAgent can doopen ↗details →

Customer-side setup

  1. 10Customer authorizes and (on Enterprise) admin allows the connectionCustomer adminopen ↗details →

Copy & paste

Scopes to request
Read content (capability)Update content (capability)Insert content (capability)Read comments / Insert comments (capabilities, optional)User information without email addresses (capability)
Developer console
https://app.notion.com/developers/connections
OAuth docs
https://developers.notion.com/guides/get-started/authorization
Authorize URL
https://api.notion.com/v1/oauth/authorize
Token URL
https://api.notion.com/v1/oauth/token
Prompt for your coding agent
Show prompt
You are integrating Notion OAuth (a 'public connection') into our app so our AI agent can read and write pages and databases in each user's workspace.
Facts: authorize URL https://api.notion.com/v1/oauth/authorize (params client_id, redirect_uri, response_type=code, owner=user, state). Token URL https://api.notion.com/v1/oauth/token, HTTP Basic base64(client_id:client_secret), JSON body. Code exchange body {grant_type:'authorization_code', code, redirect_uri}. Refresh body {grant_type:'refresh_token', refresh_token}. Every refresh returns a NEW refresh token. No scopes param; capabilities are set in the portal. Send header Notion-Version: 2026-03-11.
Tasks:
1. Add env config from the .env template (NOTION_OAUTH_CLIENT_ID, NOTION_OAUTH_CLIENT_SECRET, NOTION_REDIRECT_URI). Never commit secrets.
2. Implement GET /auth/notion/start (signed state cookie, redirect) and GET /auth/notion/callback (handle error=access_denied, verify state, exchange code).
3. Persist per user: access_token, refresh_token (encrypted), bot_id, workspace_id, workspace_name, owner.user.id. Upsert on every callback because re-authorization returns a new pair.
4. Write a Notion client wrapper that refreshes on 401 under a per-connection lock, saves the rotated pair atomically, and marks the connection needs_reconnect on invalid_grant.
5. Queue requests and honor Retry-After on 429/529 with exponential backoff and jitter. Don't retry rate_limit_reason=public_api_request_blocked. Keep each workspace under about 3 req/s.
6. After connect, call POST /v1/search. If nothing is returned, show a 'Grant access to more pages' button that restarts the OAuth flow.
7. Write tests with mocked Notion responses for exchange, refresh rotation, concurrent-refresh safety and 429 handling.
Don't create the Notion connection yourself; a human does that in https://app.notion.com/developers/connections and gives you the client ID and secret.
.env template
Show dotenv
# Notion public connection (Developer portal → Configuration tab)
NOTION_OAUTH_CLIENT_ID=00000000-0000-0000-0000-000000000000
NOTION_OAUTH_CLIENT_SECRET=replace-me-from-secret-manager
NOTION_REDIRECT_URI=https://app.example.com/auth/notion/callback
NOTION_VERSION=2026-03-11
# Used to sign the OAuth state parameter
OAUTH_STATE_SECRET=generate-a-random-32-byte-value
Token exchange + rotation-safe refresh (TypeScript)
Show typescript
const basic = () => 'Basic ' + Buffer.from(`${process.env.NOTION_OAUTH_CLIENT_ID}:${process.env.NOTION_OAUTH_CLIENT_SECRET}`).toString('base64');

async function notionToken(body: Record<string, string>) {
  const res = await fetch('https://api.notion.com/v1/oauth/token', {
    method: 'POST',
    headers: { Authorization: basic(), 'Content-Type': 'application/json', 'Notion-Version': process.env.NOTION_VERSION ?? '2026-03-11' },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error ?? 'notion_oauth_error'), { status: res.status, body: json });
  return json as { access_token: string; refresh_token: string; bot_id: string; workspace_id: string; workspace_name?: string; owner: unknown };
}

export const exchangeCode = (code: string) =>
  notionToken({ grant_type: 'authorization_code', code, redirect_uri: process.env.NOTION_REDIRECT_URI! });

// Call inside a per-connection lock (e.g. SELECT ... FOR UPDATE) and save BOTH tokens in the same transaction.
export const refresh = (refreshToken: string) =>
  notionToken({ grant_type: 'refresh_token', refresh_token: refreshToken });
Customer admin approval note (Enterprise)
Show markdown
Subject: Please approve the <YourApp> Notion connection

Our team wants to use <YourApp>'s AI agent with Notion. In Notion, go to Settings → Connections and add "<YourApp>" to the approved connections list.

Capabilities requested: Read content, Update content, Insert content, Read/Insert comments, User information without email addresses.
Access is limited to the pages each user selects during authorization. Data handling: <link to privacy policy>. Security contact: <security@yourapp.example>.

Why & details

The reasoning, evidence and sources behind everything above. Hover a badge to see its sources.

The full picture

Self-serve, review to scale

Easy: a public connection (Notion's new name for a public OAuth integration) is self-serve in the Developer portal, and the docs say no Notion review is needed unless you want a Marketplace listing (5-10 business days). The real problem for an agent is the page picker: users grant access only to the pages they pick, so your agent sees nothing else until they re-authorize or share more pages. Also, refresh tokens rotate on every refresh.

Credentials are issued immediately and the OAuth flow works for any workspace when the installation scope is 'Any workspace'. Notion's only documented review is for the optional Marketplace listing (the overview calls it a security review). One docs screenshot caption suggests a 'submitted for review' step, which conflicts with this (see uncertainties). Data access is gated by each user's page selection, not by provider approval.

Rating: Self-serve with extra configuration, policies or an optional review.

Working with your own account
30 min – 1.5 h hands-on · no wait

You created a public connection in the Developer portal, set a redirect URI and capabilities, and completed OAuth against your own workspace, selecting at least one page in the page picker.

Any customer can connect
4 h – 16 h hands-on · no wait

Installation scope is 'Any workspace'; refresh-token rotation, per-user token storage, 429/529 handling and page-picker onboarding (empty-access detection, re-auth to add pages) are built. Optional Marketplace listing adds 5-10 business days of review.

What affects your setup

Before you start — why each matters

  • Notion account with a development workspace — The public-connection form asks for a connection name and a development workspace. Docs require workspace-owner rights only for internal connections; whether public connections also need them isn't stated.
  • Server-side backend with an HTTPS callback — The token exchange needs the client secret (HTTP Basic), so it must run server-side. Register every redirect URI in the portal. If you register more than one, or pass redirect_uri on the authorize URL, you must send it again in the token request.
  • Company info, privacy policy, terms of use, support email — The legacy 'make public' form required company name, website, privacy policy URL, terms of use URL and support email. The current Developer portal docs list only name, workspace, redirect URIs, installation scope and capabilities. Have these ready anyway.
  • Marketplace listing assets (optional) — Listing name, description, category/tags, images and logo, linked to an 'Any workspace' connection.
  • Users with full access to the pages they'll share — The page picker shows only pages and databases where the user has full access. On Enterprise, an owner may have to allow the connection first.

How well this guide is backed

4 tested35 source-backed2 community-reported0 uncertain· Well evidenced (98% backed)

Step details

  1. 1Choose installation scope and capabilities↑ back to step

    Pick 'Any workspace' unless you truly need to restrict installs; this can't be changed later and only 'Any workspace' can be listed on the Marketplace. Choose the minimum capabilities your agent needs (read, update, insert content; comments optional; user info without email).

    👤 Why you: A product/business judgement only you can make

    🤖 What an agent can do: Map each agent feature to the endpoints it calls and the capabilities those need, and write a one-page decision note.

    https://developers.notion.com/guides/get-started/public-connections ↗
  2. 2Create the public connection in the Developer portal↑ back to step

    Developer portal → Build → Public connections → Create new connection. Enter the name, development workspace, redirect URI(s), installation scope and capabilities. If the form asks for company/policy URLs, enter them.

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Prepare exact field values (name, redirect URIs for dev/staging/prod, capability list) so the human only pastes them.

    https://app.notion.com/developers/connections ↗
  3. 3Store client ID/secret and authorization URL↑ back to step

    Copy the OAuth client ID and secret from the Configuration tab into your secret manager. Use the Authorization URL shown there, or build https://api.notion.com/v1/oauth/authorize?client_id=…&response_type=code&owner=user&redirect_uri=…&state=….

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Generate the .env template and wire up config loading. The human pastes the secret values.

    https://developers.notion.com/guides/get-started/authorization ↗
  4. 4Implement authorize redirect, callback and code exchange↑ back to step

    Redirect with a CSRF state. On callback, handle error=access_denied, then POST JSON {grant_type:'authorization_code', code, redirect_uri} to /v1/oauth/token with HTTP Basic auth. Persist access_token, refresh_token, bot_id, workspace_id, workspace_name and owner per user.

    🤖 What an agent can do: Write the routes, token storage schema (keyed by your user + Notion bot_id/workspace_id), and a test that the token endpoint rejects bad client credentials with 401 invalid_client.

    https://developers.notion.com/reference/create-a-token ↗
  5. 5Implement refresh with rotation-safe storage↑ back to step

    POST {grant_type:'refresh_token', refresh_token} with Basic auth. The response contains a NEW refresh token; save both atomically. Serialize refreshes per connection so two workers can't race and invalidate each other.

    🤖 What an agent can do: Add a per-connection mutex or DB row lock, refresh-on-401 middleware, and a 'reconnect Notion' state when refresh returns invalid_grant.

    https://developers.notion.com/reference/refresh-a-token ↗
  6. 6Add rate-limit and retry handling↑ back to step

    Queue outgoing requests. On 429/529, wait for Retry-After (also given in additional_data.retry_after) and use exponential backoff with jitter. Don't retry public_api_request_blocked. Send the Notion-Version header (latest 2026-03-11).

    🤖 What an agent can do: Use the official @notionhq/client SDK (it retries 429/529 automatically) or port the docs' retry function. Add a per-workspace token bucket around 3 req/s.

    https://developers.notion.com/reference/request-limits ↗
  7. 7Connect your own workspace and verify page access↑ back to step

    Run the flow, select a test page and a test database in the page picker, then call POST /v1/search to confirm that only the selected items (and their children) come back. Also test Cancel (error=access_denied).

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Write a smoke-test script that lists accessible pages, creates a page under a shared parent, and updates a database row. A human clicks through the consent and page picker.

    https://developers.notion.com/guides/get-started/authorization ↗
  8. 8(Optional) Submit a Marketplace listing↑ back to step

    Marketplace listing dashboard → Listings → Connections → Start a new connection listing. Submit it for review. Expect an email response in 5-10 business days; rejections cover brand/trademark, quality, or unmet baseline criteria.

    👤 Why you: Waiting on the provider's reviewers

    🤖 What an agent can do: Draft the listing description, categories and tags, and check them against Notion's Connection Gallery Best Practices.

    https://developers.notion.com/guides/get-started/marketplace-listing ↗
  9. 9Build page-picker onboarding for the agent↑ back to step

    Detect 'no or too few pages shared' (empty search results) and tell the user to re-run the OAuth flow or use '••• → Add connections' on a page to grant more. Optionally configure a template page that is duplicated and auto-shared during OAuth.

    🤖 What an agent can do: Implement empty-access detection, a 'Grant more pages' button that reopens the authorize URL, and agent copy that explains why it can't see a page.

    https://developers.notion.com/guides/get-started/authorization ↗
  10. 10Customer authorizes and (on Enterprise) admin allows the connection↑ back to step

    Each user who wants the agent must run the OAuth flow; a workspace can't authorize once on behalf of all members. On Enterprise workspaces that restrict installs, a workspace owner must add your connection to the approved list first.

    👤 Why you: Your customer's admin must act

    🤖 What an agent can do: Write customer-facing setup docs and an admin-approval email template that names the connection.

    https://www.notion.com/help/add-and-manage-connections-with-the-api ↗

Gotchas explained

  • BlockerThe agent only sees pages the user picked

    Access is limited to the pages and databases selected in the page picker, plus their children. Users who click through quickly may share nothing or one page, and search returns only shared content. If the user clicks 'Allow access' but your callback fails, the connection gets no access to the selected pages.

    Fix: Tell users to pick a top-level parent page or teamspace page. Detect empty or low access after connect and offer 'Grant more pages' (re-run OAuth). Make callback and token exchange reliable, and consider a template page that is auto-shared.

  • WarningRotating refresh tokens: one race and the user is disconnected

    Each refresh returns a new refresh token. Developers report that the previous one is invalidated, so concurrent refreshes or failing to save the new value cause invalid_grant.

    Fix: Serialize refresh per connection, write the new pair in one transaction, and on invalid_grant mark the connection 'needs reconnect' rather than retrying in a loop.

  • WarningNew token pair on every (re)authorization since 2026-06-08

    Public connections created after this change mint a fresh access_token/refresh_token on each successful OAuth, including when the same user re-authorizes. Older connections returned the existing token. Each workspace member also authorizes individually and gets their own tokens.

    Fix: Upsert tokens per (your user, Notion workspace_id, bot_id) on every callback instead of assuming one token per workspace.

  • WarningInstallation scope is permanent

    'Selected workspaces only' can't be switched to 'Any workspace' later and is not Marketplace-eligible. Changelog (2026-09-01): the REST API also enforces the Free-plan block limit for selected-workspace OAuth connections.

    Fix: Use 'Any workspace' for a product connection. Keep a separate connection for internal testing if needed.

  • WarningChanging capabilities forces re-authorization

    If you add capabilities (e.g. Insert comments) after launch, every existing user must re-authenticate before the change applies.

    Fix: Choose the full capability set before launch, and build a re-consent prompt for capability upgrades.

  • WarningLow rate limits for agent workloads

    180 requests/min per connection on Free/Plus workspaces (600 on Business/Enterprise), plus a per-workspace budget shared with all other connections, and about 3 req/s for data-source queries. Recursive block reads by an agent hit these quickly.

    Fix: Use a request queue honoring Retry-After, cache block trees, prefer the markdown and page-content endpoints over block-by-block traversal, and cap the agent's tool-call fan-out.

  • InfoEnterprise workspaces can block your connection

    Enterprise owners can restrict which connections members install and who can connect them to pages. Members of those workspaces may be told to 'contact your workspace owner'.

    Fix: Give Enterprise customers an admin approval guide that includes your connection's name, capabilities and data-handling summary.

Why each scope

ScopeWhy you need itTierEvidence
Read content (capability)Read pages, blocks, databases and data sources the user shared, and search them.basic
Update content (capability)Edit existing pages, properties and blocks.basic
Insert content (capability)Create new pages, database rows and blocks.basic
Read comments / Insert comments (capabilities, optional)Let the agent read discussion context and leave comments instead of editing directly.basic
User information without email addresses (capability)Resolve people properties and @mentions to names. Choose 'with email addresses' only if you need emails.basic

Reference facts

  • Refresh tokensYes. Code exchange returns access_token + refresh_token; grant_type=refresh_token returns a NEW access + refresh token pair (rotation). Store both every time.
  • Access-token lifetimeNot documented. The REST token response schema has no expires_in, so refresh on 401 unauthorized rather than on a timer. (Notion MCP tokens are separate and last about 8 hours.)
  • Client auth at token endpointHTTP Basic base64(client_id:client_secret) with a JSON body (not form-encoded). Bad client credentials → 401 {"error":"invalid_client"}.
  • ScopesNo OAuth scope parameter. Capabilities (read/update/insert content, comments, user info with or without email) are set on the connection in the portal. Changing them forces users to re-authenticate.
  • Rate limitsPer connection: 180 req/min (600 on Business/Enterprise), on a fixed 60s window. A separate per-workspace limit is shared by all connections. Data-source queries are about 3 req/s. Over the limit → 429 with Retry-After (seconds); 529 = overload. In effect since 2026-09-09.
  • Installation scope'Any workspace' (Marketplace-eligible) or 'Selected workspaces only'. Set once at creation and can't be changed.
  • Authorize endpointRequired params: client_id, redirect_uri, response_type=code, owner=user; state is optional. Notion 302s to app.notion.com/install-integration without validating client_id up front.
  • PKCE / discoveryPKCE isn't documented for REST public connections, and there's no discovery document (api.notion.com/.well-known returns 400). Hosted Notion MCP (mcp.notion.com) is a separate OAuth server that requires PKCE S256 and supports dynamic client registration and CIMD.
  • Endpoints observed liveAuthorize: HTTP 302; redirects to app.notion.com/install-integration; → endpoint is live (client validation may happen after login) · Token: HTTP 401; error "invalid_client"; → token endpoint is live and rejects a bogus code

What we verified — and what we couldn't

Live probes (no credentials)

  • ✓authorize https://api.notion.com/v1/oauth/authorizeHTTP 302; redirects to app.notion.com/install-integration; → endpoint is live (client validation may happen after login)[P1]
  • ✓token https://api.notion.com/v1/oauth/tokenHTTP 401; error "invalid_client"; → token endpoint is live and rejects a bogus code[P2]
  • ✓page https://app.notion.com/developers/connectionsHTTP 200; title "Notion"[P3]
  • ✓page https://developers.notion.com/guides/get-started/authorizationHTTP 200; title "Authorization - Notion Docs"[P4]

Open questions

  • Is any Notion review required before an 'Any workspace' public connection's Authorization URL works for other workspaces?
    The marketplace FAQ and overview say public connections work without a listing and only listings are reviewed. A screenshot caption in the Authorization guide says the Authorization URL 'populates after a public connection is submitted for review', which may be left over from the old 'Submit to make public' flow.
    To resolve: Create the connection, check whether the Authorization URL is populated right away, and complete OAuth from a second, unrelated Notion workspace.
  • How long do REST public-connection access tokens live, and do refresh tokens expire?
    Docs describe refresh but give no lifetime, and the token response has no expires_in. Notion MCP tokens (about 8h) are documented separately and may differ.
    To resolve: Log the issue time and the first 401 for test connections over a few weeks, or ask in the Notion Devs Slack.
  • Are company name, website, privacy policy, terms of use and support email still required in the new Developer portal?
    Only a third-party guide that describes the legacy 'Distribution' form lists them; the current official docs don't.
    To resolve: Open the Create new connection form at app.notion.com/developers/connections and record its required fields.
  • Must the developer be a workspace owner to create a public connection?
    Docs state the owner requirement only for internal connections.
    To resolve: Try creating a public connection from a member (non-owner) account in a test workspace.
  • Should the agent use Notion MCP (mcp.notion.com) instead of a public REST connection?
    Notion MCP supports dynamic client registration and CIMD with PKCE, and acts with the user's full permissions without a page picker. That avoids the page-picker problem but ties you to MCP tools, MCP rate limits and Enterprise MCP-client allowlists. Notion's terms for commercial multi-tenant MCP clients weren't checked.
    To resolve: Read developers.notion.com/guides/mcp/build-mcp-client and the security best-practices page, and confirm with Notion whether a production SaaS agent may connect via MCP.

Ask the provider

A ready-to-send message built from the open questions above.

Send to
No contact channel found — check https://developers.notion.com/guides/get-started/authorization for developer support
Subject
OAuth integration questions — Notion
Hi Notion developer support team,

We're building an integration where AI agent reads and writes pages and databases in the user's Notion workspace. Before we register an OAuth app and launch to customers, we couldn't find answers to the following in your documentation:

1. Is any Notion review required before an 'Any workspace' public connection's Authorization URL works for other workspaces?
   (What we found: The marketplace FAQ and overview say public connections work without a listing and only listings are reviewed. A screenshot caption in the Authorization guide says the Authorization URL 'populates after a public connection is submitted for review', which may be left over from the old 'Submit to make public' flow.)
2. How long do REST public-connection access tokens live, and do refresh tokens expire?
   (What we found: Docs describe refresh but give no lifetime, and the token response has no expires_in. Notion MCP tokens (about 8h) are documented separately and may differ.)
3. Are company name, website, privacy policy, terms of use and support email still required in the new Developer portal?
   (What we found: Only a third-party guide that describes the legacy 'Distribution' form lists them; the current official docs don't.)
4. Must the developer be a workspace owner to create a public connection?
   (What we found: Docs state the owner requirement only for internal connections.)
5. Should the agent use Notion MCP (mcp.notion.com) instead of a public REST connection?
   (What we found: Notion MCP supports dynamic client registration and CIMD with PKCE, and acts with the user's full permissions without a page picker. That avoids the page-picker problem but ties you to MCP tools, MCP rate limits and Enterprise MCP-client allowlists. Notion's terms for commercial multi-tenant MCP clients weren't checked.)

Could you point us to the right documentation or process — including whether any review or approval is needed before customers outside our own account can connect, and the typical timeline?

Thanks,
[Your name]
[Company · website]

Sources