VERIFIED 2026-05-07 / NOTION DEVELOPER REFERENCE

Webhooks and Notion integration: five outgoing events, zero incoming, and the case where you should not be using a webhook

Most guides on this topic are URL setup tutorials. They skip the fact that Notion's whole webhook surface is five event types pointing OUT, that page.content_updated is batched, that there is no incoming webhook, and that a non-trivial slice of people typing this into Google actually wanted to fire Notion from a local computer event the platform cannot see. Below is the verified payload, the verified event list, and an honest map of when a webhook is the right tool.

M
Matthew Diakonov
10 min read
Direct answer (verified 2026-05-07)

How do webhooks work with Notion?

Notion supports outgoing webhooks only. Five event types fire from Notion to a URL you control:page.created, page.content_updated, page.locked, data_source.schema_updated, and comment.created. There is also a no-code "Send webhook" button action that POSTs database page properties out, capped at five actions per automation.

There is no incoming webhook. You cannot push data into Notion via a webhook URL. To create or update a page from outside Notion you call the REST API at api.notion.com, or you route through a third-party platform (Make, Zapier, n8n) that wraps the same REST calls behind a webhook receiver they host.

Source: developers.notion.com/reference/webhooks and notion.com/help/webhook-actions.

Every event Notion will actually send you

Most "set up Notion webhooks" guides spend the first half on buttons inside the Notion UI. Skip that. The thing that decides whether webhooks solve your problem at all is the list of events Notion is willing to emit. As of the 2025-09-03 API version that list is short.

page.created

A new page lands in your workspace and your integration has access to it. Body data is empty, the entity field has the page id. Fetch the page via the REST API to read it.

page.content_updated

Page body changed. Batched: ten edits inside the aggregation window arrive as one event. Use this as a "go re-fetch" signal, not as an audit log of edits.

page.locked

A page was marked read-only. Useful for "freeze on publish" workflows and for compliance signals. Single event, no batching.

data_source.schema_updated

The 2025-09-03 replacement for the old database.schema_updated. Fires when database columns are added, removed, or retyped. Schema changes are rare so the event is rare too.

comment.created

Someone left a comment on a page or block your integration has access to. Requires the comment-read capability on the integration. The event is the only practical way to run a "watch a page for review feedback" loop without polling.

The reference page also documents page.deleted-style events and a few sub-types per the per-event pages. Treat the five above as the predictable subscription surface that every connection can rely on. If you need the exact, latest event list, the canonical place is developers.notion.com/reference/webhooks.

The actual payload, no guessing

Two payloads matter. The verification payload (sent once when you register the URL) and the event payload (sent forever after). Both are flat. The event payload tells you what happened and where, not what changed.

1. Verification payload

You paste the value of verification_token back into the Notion connection settings. The subscription is inactive until you do.

POST /your/webhook (one-time, on subscription create)

2. Event payload

The Notion docs are explicit about what this is and is not: "the webhook acts as a signal that something changed, and it is up to your connection to follow up with a call to the Notion API to retrieve the latest content." You receive a poke. You go fetch.

POST /your/webhook (every real event after verification)
5

The total number of distinct event types Notion will subscribe you to. Every other 'integration capability' is hung off these five hooks plus the REST API.

developers.notion.com/reference/webhooks, verified 2026-05-07

Setting up an outgoing webhook, the short version

The Notion docs spend a lot of words on this. The actual required steps are five.

1

Stand up a public HTTPS endpoint

Localhost is unreachable. Use Cloudflare Tunnel, ngrok, or a deployed staging URL. Your handler must respond 200 to a POST. Anything else and Notion will retry up to 8 times (attempt_number on the payload counts the retries).

2

Open the integration's Webhooks tab and create a subscription

Notion settings, Connections, your integration, Webhooks tab, Create a subscription. Paste your URL. Choose which event types you want.

3

Capture the verification_token from the first POST

Notion will POST one body of the form { verification_token: 'secret_...' }. Log it. Display it. Whatever it takes to grab the value.

4

Paste verification_token back into the Notion form

The subscription stays inactive until you confirm. Once you do, the events flow.

5

Verify the cryptographic signature on every event

Notion signs every payload with the secret tied to your subscription. Your handler should reject any POST whose signature does not verify against the body. Anyone who knows your URL can otherwise spam your endpoint.

Webhook vs REST API vs desktop agent: pick by where the trigger lives

The right tool depends on which side has the trigger. If the trigger is in Notion, a webhook can carry it out. If the trigger is in another cloud service, you need a webhook on one end plus a REST call on the other. If the trigger is on your computer, neither helps you, because nothing can fire a webhook on Notion's behalf for "the user just saved a file in Finder."

FeatureWebhook + RESTDesktop agent (accessibility tree)
Trigger lives in NotionSubscribe to page.created or page.content_updated, fetch page via APIPossible but overkill, no reason to leave the cloud path
Trigger lives in another cloud (Slack, Stripe, GitHub)That cloud sends a webhook, your handler calls Notion REST API to writePossible but overkill, the cloud-to-cloud path is shorter
Trigger lives on your Mac (file save, calendar event, app state)No path, the local event has no webhook senderWatch the local event, operate Notion's desktop app via macOS accessibility APIs
Action is to update NotionCall the REST API at api.notion.comClick and type in the Notion app via AXUIElement, no integration token needed
Action lives outside Notion (open a file, run a script, drag in Figma)Notion sends webhook out, your service does the workSkip the network round trip entirely, do it on the Mac directly
Latency on a known content changeSubject to page.content_updated batch delay (Notion's wording: 'aggregated, may not be sent immediately')macOS UI events fire as they happen, no aggregation
Setup costPublic URL, integration, verification, signature verification, retry handlingInstall the desktop agent, grant accessibility permission, write the workflow

None of these are mutually exclusive. The interesting workflows tend to combine two of the three.

Tools that bridge the incoming-webhook gap

Because Notion has no incoming webhook, anyone who wants "send a row to Notion when X happens" without writing code ends up using a third-party platform. The platform exposes a webhook URL, you point your sender at that URL, the platform translates the body into a Notion REST API call. These are the four with first-class Notion modules.

Each of these still calls the same POST /v1/pages or PATCH /v1/pages/{id} REST endpoint under the hood. The choice between them is about hosting (Zapier and Make are SaaS, n8n is self-host, Hookdeck sits in front of your own service) and not about capability.

The case where a webhook is the wrong tool entirely

Some flows that show up in support inboxes for this query. None of them are solvable with a webhook.

  • "When I save a file matching ~/Downloads/invoice-*.pdf, create a row in my Notion ops database with the file name and date." The trigger is the file system. No cloud service can fire a webhook for it.
  • "When the next event in my Calendar app starts, open the relevant Notion page in the desktop app and pin it on my second monitor." The trigger is local clock plus local app state. The action is local UI manipulation.
  • "After a QuickTime recording finishes, transcribe it locally and append a section to the Notion page I have open." The trigger is QuickTime exit. The transcription is local. The Notion update is local.
  • "When the Slack desktop app shows a new DM from a named person, add a Notion task with the DM body." Slack does have outgoing webhooks but only at the workspace level and only for specific event scopes. The desktop UI is often the simpler signal.

For each of these the right primitive is a desktop agent that watches the local event and operates Notion's desktop app through the macOS accessibility tree (the same AXUIElement API VoiceOver uses). No public URL. No verification token. No batch delay. The trade is that the work runs on your Mac while the Mac is awake, instead of in a cloud, and the coupling is to the Notion app's UI rather than to an API contract that has versioning.

Fazm is the variant of that primitive we ship. The MIT-licensed source is at github.com/m13v/fazm, the macOS desktop agent reads UI through the accessibility tree, and you can wire local triggers (file watcher, calendar fired, app state changed) to a Notion-app action chain without ever touching a webhook URL.

Webhook, REST, or desktop agent? We can help you map it.

Bring one workflow you have been trying to wire between Notion and something on your Mac. Fifteen minutes, end with a concrete plan, no slide deck.

FAQ on webhooks and Notion integration

How many event types does Notion's outgoing webhook actually send?

Five. The Notion developer reference lists page.created, page.content_updated, page.locked, data_source.schema_updated, and comment.created. data_source.schema_updated replaces the old database.schema_updated which is deprecated after the 2022-06-28 API version. There are also page.properties_updated and page.deleted style events documented per the event reference, but the canonical core surface that ships in the docs and that every connection can subscribe to is the five named above. If you want a literal lookup: developers.notion.com/reference/webhooks.

Can I send a webhook INTO Notion to create or update a page?

No. Notion has no incoming webhook. The Notion docs describe webhooks as 'real-time updates from Notion. Whenever a page or database changes, Notion sends a secure HTTP POST request to your webhook endpoint.' That is one direction only. To push data into Notion you call the REST API at api.notion.com (POST /v1/pages, PATCH /v1/pages/{id}, etc.) or you go through a third-party platform like Make, Zapier, or n8n that wraps the same REST calls behind a webhook URL they host. Do not confuse Notion's no-code 'Send webhook' button action with an incoming webhook, that one is also outgoing (a button in Notion fires a POST out to a URL you specify).

What is the page.content_updated batch delay?

Notion batches page.content_updated events to reduce 'redundant notifications.' If a user makes ten edits to a page over a minute, you do not get ten webhook deliveries, you get one (or a small number of) aggregated event(s). The exact window is not committed in the docs. The behavioral consequence: do not rely on page.content_updated for anything time-sensitive. If you need to know that a specific block changed within a few seconds, the webhook will not get you there. You either poll the page via the REST API on a tighter cadence or switch to a side-channel signal (file save, button press, calendar event) that fires the action you actually want.

What is in the actual webhook payload Notion sends?

The top-level fields are id (UUID), timestamp (ISO 8601), workspace_id, workspace_name, subscription_id, integration_id, type (e.g., page.created), authors (array of {id, type: person|bot|agent}), accessible_by (array of {id, type: person|bot}), attempt_number (1 to 8 for retries), entity ({id, type: page|block|database|data_source|comment}), and data (event-specific, often empty for page.content_updated). The Notion docs are explicit: 'the webhook acts as a signal that something changed, and it is up to your connection to follow up with a call to the Notion API to retrieve the latest content.' You do not get the new content in the body, you get a poke that says 'go fetch.'

Why does the verification step matter and what does it look like?

When you create a webhook subscription in your integration settings, Notion POSTs a one-time body to your URL containing a single field: verification_token (a string starting with 'secret_'). You paste that token back into the Notion form to prove your endpoint is reachable. Until you do that, the subscription is created but not active and no events flow. Localhost endpoints are not reachable, so you need a public HTTPS URL even during development. ngrok, Cloudflare Tunnel, or a deployed staging endpoint are the usual answers.

Can a Notion button send a webhook OUT to my server?

Yes, this is the 'webhook actions' feature. In a database button or database automation you can add a 'Send webhook' action that POSTs to a URL of your choice. Two limits to remember: only database page properties can be sent (not the rich-text body of the page), and there is a maximum of five webhook actions per automation. This is the no-code path that does not require building a Notion integration. Pair it with an incoming-webhook receiver on the other side and you have a one-direction trigger pipeline.

When is a Notion webhook the wrong tool entirely?

When the trigger or the action lives on your computer, not in Notion or in another cloud service. Cases: you want a Notion task to be created when you save a file in a specific folder; you want Notion to update when a meeting actually starts on your local Calendar app and not just when an event is created in the cloud; you want a row to flip status when you finish a recording in QuickTime; you want to move text from a PDF you opened in Preview into a Notion page. None of these have a sender that can fire a webhook. They all have a local Mac event. The right primitive is a desktop agent that watches the local event and operates Notion's desktop app through the macOS accessibility tree. No public URL, no verification token, no batch delay. The price is that the operation runs on your Mac, not in a cloud.

How does Fazm fit into the Notion webhook story?

Fazm is a macOS computer-use agent. It does not replace Notion's REST API and it does not replace a third-party automation platform. It does a thing those tools cannot do: when the trigger or action is local, it acts on Notion's desktop app directly through the AXUIElement tree. Concretely, Fazm reads buttons, menus, text fields, and tables in the Notion app the same way macOS VoiceOver does, by accessibility label and role. So 'when I save a file matching pattern X, append a row to my Notion ops database' is a single Fazm flow, no webhook, no integration token, no public URL. For everything else (Slack to Notion, Stripe to Notion, GitHub issue to Notion page) you keep using webhooks plus the REST API or a Make/Zapier scenario, and the answer in this guide stays the same.

If I want a webhook bridge between Notion and Slack, what is the cleanest path?

If both ends are cloud services, you do not need to host anything. The shortest path is a Make or Zapier or n8n scenario with a 'Notion: page in database created/updated' trigger and a 'Slack: send message' action. Notion sends the outgoing webhook (or the platform polls), the platform translates, the platform calls Slack's API. If you want more control or you are passing PII you would rather not park inside a SaaS, run an n8n instance you host yourself, point Notion's webhook at it, write the Slack call as an HTTP node. If you are a developer and you want zero abstraction, register a webhook subscription in your Notion integration, run a small Node or Go service behind Cloudflare Tunnel, verify with the verification_token, post to Slack's incoming webhook in your handler.