Guide to Notion Webhooks: Features and Best Practices
Notion has become the go-to workspace for teams managing everything from product roadmaps and wikis to project databases and company knowledge bases. With millions of users relying on it as a central hub, the ability to react to changes in real-time is critical for keeping external systems in sync, triggering automations, and building integrations that feel seamless.
Notion's integration webhooks enable exactly this. Instead of repeatedly polling the Notion API to check if anything has changed, Notion will tell you the moment something important happens, sending secure HTTP POST requests to your endpoint whenever pages, databases, or comments change.
This guide covers everything you need to know about Notion webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.
What are Notion webhooks?
Notion webhooks are HTTP callbacks that deliver event notifications to external endpoints whenever content changes within a Notion workspace. When a page is created, a database schema is modified, or a comment is added, Notion sends a JSON payload containing event metadata to URLs you configure. This enables integration with project management tools, CRMs, custom automation pipelines, and any service that can receive HTTP requests.
Webhooks in Notion exist in two distinct contexts:
- Integration webhooks — The primary webhook system for developers building integrations with the Notion API. These are configured through your integration settings and deliver events for pages, databases, data sources, and comments.
- Webhook actions — A no-code feature available in Notion buttons, database buttons, and database automations that sends HTTP POST requests to a specified URL. These are designed for simpler automation workflows and are available on paid plans.
This guide focuses primarily on integration webhooks, as they're the most flexible and commonly used webhook integration for developers.
Notion webhook features
| Feature | Details |
|---|---|
| Webhook configuration | Integration settings UI |
| Hashing algorithm | HMAC-SHA256 |
| Signature header | X-Notion-Signature |
| Retry logic | Up to 8 retries with exponential backoff over ~24 hours |
| Delivery guarantee | At-most-once |
| Event delivery timing | Most events within 1 minute; all within 5 minutes |
| Event aggregation | High-frequency events batched within short time windows |
| Payload style | Sparse (IDs and metadata only; full content requires API follow-up) |
| Authentication | HMAC-SHA256 signature verification via verification_token |
| Manual retry | Not available |
| Browsable log | Not available |
| SSL required | Yes — endpoints must be HTTPS and publicly accessible |
Supported event types
Notion webhooks notify you about changes to pages, databases, data sources, and comments. Each event represents a meaningful change to content in a workspace. The events themselves do not contain the full content that changed — they act as signals, and it's up to your integration to follow up with a call to the Notion API to retrieve the latest content.
| Type | Description | Aggregated? |
|---|---|---|
page.content_updated | Triggered when the content of a page changes — e.g. adding or removing a block. | Yes |
page.created | Triggered when a new page is created. | Yes |
page.deleted | Triggered when a page is moved to the trash. | Yes |
page.locked | Triggered when a page is locked from editing. | No |
page.moved | Triggered when a page is moved to another location. | Yes |
page.properties_updated | Triggered when a page's property is updated. | Yes |
page.undeleted | Triggered when a page is restored from the trash. | Yes |
page.unlocked | Triggered when a page is unlocked. | No |
database.created | Triggered when a new database is created. | Yes |
database.content_updated | Triggered when a database's content is updated. Deprecated in 2025-09-03 API version. | Yes |
database.deleted | Triggered when a database is moved to the trash. | Yes |
database.moved | Triggered when a database is moved to another location. | Yes |
database.schema_updated | Triggered when a database's schema is updated. Deprecated in 2025-09-03 API version. | Yes |
database.undeleted | Triggered when a database is restored from the trash. | Yes |
data_source.content_updated | Triggered when a data source's content is updated. New in 2025-09-03. | Yes |
data_source.created | Triggered when a new data source is created within a database. New in 2025-09-03. | Yes |
data_source.deleted | Triggered when a data source is moved to the trash. New in 2025-09-03. | Yes |
data_source.moved | Triggered when a data source is moved to another database. New in 2025-09-03. | Yes |
data_source.schema_updated | Triggered when a data source's schema is updated. New in 2025-09-03. | Yes |
data_source.undeleted | Triggered when a data source is restored from the trash. New in 2025-09-03. | Yes |
comment.created | Triggered when a new comment or suggested edit is added. | No |
comment.deleted | Triggered when a comment is deleted. | No |
comment.updated | Triggered when a comment is edited. | No |
Note: More event types may be added in the future. Your subscription won't update automatically to receive new event types, you need to update your subscription in your integration's Webhooks tab.
Webhook payload structure
Notion webhooks use sparse payloads. Rather than including the full content of what changed, each event delivers metadata (the event type, entity ID, timestamp, and contextual information) and expects your integration to call the Notion API to fetch the actual data. This design keeps payloads lightweight and avoids issues with stale data.
Here's an example page.content_updated payload:
{
"id": "56c3e00c-4f0c-4566-9676-4b058a50a03d",
"timestamp": "2024-12-05T19:49:36.997Z",
"workspace_id": "13950b26-c203-4f3b-b97d-93ec06319565",
"workspace_name": "Quantify Labs",
"subscription_id": "29d75c0d-5546-4414-8459-7b7a92f1fc4b",
"integration_id": "0ef2e755-4912-8096-91c1-00376a88a5ca",
"type": "page.content_updated",
"authors": [
{
"id": "c7c11cca-1d73-471d-9b6e-bdef51470190",
"type": "person"
}
],
"attempt_number": 1,
"entity": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
},
"data": {
"updated_blocks": [
{
"id": "153104cd-477e-80ec-a87d-f7ff0236d35c",
"type": "block"
}
],
"parent": {
"id": "0ef104cd-477e-80e1-8571-cfd10e92339a",
"type": "page"
}
}
}
Key payload fields
| Field | Description |
|---|---|
id | Unique ID of the webhook event. |
timestamp | ISO 8601 time at which the event occurred. Use this to order events on your side. |
workspace_id | The workspace where the event originated. |
subscription_id | The ID of the webhook subscription that triggered the delivery. |
integration_id | The associated integration ID. |
type | The event type, e.g. page.created, comment.deleted. |
authors | Array of objects identifying who performed the action. Each has an id and type (person, bot, or agent). Can be more than one for aggregated events. |
accessible_by | Array of bot/user objects with access to the entity. Only present for public integrations. |
attempt_number | The current delivery attempt (1–8). |
entity | The object that triggered the event, with id and type (page, block, database, data_source, or comment). |
data | Additional event-specific data, such as parent, updated_blocks, or updated_properties. |
Security with HMAC signatures
Notion supports HMAC-SHA256 signatures to verify webhook authenticity. When your webhook subscription is created, Notion sends a one-time verification_token to your endpoint. This token serves double duty: it confirms your endpoint is reachable during setup, and it becomes the signing secret for all subsequent webhook deliveries.
Every webhook request from Notion includes an X-Notion-Signature header containing the HMAC-SHA256 hash of the request body, signed with your verification_token.
While payload validation is optional it is strongly recommended for any production environment.
Setting up Notion webhooks
Step 1 — Create a webhook subscription
- Visit your integration settings.
- Either create a new integration or select an existing one.
- Navigate to the Webhooks tab and click + Create a subscription.
- Enter your public Webhook URL — it must be a secure (SSL) and publicly accessible endpoint. Localhost endpoints are not reachable.
- Choose which event types you'd like to subscribe to. You can modify these later.
- Click Create subscription.
At this point, your webhook is created but not yet verified.
Step 2 — Verify the subscription
When you create a subscription, Notion sends a one-time POST request to your webhook URL containing a verification_token:
{
"verification_token": "secret_tMrlL1qK5vuQAh1b6cZGhFChZTSYJlce98V0pYn7yBl"
}
To complete verification:
- Extract the
verification_tokenfrom the incoming request payload at your endpoint. - Store this token securely — you'll need it for signature validation.
- Go back to the Webhooks tab in your integration settings and click Verify.
- Paste the
verification_tokenvalue and click Verify subscription.
Once submitted, your webhook subscription becomes active and will start receiving events.
You can only change the webhook URL before verification. After verification, if you need to change the URL, you must delete and recreate the subscription. You can change the subscribed event types at any time.
Step 3 — Validate event payloads (recommended)
Once your subscription is active, implement signature validation for incoming events.
Best practices when working with Notion webhooks
Verifying HMAC signatures
When processing webhooks from Notion, verify the HMAC signature to ensure requests genuinely originated from Notion and haven't been tampered with.
Node.js
import { createHmac, timingSafeEqual } from "crypto";
const VERIFICATION_TOKEN = process.env.NOTION_VERIFICATION_TOKEN;
function verifyNotionSignature(rawBody, signatureHeader) {
const calculatedSignature = `sha256=${createHmac("sha256", VERIFICATION_TOKEN)
.update(rawBody)
.digest("hex")}`;
return timingSafeEqual(
Buffer.from(calculatedSignature),
Buffer.from(signatureHeader)
);
}
app.post("/webhooks/notion", (req, res) => {
const signature = req.headers["x-notion-signature"];
if (!verifyNotionSignature(req.rawBody, signature)) {
return res.status(401).send("Invalid signature");
}
// Process webhook
res.status(200).send("OK");
});
Python
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
def verify_notion_signature(payload, signature):
secret = os.environ["NOTION_VERIFICATION_TOKEN"]
computed = "sha256=" + hmac.new(
secret.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
@app.route("/webhooks/notion", methods=["POST"])
def handle_notion_webhook():
signature = request.headers.get("X-Notion-Signature")
if not verify_notion_signature(request.get_data(as_text=True), signature):
abort(401)
# Process webhook
return "OK", 200
Fetch full content after receiving events
Because Notion uses sparse payloads, your webhook handler should treat each event as a trigger to fetch the latest data from the API. Don't rely on the webhook payload alone for content — use the entity.id to retrieve the full page, database, or comment.
Decouple webhook processing to avoid missed retries
Notion retries failed deliveries up to 8 times over approximately 24 hours. If your endpoint takes too long to respond, Notion may consider the delivery failed. Process webhooks asynchronously to ensure you return a 200 response quickly.
Use the event timestamp for ordering
Notion warns that events may arrive in a different order than they occurred. If event ordering is critical for your workflows, use the event's timestamp field to reorder them rather than relying on delivery order:
async function processEventBatch(events) {
const sorted = events.sort(
(a, b) => new Date(a.timestamp) - new Date(b.timestamp)
);
for (const event of sorted) {
await processEvent(event);
}
}
Implement idempotent processing
Although Notion aims for at-most-once delivery, it's good practice to handle potential duplicates, especially during retries. Use the event id as an idempotency key:
async function processNotionEvent(event) {
const idempotencyKey = event.id;
const exists = await redis.get(`processed:${idempotencyKey}`);
if (exists) {
console.log(`Event ${idempotencyKey} already processed, skipping`);
return;
}
await handleEvent(event);
// Mark as processed with a 48-hour TTL
await redis.setex(`processed:${idempotencyKey}`, 172800, "1");
}
Handle aggregated events correctly
Many Notion events are aggregated — multiple rapid changes are batched into a single webhook delivery. For page.content_updated events, the data.updated_blocks array may contain multiple block IDs. For page.properties_updated, the data.updated_properties array lists the property IDs that changed. Always iterate over these arrays rather than assuming a single change.
Notion webhook limitations and pain points
Sparse payloads require extra API calls
The Problem: Notion webhook payloads contain only event metadata (entity IDs, timestamps, and event types) not the actual content that changed. To get the full page content, property values, or comment text, your integration must make follow-up API calls after every webhook event.
Why It Happens: Notion's sparse payload design keeps webhook deliveries lightweight and avoids sending stale or incomplete data. By the time your endpoint processes the event, the content may have changed again, so Notion expects you to fetch the latest state directly.
Workarounds:
- Build your webhook handler to immediately queue a fetch for the relevant entity using the Notion API.
- Cache frequently accessed pages and databases to reduce redundant API calls.
- Batch multiple events for the same entity before making API calls, since aggregated events may already group rapid changes.
How Hookdeck Can Help: Hookdeck's transformations can enrich sparse payloads by fetching additional data from the Notion API before forwarding the event to your endpoint, reducing the burden on your handler and eliminating the need to build custom enrichment logic.
Event aggregation causes delays and can swallow events
The Problem: Notion batches high-frequency events like page.content_updated within short time windows, delivering them as a single webhook event. While this reduces noise, it introduces a delivery delay (typically under one minute but up to five minutes). Worse, if events like page.created, page.deleted, and page.undeleted occur in quick succession, Notion may only deliver the most meaningful result event (or none at all if the state returns to its original one).
Why It Happens: Event aggregation is a deliberate design choice to reduce redundant webhook deliveries and improve system reliability. It helps prevent webhook storms from rapid edits (like a user typing in a document), but the trade-off is reduced granularity and potential data loss for short-lived state changes.
Workarounds:
- Don't rely on webhooks for tracking every individual edit — use them as signals that something changed, then fetch the latest state.
- For use cases requiring detailed change tracking, supplement webhooks with periodic polling as a safety net.
- Use non-aggregated events like
comment.createdorpage.lockedwhen you need immediate, ungrouped delivery.
How Hookdeck Can Help: Hookdeck can serve as a buffer between Notion and your endpoint, queuing events reliably even if they arrive with delays or in unexpected batches. Hookdeck's event log gives you full visibility into what was delivered and when, making it easy to audit whether events were aggregated or dropped.
No webhook delivery logs or visibility
The Problem: Notion provides no built-in dashboard or logs showing webhook delivery status. You can't see which events were sent, whether they succeeded or failed, what the response codes were, or when retries occurred. The only feedback you get is if a webhook action in an automation fails then you'll see an exclamation mark, and the automation pauses.
Why It Happens: Notion's webhook system is still relatively new, and delivery observability features haven't been built into the platform yet. The integration settings UI shows subscription status but not individual delivery history.
Workarounds:
- Implement comprehensive logging in your webhook handler, capturing every incoming event with its
attempt_number,id, andtimestamp. - Set up monitoring and alerting on your endpoint to detect delivery gaps.
- Use a request inspection tool during development to visualize incoming payloads.
How Hookdeck Can Help: Hookdeck's dashboard provides complete visibility into every webhook delivery, including status codes, latency, payloads, headers, and retry attempts. You can search, filter, and inspect events without building any custom logging infrastructure.
No built-in dead letter queue
The Problem: If your endpoint fails to acknowledge an event after all 8 retry attempts (spanning approximately 24 hours), the event is permanently lost. Notion does not maintain a record of failed deliveries for later inspection or replay.
Why It Happens: Notion's at-most-once delivery model prioritizes simplicity over guaranteed delivery. The retry mechanism with exponential backoff handles transient failures, but persistent endpoint issues result in permanent event loss.
Workarounds:
- Ensure your webhook endpoint has high availability and returns 200 responses quickly.
- Implement your own logging of received events so you can identify gaps through reconciliation.
- Use a message queue (like Hookdeck, SQS, RabbitMQ, or Redis) between your endpoint and your processing logic as a buffer.
- Periodically poll the Notion API as a backup to catch events that may have been missed.
How Hookdeck Can Help: Hookdeck automatically preserves all failed webhooks, allowing you to inspect, debug, and replay them once issues are resolved. This acts as a built-in dead letter queue, ensuring no events are permanently lost due to endpoint downtime.
Webhook URL cannot be changed after verification
The Problem: Once your webhook subscription is verified, you cannot update the endpoint URL. If you need to change the URL (for example when migrating to a new server, changing environments, or updating your routing) you must delete the subscription entirely and recreate it from scratch, including going through verification again.
Why It Happens: The URL is locked after verification as a security measure, tying the verified endpoint to the subscription. However, this creates friction for common operational scenarios.
Workarounds:
- Plan your URL structure carefully before creating subscriptions.
- Use a reverse proxy or load balancer URL that you control, so you can change the backend without modifying the webhook URL.
- Automate subscription teardown and recreation if you need to change URLs frequently.
How Hookdeck Can Help: By pointing your Notion webhook at a stable Hookdeck connection URL, you can change your actual destination endpoint at any time through Hookdeck's dashboard without touching your Notion subscription. This decouples your Notion configuration from your infrastructure routing.
No workspace-level webhook subscriptions
The Problem: Notion webhooks require your integration to have explicit access to each page or database it wants to monitor. You can't set up a single subscription that automatically receives events for everything in a workspace. If a new page is created inside a private page your integration doesn't have access to, the event won't be triggered.
Why It Happens: Notion's permission model is granular so integrations must be explicitly added to pages and databases. This is a security feature, but it means webhook coverage is limited to content your integration has been invited to.
Workarounds:
- Educate workspace users to add your integration to new pages and databases as they create them.
- Use the Notion API to periodically search for content your integration has access to and verify coverage.
- For workspace-wide monitoring, consider using Notion's audit log (available on Enterprise plans) for user and settings changes that webhooks don't cover.
How Hookdeck Can Help: While Hookdeck can't change Notion's permission model, it can help you manage and route events from multiple subscriptions across different pages and databases through a single, organized destination, simplifying the operational overhead of managing many subscriptions.
API rate limits affect follow-up calls
The Problem: Because Notion webhooks use sparse payloads, your integration needs to make follow-up API calls to fetch the full content. Notion's API rate limit is an average of three requests per second per integration. During high-activity periods (when many webhook events arrive in quick succession) your follow-up API calls can easily hit rate limits, resulting in HTTP 429 errors and failed data fetches.
Why It Happens: The sparse payload design combined with the relatively low rate limit creates a bottleneck. Each webhook event typically requires at least one API call (often more) to fetch the data you actually need, and the rate limit doesn't distinguish between webhook-triggered calls and other API usage.
Workarounds:
- Implement request queuing with rate limiting in your webhook handler.
- Batch and deduplicate events for the same entity before making API calls.
- Use exponential backoff when you receive 429 responses.
- Cache API responses to reduce redundant calls for frequently updated entities.
How Hookdeck Can Help: Hookdeck's rate limiting and queuing capabilities can throttle event delivery to your endpoint, ensuring your handler processes events at a pace that respects Notion's API rate limits. Combined with transformations for payload enrichment, you can reduce the number of follow-up API calls needed.
No support for user or workspace-level events
The Problem: Notion webhooks do not support notifications for user changes (including workspace membership changes, email or name updates, and permission modifications) or workspace and teamspace settings changes. If you need to track who joined the workspace, which permissions changed, or how team settings were modified, webhooks can't help.
Why It Happens: The current webhook system is focused on content events — pages, databases, and comments. User and workspace management events are considered a separate domain and aren't yet part of the webhook event model.
Workarounds:
- Use Notion's audit log (available on Enterprise plans) for user and workspace changes.
- Poll the Notion Users API periodically to detect membership changes.
- Build manual notification workflows for permission changes using Notion's UI automations.
How Hookdeck Can Help: Hookdeck can integrate with multiple event sources. If you build a polling-based system that checks for user or workspace changes, Hookdeck can normalize those polled events into the same webhook pipeline as your real-time Notion events, giving you a unified view across both event types.
Limited to five webhook actions per automation
The Problem: When using Notion's no-code webhook actions (in buttons and database automations), you're limited to a maximum of five webhook actions per automation, and they only support POST requests.
Why It Happens: This is a product limitation of Notion's automation builder, designed for simpler workflows rather than complex multi-destination routing.
Workarounds:
- Use integration webhooks instead of webhook actions for more flexibility.
- Build a fan-out service that receives a single webhook and distributes it to multiple destinations.
- Combine multiple actions into a single endpoint that handles routing internally.
How Hookdeck Can Help: Hookdeck's fan-out capabilities can take a single webhook from Notion and route it to multiple destinations, eliminating the five-action limit. You can also transform the payload differently for each destination.
Testing Notion webhooks
Use the built-in test scenarios
Notion provides three recommended test scenarios after setting up your subscription:
- Change a page title — Tests aggregated event delivery. Change the title of a page your integration has access to, wait a minute or two, and verify your endpoint receives a
page.content_updatedevent. - Add a comment — Tests instant event delivery. Add a comment to an accessible page and verify your endpoint receives a
comment.createdevent within seconds. (Requires the "comment read" capability.) - Modify a database schema — Tests structural change events. Add, rename, or delete a property in a connected database and verify your endpoint receives a
data_source.schema_updated(ordatabase.schema_updatedon older API versions) event.
Use a request inspector
Before building your handler, inspect real Notion payloads using a request inspection service like Hookdeck Console:
- Create a temporary inspection URL.
- Configure it as your webhook endpoint in your Notion integration.
- Trigger real events by making changes in your workspace.
- Inspect the payload structure, headers, and timing.
This helps you understand the exact shape of events before writing any processing logic.
Validate in staging
Test your webhook integration with realistic scenarios:
- Single page creation and deletion.
- Rapid sequential edits to test aggregation behavior.
- Multiple property changes on a database page.
- Comment threads with replies.
- Schema changes (adding, renaming, and deleting properties).
- Simulated endpoint downtime to verify retry behavior.
Conclusion
Notion webhooks provide a solid foundation for building real-time integrations with one of the most widely used workspace tools. The event system covers the core content lifecycle (page and database CRUD operations, schema changes, and comments) and the HMAC-SHA256 signature verification adds a strong security layer.
However, the sparse payload design, event aggregation behavior, lack of delivery logs, and absence of a dead letter queue mean production deployments require careful consideration. Implementing proper signature verification, asynchronous processing, and idempotent event handling will address the most common issues.
For integrations with moderate event volumes and reliable endpoints, Notion's built-in webhook system combined with proper error handling works well. For higher-volume use cases, complex multi-destination routing, or mission-critical workflows where delivery guarantees matter, webhook infrastructure like Hookdeck can address Notion's limitations by providing payload enrichment, configurable retries, automatic dead letter queuing, and comprehensive delivery monitoring without modifying your Notion configuration.