---
name: notion-oauth-app
description: Creates or signs in to a Notion account and registers a public connection (Notion's OAuth app) in the Notion Developer portal to obtain an OAuth client ID and client secret — with redirect URIs, capabilities, the page-sharing permission model, token and refresh-token behaviour, and a safe credential handoff. Use when asked to get Notion OAuth credentials, create a Notion integration or connection, set up a Notion developer account, choose between an internal and a public Notion integration, rotate a Notion client secret, or debug a Notion connection that authorizes successfully but returns no pages. For any other vendor's developer portal, use that vendor's skill instead.
---

# Notion OAuth2 App Registration

Get a working Notion OAuth2 client — an account, a **public connection**, redirect URIs, capabilities, client ID and
client secret — for a platform that connects many customers' Notion workspaces.

Three things are expensive to get wrong here, and none of them is the signup form:

1. **Internal vs public.** Notion calls its OAuth apps *public connections*; the other kind, an *internal connection*,
   is one workspace with a static token and **no OAuth flow at all**. The type is chosen at creation and is not a
   toggle you flip later. A multi-tenant connector needs a public connection (§3).
2. **Installation scope.** A public connection is created as **Any workspace** or **Selected workspaces only**, and
   that choice is permanent. The restricted one cannot ever be listed on the Marketplace (§3).
3. **The page-level permission model.** A Notion connection sees *nothing* until a workspace member explicitly shares
   pages or databases with it. A token that authenticates perfectly and returns an empty workspace is almost always
   this — not a scope problem, because Notion has no per-request scopes at all. §6 is the section that decides whether
   customers think your connector works.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **Connection name**, logo, description | Shown on the consent screen and in the customer's workspace |
| **Notion account** to own it, and the **development workspace** | Must be a Workspace Owner (§2) |
| **Internal or public** | Decides everything else (§3) |
| **Installation scope** — Any workspace, or Selected workspaces only | Permanent, set at creation (§3) |
| **Redirect URIs** | Every callback host your platform serves (§4) |
| **Capability set** | Content read/update/insert, comments, user information incl. email (§5) |
| **Optional Notion template URL** | Offers users a page to duplicate during auth instead of picking pages (§6) |
| **Marketplace listing wanted?** | Optional, separate review (§3) |

## Quick Start

1. Confirm a **new** connection is needed — a new client ID orphans every existing customer connection (§1).
2. Sign in to Notion as a **Workspace Owner** of the workspace that will own it (§2).
3. Create a **public connection** in the Developer portal; pick the installation scope carefully — it is permanent (§3).
4. Add **every** redirect URI under **OAuth Domain & URIs** (§4).
5. Set capabilities, including whether user email addresses are exposed (§5).
6. Read the page-sharing model before you promise anyone this works (§6).
7. Capture client ID and client secret from the **Configuration** tab (§7).
8. Confirm token and refresh-token behaviour against the live token response, not from memory (§8).
9. Verify from a **second, unrelated workspace** (§9), then hand the credentials over (§10).

## Platform state (verified 2026-09-20 — re-verify before trusting)

Notion reorganised this whole surface in 2026. Anything written before mid-2026 uses the old vocabulary and the old
portal URL, and will send you to pages that no longer exist.

- **"Integrations" are now "connections."** The docs, the portal and the Notion UI say *connection*; *integration* is
  called out as the legacy synonym. Expect both words in the wild.
- **The Developer portal is new (announced 2026-05-12)** and lives at `https://app.notion.com/developers/connections`.
  It is one place for **Internal connections**, **Public connections**, **Workers**, and **Personal access tokens**,
  under a **Build** section in the sidebar.
- **Personal access tokens (PATs) exist now**, are user-scoped, and — unlike everything else here — **do expire**, on a
  date chosen at creation (7 / 30 / 90 / 180 days or 1 year; 1 year if unchosen; changelog 2026-07-02). A PAT is not an
  OAuth credential and cannot serve a multi-tenant connector. Do not let a "just make a token" shortcut end the run.
- **Installation scope** (Any workspace vs Selected workspaces only) is a newer creation-time field and **cannot be
  changed afterwards**.
- **OAuth authorizations now mint a fresh token pair each time** (changelog 2026-06-08) for *new* public connections,
  where they used to return the existing active token. Existing connections keep the old behaviour. Store the token
  pair from every successful response, including re-authorizations of the same connection.
- **The current API version is `2026-03-11`**, and `Notion-Version` is a required header on every REST request. There
  have been two breaking versions since 2022: `2025-09-03` (databases split into database containers plus
  `/v1/data_sources`) and `2026-03-11` (`archived` → `in_trash`, `after` → `position`, `transcription` →
  `meeting_notes`).
- **Rate limits changed 2026-09-09**: a per-connection budget of **600 req/min** on Business and Enterprise and
  **180 req/min** elsewhere, in a fixed 60-second window, *plus* a separate **per-workspace** limit shared across all
  of that workspace's connections. So a well-behaved connector can still be throttled by someone else's. Handle 429
  **and 529**, and obey `Retry-After`.

If the portal does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing connection, or register a new one

A new public connection means a **new client ID, and every existing customer authorization is bound to the old one** —
every customer would have to re-authorize and re-pick their pages. Reuse the existing connection for: adding a redirect
URI, changing capabilities, rotating a compromised secret, or diagnosing an authorization failure.

Register a **new** connection only when the user explicitly wants one: a replacement for a compromised app, a separate
app for a different product, or — the Notion-specific case — because the existing one was created as **Selected
workspaces only** and now needs to reach any workspace, or needs a Marketplace listing. That scope cannot be widened;
a new connection is the only path, and it is a customer-visible migration, not a config change. Say which path you are
taking before you touch the portal.

## 2. Account and workspace

- Sign in at `https://www.notion.so/login`, or sign up at `https://www.notion.so/signup`.
- **You must be a Workspace Owner** of the workspace you pick to create a connection there. A plain Member cannot.
- Every connection is created against a **development workspace** — the workspace that owns it administratively. For a
  public connection this is *not* a limit on who can install it; installation scope (§3) governs that.
- **All internal connections in a workspace are visible to every Workspace Owner**, including ones others created. If
  the user expects privacy from colleagues, say so now.
- A free personal workspace is enough to create and test a public connection. Note that some customer-side behaviour —
  the higher rate limit, Enterprise connection allow-lists (§6) — only appears on paid plans, so you cannot fully
  rehearse an Enterprise customer's experience from a free workspace.

Hand control back for anything only a human can do: email verification, 2FA, accepting terms, being promoted to
Workspace Owner. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Give
the user an exact, ordered click path with the literal values to paste — the redirect URIs from §4 and the capability
selections from §5 — and continue once they report back with the client ID.

## 3. Create the connection — internal vs public

This is the fork in the road. Pick before you open the form; the type is chosen at creation.

| | Internal connection | Public connection |
| --- | --- | --- |
| Workspaces | Exactly one | Many, subject to installation scope |
| Auth | **Static installation access token. No OAuth.** | **OAuth 2.0**, one token per authorizing user |
| Identity | Its own **bot user**, independent of any person | Acts **on behalf of the user who authorized it** |
| Page access | A member shares pages via **Add connections**, or the owner via the **Content access** tab | The user picks pages in the **OAuth page picker** |
| Marketplace | Never eligible | Eligible only if scope is *Any workspace* |

**A multi-tenant connector — a platform that connects other people's Notion workspaces — needs a public connection.**
An internal connection has no authorize URL, no client ID and no client secret to obtain; if someone asks for "Notion
OAuth credentials" and you end up on the internal page, you are on the wrong page. Internal connections are right for a
single team's own automation, and are the fastest way to get a token for testing.

### Creating a public connection

**Developer portal → Build → Public connections → Create new connection.** The form asks for:

- Connection name and **development workspace**
- **Redirect URI(s)** for the OAuth flow (§4)
- **Installation scope** — **Any workspace** (Marketplace-eligible) or **Selected workspaces only** (pick the
  workspaces from the list; never Marketplace-eligible). **Set once at creation, cannot be changed.** If there is any
  chance of a Marketplace listing, or of an unknown customer installing, choose *Any workspace*.
- **Capabilities** (§5)

After creation, the **Configuration** tab carries the client ID, the client secret and the connection's authorization
URL; **Content access** manages page access; Marketplace listing details live in a separate **Listings** section.

### Creating an internal connection (only if that is genuinely what is wanted)

**Developer portal → Build → Internal connections → Create a new connection** → name and workspace → **Configuration**
tab for the **Installation access token**. Then grant page access (§6) — a new connection has none, and every request
fails until it does. The token can be refreshed from the same tab if it leaks; that invalidates the old one.

### Listing and review

Listing on the **Notion Marketplace** is **optional and separate** — a public connection works through its OAuth flow
without ever being listed. If the user does want a listing: Marketplace listing dashboard → **Listings → Connections →
Start a new connection listing** (name, description, category and tags, images, and the public connection to associate
with it), save as a draft, then submit. Notion reviews **every** submission and public connections must pass a
**security review** before listing; expect a reply by email in **5–10 business days**, with feedback and a resubmit
path if rejected. Only *Any workspace* connections are eligible.

Note one inconsistency to watch for: a screenshot in Notion's authorization guide is captioned "The Authorization URL
field populates after a public connection is submitted for review," while the Marketplace guide states plainly that
listing is optional and a public connection works without it. If the **Authorization URL** field is blank on a freshly
created connection, that caption is the thing to check before assuming the portal is broken — and report what you see.

## 4. Redirect URIs

Register **every** callback host your platform serves. Notion accepts multiple, under **OAuth Domain & URIs** on the
Configuration tab. For Unified.to these are one per data center; confirm the current list with the platform owner
rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

The rule that catches people out:

> **`redirect_uri` becomes mandatory in the token-exchange body as soon as more than one redirect URI is configured.**

Notion spells it out: `redirect_uri` is required in the `POST /v1/oauth/token` body if the `redirect_uri` query
parameter was set on the authorize URL, **or** if the connection has more than one redirect URI configured — and it is
*not allowed* in the single-URI-and-no-query-param case. Since a multi-region platform has four, it is always required,
and it must be the same value that was sent on the authorize call. A mismatch comes back as `invalid_grant` at the
exchange, not at save time.

Notion does not publish scheme or host restrictions for redirect URIs (no documented localhost or HTTP rule) — treat
matching as exact, register the literal strings above, and do not rely on trailing-slash or subpath forgiveness.

## 5. Capabilities — Notion's answer to scopes

**Notion has no OAuth scopes.** The authorize URL takes `client_id`, `redirect_uri`, `response_type=code`, `owner=user`
and optional `state` — and nothing else. There is no `scope` parameter to send, and nothing to line up between the app
registration and the authorize call. Instead, **capabilities are configured on the connection itself**, in the portal,
and the consent screen describes them to the user.

| Group | Options |
| --- | --- |
| Content | **Read content**, **Update content**, **Insert content** — any combination |
| Comments | **Read comments**, **Insert comments** |
| User information | **No user information** / **User information without email addresses** / **User information with email addresses** |

What this means in practice:

- Capabilities gate **endpoints and response contents**. Insert-only cannot read full objects; read-only cannot call
  update endpoints; "no user information" strips names, avatars and emails out of user objects entirely.
- **Changing capabilities forces re-authorization.** Notion is explicit: for public connections, users must
  re-authenticate if the capabilities changed since they last authorized. So capability changes are customer-visible
  events — plan them like a migration, not a tweak.
- Request the minimum. Notion's own guidance is that fewer capabilities make a workspace admin more likely to install
  the connection, and **User information with email addresses** is the one a security reviewer will ask about.

> **As of 2026-09-20, Unified.to's Notion connector** sends no scopes at all on the authorize call (correct — Notion
> has none), and passes `owner=user` as Notion requires. Its object coverage implies the capability set it needs:
> knowledge-base spaces, pages and **comments** (read and insert), database / table / record / query objects (read,
> insert and update content), and a people object built from Notion users that maps **email addresses** — which means
> **User information with email addresses**, the most scrutinised setting on the list. Confirm the exact set with the
> connector's owner before ticking boxes, and remember that ticking a new one re-authorizes every existing customer.

## 6. The page-sharing permission model — read this before promising anything

This is Notion's headline trap, and it has no equivalent at most vendors. **A connection has access to nothing by
default.** Authorization does not grant workspace-wide access; it grants access to the specific pages and databases a
human explicitly hands over, one at a time.

How access is actually granted:

- **Public connections** — during the OAuth flow, after the capabilities prompt, Notion shows a **page picker**. The
  user searches for and selects the pages and databases to share. Only resources the user has *full access* to are
  offered. **Selecting a parent page grants access to all of its children**, which is the single most useful thing to
  tell a customer: pick the top-level page or teamspace page, not thirty leaves.
- **Internal connections** — the owner uses the **Content access** tab in the portal (**Edit access** → select pages),
  or any workspace member opens a page → **•••** → **Connections** → **+ Add connection** → pick the connection, and
  confirms it covers the page and its children.
- **After the fact** — a user can share more pages later through the same **Add connections** menu; they do not have to
  re-run OAuth to widen access. Conversely, hovering a connection's name and pressing **Disconnect** removes access to
  that page, silently, with no notification to you.

Two consequences that cause most "the integration is broken" tickets:

1. **Per-user, not per-workspace.** After a user authorizes a public connection, **only that user** can interact with
   it or share pages with it. If five people in one workspace want it, all five run the OAuth flow individually and
   each gets their own token and their own page selection. Do not build on the assumption that one authorization covers
   a customer's whole company.
2. **An empty workspace is a permission symptom, not a scope bug.** `POST /v1/search` searches only pages and data
   sources **that have been shared with the connection**, so a token with no shared pages returns an empty list, a 200,
   and no error. Direct reads return `object_not_found` (404) with a message like *"Make sure the relevant pages and
   databases are shared with your connection."* Notion states outright that the 404 "can also indicate that the
   resource has not been shared with owner of the bearer token." There is no capability, no scope and no re-
   registration that fixes it — a human has to share pages.

Two more wrinkles worth knowing:

- **Enterprise admins can gate connections.** On Enterprise plans, workspace owners can build an approved-connections
  list, restrict which connections members may install, and decide whether all members or only the connection owner may
  connect or disconnect a connection from pages. A customer whose admin has not approved your connection cannot install
  it, no matter what you register.
- **The optional template.** A public connection can offer a Notion page to duplicate during auth (Configuration tab →
  Basic information → **Notion URL for optional template**). If the user takes it, Notion adds the connection,
  duplicates the template into their workspace and shares that new page with the connection automatically, returning
  its id as `duplicated_template_id`. It is the one path that guarantees a non-empty workspace on first connect.

## 7. Capture the credentials

From the connection's **Configuration** tab in the Developer portal:

- **OAuth client ID** and **OAuth client secret** (public connections), or the **Installation access token** (internal)
- The connection's **Authorization URL**
- Endpoints, which are fixed and the same for everyone:
  - authorize — `https://api.notion.com/v1/oauth/authorize`
  - token / refresh — `https://api.notion.com/v1/oauth/token`
  - introspect — `https://api.notion.com/v1/oauth/introspect`; revoke — `https://api.notion.com/v1/oauth/revoke`
- API base `https://api.notion.com`, and the `Notion-Version` value the client will send (see **Platform state**)

The token exchange is **HTTP Basic authentication** — `client_id:client_secret`, base64-encoded, in the `Authorization`
header — with a **JSON** body (`grant_type`, `code`, and `redirect_uri` per §4). Not form-encoded, and the credentials
do not go in the body. Refresh works the same way: Basic auth, JSON body, `grant_type=refresh_token`.

Rotating the client secret breaks every token exchange and refresh until the new value is deployed. Never rotate
without explicit go-ahead and a cutover plan.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be rotated, then move on.

> **As of 2026-09-20, Unified.to's Notion connector** exchanges and refreshes tokens exactly this way — HTTP Basic auth
> from the client credentials with a JSON-encoded POST body, sending `redirect_uri` on the exchange (which is what the
> multi-URI rule in §4 requires) — and treats the resulting token as a bearer token. It also accepts a **static
> internal-connection token** as an alternative to OAuth for single-workspace customers. It **pins
> `Notion-Version: 2022-06-28`**, four breaking versions behind the current `2026-03-11`. That pin is deliberate and
> keeps old request/response shapes, but note Notion's rule that *additive* changes ship to every version at once, so
> pinning does not freeze responses — new fields will appear. Confirm all of this with the connector's owner before
> registering anything or reporting it as settled.

## 8. Token behaviour — verify this, do not recite it

Notion's token model changed, and the widely-repeated "Notion tokens never expire, there are no refresh tokens" answer
is now wrong on the second half. What the docs say as of 2026-09-20:

- **Refresh tokens exist.** The token response carries `refresh_token` alongside `access_token`, and
  `POST /v1/oauth/token` with `grant_type=refresh_token` "generates a new access token **and a new refresh token**."
  Treat refresh as **rotating**: store the new pair from every response, or the second refresh fails.
- **No expiry is documented for OAuth access tokens.** There is no `expires_in` in the documented token response
  schema, and token introspection returns `active`, `scope` and `iat` — an issued-at, with no `exp`. Notion does not
  publish a lifetime for REST-API OAuth access tokens. Build as if they can expire anyway: implement refresh, and treat
  a 401 `unauthorized` as "refresh and retry once," not "the customer must re-authorize."
- **Do not confuse these with the other two token types.** *Notion MCP* access tokens last about eight hours (raised
  from one hour, changelog 2026-07-14) and do carry `expires_in`. *Personal access tokens* expire on a date chosen at
  creation, up to a year. Neither figure applies to a public connection's OAuth access token.
- **Re-authorization now mints a new pair** for connections created after 2026-06-08 rather than returning the existing
  active token, so a customer who reconnects gives you a token you must store; the record keyed by `bot_id` is what
  ties a token pair to an authorization.
- **Revocation exists** (`/v1/oauth/revoke`), and a customer can also disconnect the connection from their side, at
  which point the token stops working with no notice to you.

Because Notion has changed this twice in a year, **re-read the authorization guide and the changelog at the start of
any run** rather than trusting this section. If the live token response now includes `expires_in`, that is the answer,
and this section is stale.

## 9. Verify end-to-end

Testing in the workspace that owns the connection proves almost nothing — that workspace's owner sees everything.
Test the path a customer takes:

1. Authorize from a **second, unrelated workspace** (a fresh free workspace is fine) through your platform's real
   connect flow, as a **non-owner Member** if customers will be.
2. At the page picker, deliberately share **one** page. Confirm your connector reads that page *and its children*, and
   does **not** see the rest of the workspace. That is correct behaviour, and worth showing the user.
3. Confirm the token response carries a **`refresh_token`**, and store `bot_id`, `workspace_id`, `workspace_name`,
   `owner` and `duplicated_template_id` alongside it.
4. Force a **refresh**, then force a **second** refresh using the token returned by the first — that is what catches a
   client that ignores rotation.
5. Call `GET /v1/users/me` with the right `Notion-Version` header to confirm the bot user, then `POST /v1/search` to
   confirm the shared page is visible.
6. If email addresses matter, confirm a user object actually carries `person.email` — if it does not, the capability
   is set to the no-email variant (§5), and fixing it re-authorizes everyone.

| Symptom | Cause |
| --- | --- |
| Auth succeeds, every list is empty, no error | Nothing shared with the connection (§6) — the default state |
| `object_not_found` (404) on a known page | Same: that page was not shared, or was disconnected later (§6) |
| `invalid_grant` at token exchange | `redirect_uri` missing from the JSON body, or not identical to the one sent on authorize (§4) |
| `invalid_client` / 401 at token exchange | Credentials not sent as base64 HTTP Basic, or sent in the body instead (§7) |
| Second refresh fails | Client is not storing the rotated `refresh_token` (§8) |
| `unauthorized` (401) on every call | Wrong token type (PAT expired, internal token refreshed) or a revoked/disconnected connection (§8) |
| User objects have no email | Capability is "without email addresses" (§5) |
| Missing `Notion-Version` → 400 | The header is required on every REST request (see **Platform state**) |
| One customer sees data, their colleague sees none | Per-user authorization — each member must run the flow (§6) |
| Customer's admin blocks the install | Enterprise approved-connections list (§6) |
| 429 despite low traffic | The shared per-workspace rate limit, not yours; obey `Retry-After`, handle 529 too |
| Response shapes changed without a version bump | Additive changes ship to every API version at once (§7 note) |

## 10. Hand off — never commit the secret

- **Do not** write the client secret, an installation access token or a PAT into source control, a test, a fixture, a
  committed `.env`, a ticket, a PR body, or a chat channel. Values go to the user, for the secret store or console.
  Notion says the same in its own docs; if a token is exposed, refresh or revoke it from the portal.
- If a code change is needed (a redirect host, the `Notion-Version` pin, refresh-token rotation), keep it secret-free
  and say what the human must set out of band.
- Close with: connection name and type (public connection), the owning workspace, **installation scope and the fact
  that it is permanent**, the client ID, where the secret was delivered, the authorize and token endpoints, the exact
  capability set, whether email addresses are exposed, the `Notion-Version` in use, the refresh-token behaviour you
  observed, whether a Marketplace listing was submitted, and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the user wants a **single-workspace** setup and an internal connection
or PAT is the right answer (that run does not produce OAuth credentials at all); the installation scope is ambiguous,
because it is permanent; a capability change is on the table, because it re-authorizes every existing customer; the
existing connection is *Selected workspaces only* and a Marketplace listing is wanted (a new connection and a customer
migration); the Marketplace or security review asks for compliance, legal, data-retention or volume claims; a
customer's Enterprise admin must approve the connection or share pages (customer-side, not registerable); or the portal
does not match the **Platform state** section above.

## References

- Overview and connection-type comparison — https://developers.notion.com/guides/get-started/overview
- Public connections — https://developers.notion.com/guides/get-started/public-connections
- Internal connections — https://developers.notion.com/guides/get-started/internal-connections
- Authorization (full OAuth flow) — https://developers.notion.com/guides/get-started/authorization
- Personal access tokens — https://developers.notion.com/guides/get-started/personal-access-tokens
- Secure API tokens — https://developers.notion.com/guides/get-started/handling-api-keys
- Preparing your connection for users — https://developers.notion.com/guides/get-started/preparing-for-users
- List on the Marketplace — https://developers.notion.com/guides/get-started/marketplace-listing
- Connection capabilities — https://developers.notion.com/reference/capabilities
- Create a token — https://developers.notion.com/reference/create-a-token
- Refresh a token — https://developers.notion.com/reference/refresh-a-token
- Introspect a token — https://developers.notion.com/reference/introspect-token
- Revoke a token — https://developers.notion.com/reference/revoke-token
- Authentication — https://developers.notion.com/reference/authentication
- Status and error codes — https://developers.notion.com/reference/status-codes
- Versioning — https://developers.notion.com/reference/versioning
- Changes by version — https://developers.notion.com/reference/changes-by-version
- Upgrading to 2025-09-03 (databases → data sources) — https://developers.notion.com/guides/get-started/upgrade-guide-2025-09-03
- Request limits and rate limits — https://developers.notion.com/reference/request-limits
- Changelog — https://developers.notion.com/page/changelog
- Developer portal — https://app.notion.com/developers/connections
- Marketplace listing dashboard — https://www.notion.so/profile/connections
- Notion Marketplace — https://www.notion.com/integrations/all
- Help: add and manage connections with the API — https://www.notion.com/help/add-and-manage-connections-with-the-api
- Help: members, admins, guests and groups — https://www.notion.com/help/add-members-admins-guests-and-groups
