Skip to main content
Notion MCP accepts an ID-JAG (Identity Assertion JWT Authorization Grant) at its token endpoint. An ID-JAG is a short-lived JWT that your company’s identity provider (IdP) signs for one user and one MCP client. Your MCP client trades it for a Notion MCP access token without opening a browser or showing a consent screen. This page lists what Notion checks and what it returns. It’s for developers of MCP clients and identity providers. The protocol itself is defined in two specs, and this page doesn’t restate them: For the interactive OAuth flow, see Build an MCP client.

Requirements

Enterprise-managed authorization is part of the Enterprise plan. The workspace must use SAML SSO. An admin turns on Enterprise-managed connections in Notion’s settings and enters the identity provider issuer URL. That URL must be a plain https URL with no query string, fragment, or credentials. It must also be on the host of the IdP that runs the workspace’s SAML SSO. Each issuer is bound to exactly one workspace. Notion checks this binding every time it issues a token and every time a token is used. If an admin turns the setting off, removes SAML SSO, moves to a different IdP, or the workspace leaves the Enterprise plan, existing tokens stop working.

Token endpoint

The metadata document lists the grant type in grant_types_supported and the grant profile in authorization_grant_profiles_supported. Your MCP client needs a client ID before it can call the token endpoint. Register with dynamic client registration at https://mcp.notion.com/register, or use a client ID metadata document. The registration must include at least one redirect URI, even though this grant never redirects. Notion uses it to match the connection to the same client settings as an interactive connection. Without one, the exchange fails. Public clients (token_endpoint_auth_method of none) are allowed, and they send client_id in the form body. Confidential clients use the method they registered. With client_secret_basic, send the client ID and secret in an HTTP Basic Authorization header, and leave both out of the form body. With client_secret_post, send client_id and client_secret in the form body.

Request

Send a form-encoded POST to the token endpoint.

Response

A successful exchange returns JSON:
There’s no refresh_token. When the access token expires, get a new ID-JAG from the IdP and exchange it again. Use expires_in to schedule that. Don’t assume a fixed value: the default is 8 hours, but Notion can change it. Treat the access token as a secret. Don’t put it in browser page source, client bundles, logs, or public repositories.

ID-JAG header

ID-JAG claims

All required string claims must be non-empty strings.

Lifetime and clock skew

Notion allows 60 seconds of clock skew. With that allowance:
  • iat can be at most 60 seconds in the future.
  • nbf, if present, can be at most 60 seconds in the future.
  • exp minus iat can be at most 360 seconds (a 300-second limit plus skew). Give ID-JAGs a lifetime of 5 minutes or less.
  • The assertion must not have expired when Notion issues the token. An assertion that expires during the exchange fails with Assertion has expired.

Replay

Treat each ID-JAG as single-use. Notion records each iss and jti pair until at least the assertion’s exp, and rejects a later exchange with the same pair. This check is best-effort: two exchanges of the same assertion sent at the same moment, or through different regions before the record propagates, can both succeed. Don’t rely on it as your only replay defense. Have the IdP use a new jti for every assertion, and have the client get a new ID-JAG for every exchange, including retries.

Signing keys

Notion finds the IdP’s signing keys with OpenID Connect discovery. It doesn’t accept a JWKS URL in configuration.
  1. Notion fetches <issuer>/.well-known/openid-configuration. It appends the path to the issuer, so an issuer with a path, like https://example.okta.com/oauth2/default, works. Redirects aren’t followed.
  2. Notion reads jwks_uri from that document. The jwks_uri must have the same origin as the issuer.
  3. Notion fetches the JWKS. It must be JSON with a keys array, 64 KB or smaller.
Each fetch times out after 10 seconds. Notion sends User-Agent: notion-hosted-mcp on both fetches. If your IdP sits behind a firewall or bot filter that blocks requests by user agent, allow that value. Notion caches the discovery result for 10 minutes and the JWKS for 5 minutes. When an ID-JAG has a kid that isn’t in the cached JWKS, Notion refetches the JWKS once, at most every 30 seconds per issuer. Publish a new key in the JWKS before you start signing with it. For RS256, the key must be an RSA key. For ES256, it must be an EC key on the P-256 curve. If the key has use, key_ops, or alg, they must allow signature verification with the ID-JAG’s alg.

How Notion finds the user

Notion maps the ID-JAG to a user in the workspace bound to the issuer. On the first exchange for an iss and sub pair, Notion looks up the Notion account with the email in the email claim. If it finds one, Notion links that iss and sub pair to the account. After that, sub is what counts. Later exchanges find the user by iss and sub, even if the email changes or is left out. The exchange fails if any of these is true:
  • The iss and sub pair has no link yet and the ID-JAG has no email claim.
  • No Notion account has that email.
  • The email belongs to an account already linked to a different sub from the same issuer.
  • The user isn’t a member of the bound workspace.
  • An admin has denied the user’s enterprise-managed access. Admins can do this with the update enterprise-managed access endpoint.

What the token can do

The access token works with the Notion MCP server at https://mcp.notion.com/mcp. Send it as a bearer token in the Authorization header. It acts as the mapped user in the bound workspace, and it can reach the same content as a connection that the user authorized through the browser flow. The connection shows up in the workspace’s list of MCP client connections, marked as enterprise-managed. Notion checks the issuer binding and the user’s access on every request. If either no longer allows the user, the token stops working before it expires. The first connection for a user records an MCP server connected event. Changes to enterprise-managed settings and to member access are recorded too. See Audit log events and SIEM events.

Errors

Error responses use the OAuth format: a JSON body with error and error_description. Invalid assertion means the ID-JAG itself failed a check. Assertion was not authorized means the ID-JAG passed those checks, but Notion refused it. Common reasons are that Notion couldn’t map it to a workspace member, the user’s access is denied, the ID-JAG has a cnf or authorization_details claim, aud is an array with more than one value, or the client has no redirect URI. Notion keeps these messages generic on purpose. A more specific message would let anyone with a forged or stolen assertion learn which issuers are configured and which users exist. Notion doesn’t say which check failed, and a request that names an untrusted issuer gets the same response as one with a bad signature.

Common causes of invalid_grant

These failures all return Invalid assertion. Check the item in the right column.