Notion Integration
Receive and route Notion webhooks for page and database changes, comments and workspace activity.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Notion Production",
"slug": "notion",
"provider": "notion",
"signingSecret": "your-notion-webhook-secret"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/notion2. Configure the Notion Webhook
- Open your integration in the Notion Developer Portal
- Enable webhooks for the integration
- Paste your Hookbase ingest URL as the webhook URL
- Copy the webhook signing secret and store it on your Hookbase source as
signingSecret - Confirm the integration has access to the pages and databases you want to watch — Notion sends events only for content the integration can see
See Notion's webhooks reference for the event catalog and subscription rules.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Docs Indexer", "slug": "notion-indexer", "url": "https://api.myapp.com/webhooks/notion"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Notion to Indexer", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
Notion signs webhooks with HMAC-SHA256 over the raw request body. The hex digest is sent in the
X-Notion-Signature header behind a sha256= prefix:
X-Notion-Signature: sha256=3f5a1e9c7b2d4086a1c3e5d7f9b0a2c4e6d8f0a1b3c5d7e9f1a3b5c7d9e1f3a5Hookbase verifies this automatically once the source has both fields set:
{
"provider": "notion",
"signingSecret": "your-notion-webhook-secret"
}There is no timestamp in this scheme, so nothing expires and nothing needs a clock tolerance.
Info
Hookbase hashes the bytes that arrived. Notion's own sample code hashes
JSON.stringify(body) — the re-serialized object rather than the request body. Those two strings
agree only because Notion sends minified JSON. Verifying against the raw body is correct wherever
they agree and correct where they would not.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
Common Events
| Event | Description |
|---|---|
page.created | A page was created |
page.content_updated | Page content was updated |
page.properties_updated | Page properties were updated |
page.deleted | A page was deleted |
page.undeleted | A page was restored |
database.created | A database was created |
comment.created | A comment was created |
Page Created
{
"type": "page.created",
"timestamp": "2024-01-15T10:30:00.000Z",
"workspace_id": "ws-abc123",
"data": {
"page": {
"id": "page-def456",
"parent": {
"type": "database_id",
"database_id": "db-ghi789"
},
"properties": {
"Name": {
"title": [
{
"text": {
"content": "New Feature Spec"
}
}
]
},
"Status": {
"select": {
"name": "Draft"
}
}
},
"created_time": "2024-01-15T10:30:00.000Z",
"created_by": {
"id": "user-jkl012"
}
}
}
}Page Properties Updated
{
"type": "page.properties_updated",
"timestamp": "2024-01-15T11:00:00.000Z",
"workspace_id": "ws-abc123",
"data": {
"page": {
"id": "page-def456"
},
"updated_properties": [
"Status"
]
}
}Info
Notion's update events name what changed, not the new value. A page.properties_updated event
lists the changed property names; reading the values means calling Notion's API for the page.
Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
Flatten for an Index
function transform(payload) {
const page = payload.data && payload.data.page ? payload.data.page : {};
return {
event: payload.type,
workspace_id: payload.workspace_id,
page_id: page.id || null,
parent_database_id: page.parent && page.parent.database_id
? page.parent.database_id
: null,
updated_properties: payload.data ? payload.data.updated_properties || [] : [],
occurred_at: payload.timestamp
};
}Slack Notification for New Pages
function transform(payload) {
const page = payload.data && payload.data.page ? payload.data.page : {};
const titleProp = page.properties && page.properties.Name
? page.properties.Name.title
: null;
const title = titleProp && titleProp.length
? titleProp[0].text.content
: page.id;
return {
text: `New Notion page: ${title}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Page:*\n${title}` },
{ type: "mrkdwn", text: `*Event:*\n${payload.type}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
Page Creations Only
{
"name": "New Pages",
"logic": "AND",
"conditions": [
{
"field": "type",
"operator": "equals",
"value": "page.created"
}
]
}One Database
{
"name": "Specs Database",
"logic": "AND",
"conditions": [
{
"field": "data.page.parent.database_id",
"operator": "equals",
"value": "db-ghi789"
}
]
}Any Page Change
{
"name": "Page Changes",
"logic": "AND",
"conditions": [
{
"field": "type",
"operator": "starts_with",
"value": "page."
}
]
}Headers
| Header | Description |
|---|---|
X-Notion-Signature | Hex HMAC-SHA256 of the raw body, prefixed sha256= |
Hookbase does not forward these headers to your destination. A delivery is a fresh request:
Hookbase sets Content-Type, User-Agent: Hookbase/1.0, X-Delivery-ID and X-Event-ID, then
adds the headers and auth you configured on the destination. Nothing Notion sent reaches your
handler as a header — read what you need out of the body, or set it on the destination yourself.
Of the incoming headers, only content-type, user-agent, x-github-event, x-gitlab-event and
stripe-signature are stored on the event, so those are the only ones visible later in the
dashboard or the API.
Troubleshooting
Signature Verification Failed
- Confirm the source's
providerisnotion— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, and Notion sendsX-Notion-Signature - Confirm
signingSecretis the webhook signing secret from the integration, not the integration's internal API token - Rotating the integration's webhook secret invalidates the old value — update the source when you rotate
Missing Events
- The integration must be shared with the page or database. Content the integration cannot see produces no events
- Check which event types the integration is subscribed to in the Developer Portal
- A page inside an unshared parent inherits that: sharing the parent is usually the fix
Duplicate Events
Hookbase has no provider event-id extractor for Notion, so the default auto dedup strategy falls
back to hashing the payload. Editing the same page twice produces two distinct payloads and two
events; that is Notion reporting two changes, not a duplicate.