All integrations

Notion

NOTION · FILES & DOCS

Connect Notion to your AI app with scoped access.

Let users connect Notion and make supported tools available to your agent. Document the provider’s sharing requirements alongside the permissions enforced by Fabriq.

Connect Notion with Fabriq Developer

Your user consents at Notion. The credential lands in a vault your agent cannot read, and the tool list it gets back is the intersection of that user’s grant and the agent’s own scopes.

provider “notion”
from af_sdk import AgenticFabriq

af = AgenticFabriq()                      # reads AF_APP_ID / AF_APP_SECRET

url = await af.initiate_connection(request, provider="notion")   # consent at Notion
return redirect(url)

async with await af.for_user(request) as session:
    tools = await session.get_tools()     # only what this user granted

Provider names are surfaces, not vendors, and the authoritative list is the capabilities endpoint rather than any page including this one — an unknown name returns 400 with the valid list. Fabriq Developer is free to start and needs no provider paperwork. Start free or read the quickstart.

Ship the Notion integration

Every route, header, field name and error string below is the gateway’s own, and every tool id and scope comes from the generated schema behind the action list further down this page.

Prerequisites

Before the redirect above can do anything, four things have to be in place on the Fabriq side and one or two on the Notion side.

  • A Fabriq Developer app, and its AF_APP_ID and AF_APP_SECRET. They ride on every app-scoped call as X-App-Id and X-App-Secret.
  • A Redirect URL saved on the agent’s Connect & webhooks page. It is checked before anything else, so leaving it empty fails every connect with “B2B2C OAuth callback URL not configured” and hides whatever else is wrong.
  • An external user id of your own, one per real person, created with POST /api/v1/apps/{app_id}/external-users. The call is idempotent and a 409 means the id already exists.
  • Grants on the agent itself. The tool list a user gets back is the intersection of that user’s grant and the agent’s own scopes, so an agent with no overlapping grant sees nothing even after a clean consent.
  • At least one page or database shared with the connection. Notion is explicit that “before a connection can interact with your Notion workspace page(s), the page must be manually shared with the connection”, through the ••• menu at the top right of a page and Add connections.
  • A plan for who does that sharing. Notion documents that after a user authorizes a public connection, only that user can interact or share pages and databases with it, so two people in one workspace can end up with different halves of the same integration.
Create the user
# AF_BASE is your gateway base URL, from the Fabriq Developer console.
export AF_APP_ID=...        # sent as X-App-Id
export AF_APP_SECRET=...    # sent as X-App-Secret

# One external user id per real person. Idempotent; 409 means it exists.
curl -X POST "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"external_user_id": "user_8412"}'

Connect an account

The gateway provider id is notion. Post the initiate, redirect the user to oauth_url, and let them authorize and pick pages in Notion’s own picker. The capabilities the connector declares are read_content, insert_content, update_content and read_user_info.

Sharing is the half of this that happens outside your flow. The consent screen does not grant content access on its own: a connection reaches a page only once someone adds it there, and Notion notes that a connection added to a page can also reach that page’s children.

Start the grant
# 1. Ask the gateway whether this deployment can connect notion at all.
#    This endpoint is the authoritative list, not any page including this one.
curl "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/providers/capabilities" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"
#    Route on oauth || key_connect. The other flags describe or explain.

# 2. Start the grant, then redirect the user to oauth_url.
curl -X POST \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/oauth/notion/initiate" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"connection_id": "default"}'
#    Response fields: oauth_url, state, connection_id, credential_source.

Make one tool call

notion_get_connection_info declares no scope at all and returns the workspace name and the connection status, which makes it the right first call: it separates “the connection is broken” from “nothing has been shared with it yet” before you spend a scope finding out.

Then run notion_search (POST /api/v1/tools/notion/search, read_content, no required arguments). An empty result from a healthy connection is the signature of the sharing step not having happened, not of a failure.

Call one tool
# A user-scoped token, good for 900 seconds. Cache it per user; re-mint
# roughly 60s before expiry, because these tokens cannot be refreshed in place.
TOKEN=$(curl -s -X POST \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/token" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' -d '{}' | jq -r .access_token)

# External-user tokens go to /mcp/external. Never /mcp: both routes answer 200
# for an external token, so the wrong one fails silently.
curl -X POST "$AF_BASE/mcp/external" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
       "params": {"name": "notion_get_connection_info", "arguments": {}}}'

Inspect the result

Scope to action, as the connector declares it: read_content covers notion_search, notion_list_pages, notion_get_page, notion_list_databases, notion_get_database, notion_query_database, notion_get_block and notion_get_block_children. insert_content covers notion_create_page, notion_create_database and notion_append_block_children. update_content covers notion_update_page and notion_archive_page. read_user_info covers notion_list_users and notion_get_user.

Sixteen actions, and no scope in that list overrides Notion’s own sharing model. A user can grant read_content and still see nothing, because the grant and the sharing are two separate acts.

  • tools/list has two gates: the connection has to exist, and the agent’s grants have to intersect the connection’s scopes. Connected with zero tools is a real state, so say “connected but no actions live” rather than showing the integration as on.
  • A listed connection is not a working one. The initiate writes the row before the user ever reaches the provider: status “configured” means the row exists and nothing else does, and it yields zero tools. Key every user-facing state off status == "connected".
  • Do not infer readiness from granted_scopes being non-empty. Some providers legitimately grant none.
  • The callback query parameters — status, tool, connection_id, external_user_id — are not signed. Treat them as a UI hint, resolve the user from your own session, and confirm by re-reading the connections list. The signed channel is the connection.completed webhook.
Read the connection back
# What this user can actually do right now.
curl -X POST "$AF_BASE/mcp/external" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'

# And the connection row behind it. Only status "connected" is usable.
curl "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/connections" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"

# Shape of a connection row, using the field names the gateway documents:
# [
#   {
#     "tool": "notion",
#     "connection_id": "default",
#     "status": "connected",
#     "granted_scopes": ["read_content"]
#   }
# ]

The payload above shows the shape of a response using the field names the gateway documents. It is illustrative: it was not captured from a live Notion account, so treat the values as placeholders and read your own response.

Disconnect

Deleting the connection is one app-scoped call, and it is the only way to take Notion away from an agent without taking the agent down.

  • The delete removes the stored credential and the connection row on the Fabriq side, so the user has to authorize again to reconnect.
  • Whether the authorization is still listed in the provider’s own connected-apps screen is the provider’s state, not Fabriq’s. If your flow needs the grant gone there too, send the user to the provider and check.
  • Drop any cached user token when you disconnect, and on a connection.needs_reauth webhook.
Delete the connection
# ?tool= is required and a 422 is what you get without it: connection_id
# defaults to "default" for every provider, so a user with two providers
# connected has two rows sharing that id and the delete would be ambiguous.
curl -X DELETE \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/connections/default?tool=notion" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"

Troubleshooting

  • notion_search returns nothing after a clean connect: nobody has shared a page with the connection yet. Notion requires the page to be shared manually through Add connections, and there is no error that reads as “you were never given this”.
  • Coverage differs between two users in the same workspace: with a public connection the authorization and the sharing are both per user, so what the agent can reach depends on who onboarded it.
  • A page appears but its sub-pages do not, or the reverse: a connection added to a page can access that page’s children, so where in the tree the connection was added decides what the agent sees.
  • notion_archive_page is a soft delete of a page the user shared, and notion_update_page modifies an existing page in place. Both sit behind update_content, which is the grant worth naming explicitly on your consent copy.
  • Consent completed but tools/list comes back empty: diff the authorize URL’s scope= parameter against the connection row’s granted_scopes before blaming the console. The URL tells you what was actually requested, and a scope never requested can never be granted.
  • Every connect fails with “B2B2C OAuth callback URL not configured”: the Redirect URL on the agent’s Connect & webhooks page is empty. It is checked first, so it masks every other misconfiguration.
  • Calls return 200 and nothing happens: an external-user token was posted to /mcp instead of /mcp/external. Both answer 200, so this one fails silently — pin the route with a test.
  • An unknown provider name returns 400 with the list of valid names. Read that error; it is accurate. Provider names are surfaces rather than vendors.

Notion-side requirements checked against Notion authorization (pages must be shared with a connection) (2026-10-29), Notion integration capabilities (2026-10-29), Notion request limits (2026-09-29).

Connecting Notion in practice

Notion’s API cannot find anything nobody handed it: a page or database has to be shared with the connection by hand before it exists to the API, so "search my workspace" returns only what someone explicitly added. Throughput is 180 requests per minute on most plans and 600 on Business and Enterprise, and any array of blocks or rich text is capped at 100 elements per request.

Scopes that draw scrutiny

  • User information with email addresses The widest of the three user capability tiers. The alternatives are "User information without email addresses", which returns name and profile image, and "No user information".
  • Update content Modifies existing pages rather than appending, so an agent holding it can rewrite a document in place with no version the API exposes.
  • Insert content Creates new content in the shared pages, which is how an agent publishes into a workspace others read as authoritative.
  • Read comments Comments are a separate capability from content, and they carry the discussion a page deliberately leaves out.

Rate limits

  • 180 requests per minute, an average of 3 per second, on all plans other than Business and Enterprise.
  • 600 requests per minute, an average of 10 per second, on Business and Enterprise.
  • Payload caps: 1,000 block elements and 500 KB overall per request; 100 elements in any array of block types or rich text objects; 100 related pages and 100 users per request.
  • Field caps: 2,000 characters of text content, 2,000 characters for any URL, 1,000 for an equation expression, 200 for an email address.

Who has to approve

Internal connections can only be created by a workspace owner in the developer portal, and on Business and Enterprise plans creating a personal access token is restricted by default until a workspace owner enables it in Settings → Connections. For a public connection each member authorizes separately, and only that user can share pages and databases with it.

Worth knowing

  • Explicit sharing is the permission model: until someone uses "Add connections" on a page, the integration gets nothing, and there is no error that reads as "you were never given this".
  • With a public connection the sharing is per user, so two people in the same workspace see different halves of it and the agent’s coverage depends on who onboarded.
  • The 100-element array cap means a long page is many requests, which interacts badly with 3 requests per second on non-Business plans.
  • Notion returns 429 with a Retry-After header and asks that you respect it with exponential backoff and jitter rather than assuming a fixed delay.

Checked against Request limits (2026-09-29), Integration capabilities (2026-09-29), Authorization (2026-09-29), Create a Notion integration (2026-09-29)

Acts as the person, not as itself

Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.

Credentials never touch the agent

Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.

Every call on the record

Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.

What an agent can do

Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.

notion_append_block_childrenWRITE

Append new content blocks to a Notion page or block.

insert_content
notion_archive_pageWRITE

Archive a Notion page (soft delete).

update_content
notion_create_databaseWRITE

Create a new Notion database as a child of an existing page.

insert_content
notion_create_pageWRITE

Create a new Notion page. Must specify either parent_page_id (to create as child of a page) or database_id (to create as entry in a database).

insert_content
notion_get_blockREAD

Get a specific Notion block by ID.

read_content
notion_get_block_childrenREAD

Get children blocks of a Notion block or page.

read_content
notion_get_connection_infoREAD

Get Notion connection information including workspace name and connection status.

no provider scope
notion_get_databaseREAD

Get a specific Notion database by ID. Returns database schema and properties.

read_content
notion_get_pageREAD

Get a specific Notion page by ID. Returns page properties and metadata.

read_content
notion_get_userREAD

Get information about a specific Notion user.

read_user_info
notion_list_databasesREAD

List Notion databases the integration has access to.

read_content
notion_list_pagesREAD

List recent Notion pages the integration has access to.

read_content
notion_list_usersREAD

List users in the Notion workspace.

read_user_info
notion_query_databaseREAD

Query a Notion database with optional filters and sorts. Returns matching pages/rows.

read_content
notion_searchREAD

Search Notion workspace for pages and databases matching a query.

read_content
notion_update_pageWRITE

Update properties of an existing Notion page.

update_content

Put Notion behind one governed endpoint.

Same permissions, same audit trail, whatever else you connect next.