Providers / April 25, 2026

How to receive Notion webhooks (page, database, and comment events)

Notion webhook flow showing verification handshake and event delivery

Notion fires webhooks for content events in workspaces where your integration is installed: page creation and updates, database schema changes, comments, deletions, and more. They're how integrations stay in sync with what's happening in Notion without polling.

This guide covers the one-time verification handshake, what Notion sends, how to verify each event, and what production reliability requires.

What Notion sends

Notion webhooks are JSON POST requests. A page.properties_updated event:

{
  "id": "67890abc-1234-5678-9abc-def012345678",
  "timestamp": "2026-04-25T14:30:00.000Z",
  "workspace_id": "11111111-2222-3333-4444-555555555555",
  "subscription_id": "abcdef12-3456-7890-abcd-ef1234567890",
  "integration_id": "98765432-10ab-cdef-1234-567890abcdef",
  "type": "page.properties_updated",
  "entity": {
    "id": "ffffeeee-dddd-cccc-bbbb-aaaa99998888",
    "type": "page"
  },
  "data": {
    "updated_properties": ["title", "status"]
  }
}

Headers:

X-Notion-Signature: sha256=<hex signature>
Content-Type: application/json

Common Notion event types:

  • page.created, page.properties_updated, page.content_updated, page.moved, page.deleted, page.undeleted
  • database.created, database.schema_updated, database.deleted
  • comment.created, comment.deleted
  • data_source.content_updated (when database row content changes)

The one-time verification handshake

Before Notion will start sending event webhooks to your URL, you have to complete a verification handshake.

When you first add a webhook URL in your integration's settings, Notion POSTs a verification request:

{ "verification_token": "secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }

You then take that verification_token value and paste it into the Notion integration dashboard to confirm. After confirmation, Notion uses that token as the HMAC key for signing future events.

Two implications:

  1. The verification token is your signing secret — store it encrypted.
  2. If you re-issue a webhook URL, you'll redo the handshake and get a new token.

Notion's signature scheme

Each event's X-Notion-Signature header is sha256= plus the hex-encoded HMAC-SHA256 of the raw request body using the verification token as the key:

signature = "sha256=" + hex(hmac_sha256(verification_token, raw_body))

Verification:

  1. Read the raw body before any JSON parsing
  2. Compute HMAC-SHA256(verification_token, raw_body) and hex-encode
  3. Compare constant-time with the value after sha256=

There's no timestamp in the signature header itself, so use the timestamp field in the payload (and the id field for idempotency) to detect stale or duplicate events.

Notion's retry policy

Notion retries failed webhooks (non-2xx, or no response within ~10 seconds) on a backoff schedule. Specific retry counts and timing vary, but the window is finite — past it, the event is marked failed in the integration's webhook logs.

This means:

  • Acknowledge fast — return 2xx in under one second
  • Be idempotent on event id — duplicates will arrive during retries
  • Have a recovery story for events past the retry window — you can manually re-trigger from the Notion dashboard, but only one at a time

DIY: minimal Notion webhook handler in Node

import { createHmac, timingSafeEqual } from 'crypto'

const verificationToken = process.env.NOTION_VERIFICATION_TOKEN!

export async function POST(req: Request) {
  const sig = req.headers.get('x-notion-signature')!
  const body = await req.text()

  // Initial verification handshake — capture the token and ack
  const parsed = JSON.parse(body)
  if ('verification_token' in parsed) {
    // Persist `parsed.verification_token` securely; you'll paste it into
    // the Notion dashboard to complete setup
    return new Response('ok', { status: 200 })
  }

  // Event verification
  const expected =
    'sha256=' +
    createHmac('sha256', verificationToken).update(body).digest('hex')
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response('Bad signature', { status: 400 })
  }

  // Idempotency on parsed.id
  // ...

  queueWork(parsed)

  return new Response('ok', { status: 200 })
}

Production needs more:

  • A persistence layer for event-id idempotency
  • A queue for async work
  • A replay path for events past Notion's retry window
  • Filtering — large workspaces produce huge volumes of low-value events
  • Routing — page.* events to one handler, database.* to another, comment.* to a third

Hooksbase: receive Notion webhooks without rebuilding the rest

Notion isn't one of Hooksbase's five pre-verified provider packs (Stripe, GitHub, Clerk, Slack, Resend), and Hooksbase does not forward Notion's original signature header to your destination. If you need Notion HMAC verification, verify it in a small pre-ingest forwarder, then post the verified raw body to Hooksbase with the bearer ingest secret.

Setup:

  1. Create a webhook in Hooksbase with your application URL as the destination
  2. Add your verification forwarder URL to Notion's webhook settings
  3. Complete the Notion verification handshake in the forwarder
  4. Verify subsequent event signatures in the forwarder, then POST verified events to Hooksbase with Authorization: Bearer <ingest secret>

You get:

  • Acknowledge in milliseconds to Notion regardless of how slow your downstream is
  • Idempotency for your destination — every dispatch includes a unique webhook-id header (Standard Webhooks-compatible) you can dedupe on across retries
  • Retries with exponential backoff to your endpoint after Hooksbase accepts the event
  • Routing rules by event content — send page.* events to one destination, database.* to another
  • Payload transforms — extract just the fields your downstream needs from Notion's nested structure
  • Deterministic replay — re-run a failed delivery with the same payload bytes while the payload is retained
  • Delivery history and DLQ

Common Notion webhook use cases for AI agents

  • Content sync agentpage.content_updated events trigger an agent that re-indexes the page into your search/RAG store
  • Task automation agentpage.properties_updated events on a tasks database trigger an agent that propagates status changes to Slack, GitHub, or your project tracker
  • Documentation review agentpage.created events in a docs database trigger an agent that runs a style and accuracy review and posts comments
  • Comment triage agentcomment.created events trigger an agent that classifies the comment and notifies the right person
  • Database schema sync agentdatabase.schema_updated events trigger an agent that updates downstream consumers (your app's TypeScript types, your data warehouse schema)

Where to go next

Start free at app.hooksbase.com.

Related guides