The server
notion_server.py
uses_scopes=False because the link carries none. token_request_format and
client_secret_basic are the JSON body and Basic auth Notion’s token endpoint
takes. revocation is how a user is disconnected: Disconnecting.
Connecting a user
notion_users.py
Capabilities
Notion’s permissions are the integration’s capabilities, ticked where you register it and never sent on a request. Each tool declares the ones it uses, spelled as Notion’s console spells them, soscopes_for is the list to tick:
notion_capabilities.py
restricted_resource, which names
none, so InsufficientScope reports the
tool’s own. A page the user did not share answers 404 instead: that is which pages
the integration may see, not what it may do.
Refresh behaviour
Notion’s token response carries arefresh_token and no expires_in, and its
docs don’t say how long an access token lasts or what a refresh does to the
tokens before it. Measured against a live connection: every refresh rotates the
refresh token, and the access token before it stops working about three
seconds later. That is why the loader returns the whole grant. A worker holding
only its own copy of the refresh token presents one another worker has spent,
which Notion refuses once the newer grant has served a call, and a worker that
refreshes on its own retires the token
the others are still using. For the same reason, more than one process passes
refresh_lock too: A lock around the refresh.
With no lifetime to go on, the client keeps a token until the API refuses it,
and that one call fails before the next refreshes.
Disconnecting
TheSubjectProvider the pack is configured with builds the user’s client from
your store and revokes the grant, in one call:
notion_disconnect.py
v1/oauth/revoke. Measured live, a live token ends the grant: the access
token is refused within a second and the refresh token is dead.
Notion answers 200 to any token, though: one already revoked, one a refresh
has retired, a refresh token, or a string it never issued, none of which end
anything. RFC 7009 asks exactly that of a server, so Notion is right by the
standard, but it means revoke() returns True either way, and only the token
sent decides whether the user was disconnected. OAuth2Client.revoke() sends
the access token your store holds, under refresh_lock, so it is the live one.
A stale token passed to revoke_token reports
True and leaves the grant alive.
Where the rest is
- Register a public Notion integration — the screens, the redirect URL, the capabilities
- Getting the grant — the route, the session,
state, PKCE - Connect your users’ accounts — every pack’s providers, side by side
- Packs — the Notion tools