Automate Notion team collaboration by subscribing to page.updated webhook events via POST /v1/webhooks, then responding in real-time: fetch the updated page with GET /v1/pages/{id}, post a threaded comment with POST /v1/comments, and notify your team via Slack or email. Webhook payloads are sparse — they contain only IDs and timestamps, requiring follow-up API calls. Rate limit: 3 req/s per integration.
API Quick Reference
Bearer Token
3 requests/second
JSON
Available
Understanding the Notion API
The Notion REST API supports reading and writing workspace content via Bearer token authentication. In 2025-2026, Notion introduced a webhook system (public beta) allowing integrations to subscribe to real-time events rather than polling. Webhooks are created via POST /v1/webhooks using Notion-Version: 2026-03-01 or later.
For team collaboration automation, the key integration points are: webhooks for detecting assignment changes (page.updated events), GET /v1/pages/{id} to fetch current page state, POST /v1/comments to add threaded notifications on the page itself, and GET /v1/users to resolve user IDs to names and emails for external notifications.
Critical limitation: webhook payloads are sparse — they contain only entity IDs, event types, and timestamps. Your handler must make follow-up API calls to read the actual page content. Documentation is at developers.notion.com.
https://api.notion.comSetting Up Notion API Authentication
Notion uses Bearer tokens for internal integrations. For collaboration automation you need Insert comments (to post threaded notifications) and Read user information capabilities in addition to Read content. Without Insert comments, POST /v1/comments returns 403 restricted_resource. Webhook signatures use HMAC-SHA256 keyed by the verification_token from the webhook subscription.
- 1Go to notion.so/my-integrations (workspace owner required)
- 2Create or open your integration and enable: Read content, Insert comments, Read user information
- 3Copy the Internal Integration Secret (ntn_ prefix)
- 4Share the target database with the integration via '...' > Connections
- 5Set up a public HTTPS endpoint for webhook delivery (use ngrok for local testing)
- 6Store credentials: export NOTION_TOKEN=ntn_your_token
- 7After creating the webhook subscription (next section), store the verification_token for signature verification
1import os2import hmac3import hashlib45NOTION_TOKEN = os.environ['NOTION_TOKEN']6WEBHOOK_SECRET = os.environ.get('NOTION_WEBHOOK_SECRET', '')78HEADERS = {9 'Authorization': f'Bearer {NOTION_TOKEN}',10 'Notion-Version': '2025-09-03',11 'Content-Type': 'application/json'12}1314def verify_notion_webhook(raw_body: bytes, signature_header: str) -> bool:15 """Verify X-Notion-Signature header using HMAC-SHA256."""16 expected = hmac.new(17 WEBHOOK_SECRET.encode(),18 raw_body,19 hashlib.sha25620 ).hexdigest()21 return hmac.compare_digest(f'sha256={expected}', signature_header)Security notes
- •Store the Notion token and webhook verification_token in environment variables
- •Always verify the X-Notion-Signature header before processing webhook payloads
- •Use hmac.compare_digest for signature comparison — never use == to avoid timing attacks
- •Webhook endpoint must be HTTPS — Notion does not deliver to plain HTTP endpoints in production
- •Only enable the capabilities the integration needs — if you do not need to post comments, do not enable Insert comments
Key endpoints
/v1/webhooksSubscribe to Notion events for a specific page or database. Requires Notion-Version: 2026-03-01 or later. Notion sends a verification POST to your URL first — you must echo back the verification_token to activate the subscription.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | required | HTTPS URL that will receive webhook event deliveries. |
events | array | required | Array of event types to subscribe to: page.created, page.updated, page.content_updated, database.row_added, comment.created. |
resource | object | required | The page or database to subscribe to: {type: 'database', id: 'DATABASE_ID'} or {type: 'page', id: 'PAGE_ID'}. |
Request
1{"url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"resource":{"type":"database","id":"DATABASE_ID"}}Response
1{"id":"webhook-id","url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"verification_token":"abc123","active":false}/v1/pages/{page_id}Fetch the current state of a page including all properties. Use this in the webhook handler to read the updated page content after receiving a page.updated event.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | required | UUID of the page to retrieve. Available in the webhook payload as entity.id. |
Response
1{"object":"page","id":"page-id","properties":{"Name":{"title":[{"plain_text":"Fix auth bug"}]},"Assignee":{"people":[{"id":"user-id","name":"Alice","object":"user"}]},"Status":{"status":{"name":"In Progress"}}}}/v1/commentsCreate a threaded comment on a page. The integration must have Insert comments capability. Use this to notify team members directly within Notion by mentioning them in a comment.
| Parameter | Type | Required | Description |
|---|---|---|---|
parent | object | required | {page_id: 'PAGE_ID'} to comment on a page, or {block_id: 'BLOCK_ID'} for inline block discussion. |
rich_text | array | required | Array of rich text objects forming the comment body. Use mention type objects to tag users. |
Request
1{"parent":{"page_id":"PAGE_ID"},"rich_text":[{"type":"text","text":{"content":"Assigned to "}},{"type":"mention","mention":{"type":"user","user":{"id":"USER_ID"}}}]}Response
1{"object":"comment","id":"comment-id","parent":{"type":"page_id","page_id":"page-id"},"rich_text":[{"type":"text","text":{"content":"Assigned to "}},{"type":"mention","mention":{"user":{"id":"user-id","name":"Alice"}}}]}/v1/users/{user_id}Fetch details for a specific Notion user including name and email (email requires Read user information with email capability). Use this to resolve user IDs from the Assignee property to names for external notifications.
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id | string | required | The Notion user UUID. Found in people property values and webhook payload actor fields. |
Response
1{"object":"user","id":"user-id","type":"person","name":"Alice Smith","person":{"email":"alice@example.com"}}Step-by-step automation
Create a Webhook Subscription
Why: Webhooks eliminate polling and give you real-time notification when any page in the database is updated, including assignment changes.
Send POST /v1/webhooks with Notion-Version: 2026-03-01 (the webhook endpoint requires this version). Include your HTTPS handler URL, the events array, and the database resource. Notion will POST a verification payload to your URL — you must echo back the verification_token within the Notion UI or API to activate the subscription.
1curl -X POST 'https://api.notion.com/v1/webhooks' \2 -H 'Authorization: Bearer $NOTION_TOKEN' \3 -H 'Notion-Version: 2026-03-01' \4 -H 'Content-Type: application/json' \5 -d '{6 "url": "https://yourapp.com/webhooks/notion",7 "events": ["page.updated"],8 "resource": {"type": "database", "id": "YOUR_DATABASE_ID"}9 }'Pro tip: The webhook subscription endpoint requires Notion-Version: 2026-03-01, not 2025-09-03. Keep a separate headers object for webhook management calls that uses the newer version.
Expected result: A webhook object with id, verification_token, and active: false. Store the verification_token as NOTION_WEBHOOK_SECRET — you need it to verify incoming signatures.
Handle Webhook Events and Fetch Page Details
Why: Webhook payloads are sparse — they only contain entity IDs and event types, so you must fetch the actual page to see what changed.
Your webhook handler receives a POST with the event type and page ID. Verify the X-Notion-Signature header using HMAC-SHA256 with your verification_token, then fetch the full page with GET /v1/pages/{id}. Check if the people property changed to detect assignment updates.
1# Webhook handler example — verify signature and fetch page2curl -X GET 'https://api.notion.com/v1/pages/PAGE_ID_FROM_WEBHOOK' \3 -H 'Authorization: Bearer $NOTION_TOKEN' \4 -H 'Notion-Version: 2025-09-03'Pro tip: Always return HTTP 200 from your webhook handler as quickly as possible. If Notion does not receive a 200, it will retry delivery. Do all heavy processing (fetch user, post comment, send Slack) asynchronously after returning 200.
Expected result: The webhook handler returns 200 quickly and processes the page update asynchronously. The fetched page object contains all current property values including the Assignee people array.
Post a Threaded Comment on the Page
Why: Commenting directly on the Notion page keeps the notification in context — team members see the assignment notification where the task lives.
Use POST /v1/comments with the page ID and a rich text body that mentions the assigned user. The mention type in rich text creates an @-mention that sends a Notion notification to the mentioned user. The integration must have Insert comments capability.
1curl -X POST 'https://api.notion.com/v1/comments' \2 -H 'Authorization: Bearer $NOTION_TOKEN' \3 -H 'Notion-Version: 2025-09-03' \4 -H 'Content-Type: application/json' \5 -d '{6 "parent": {"page_id": "PAGE_ID"},7 "rich_text": [8 {"type": "text", "text": {"content": "This task has been assigned to "}},9 {"type": "mention", "mention": {"type": "user", "user": {"id": "USER_ID"}}},10 {"type": "text", "text": {"content": ". Please review and update the status."}}11 ]12 }'Pro tip: If your integration does not have Insert comments enabled, the POST /v1/comments call returns 403 restricted_resource with a confusing error message. Check the capabilities at notion.so/my-integrations — this is separate from Read/Update content capabilities.
Expected result: A comment appears on the Notion page mentioning the assigned user. The mentioned user receives a Notion in-app notification.
Send External Notification via Slack
Why: Not all team members monitor Notion actively — an external Slack message ensures the assignment notification reaches the right person immediately.
After posting the Notion comment, fetch the assignee's details with GET /v1/users/{id} to get their name, then send a Slack message to the team channel. If you need the user's email for direct Slack matching, enable Read user information including email on the integration.
1# First fetch user details2curl -X GET 'https://api.notion.com/v1/users/USER_ID' \3 -H 'Authorization: Bearer $NOTION_TOKEN' \4 -H 'Notion-Version: 2025-09-03'56# Then send Slack notification7curl -X POST 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK' \8 -H 'Content-Type: application/json' \9 -d '{"text": ":pencil: Alice Smith has been assigned: Fix auth bug"}'Pro tip: GET /v1/users/{id} returns email only if the integration has 'Read user information including email addresses' capability. The basic 'Read user information' capability returns name and avatar but not email.
Expected result: A Slack message is sent to the configured channel with the assignee name, task name, and a link to the Notion page.
Complete working code
This complete Flask webhook handler processes Notion page.updated events: it verifies the X-Notion-Signature, fetches the updated page, checks for Assignee property changes, posts a threaded comment on the page mentioning the assignee, and sends a Slack notification. Returns 200 immediately and processes asynchronously.
1import os2import hmac3import hashlib4import threading5import requests6from flask import Flask, request, jsonify78app = Flask(__name__)910NOTION_TOKEN = os.environ['NOTION_TOKEN']11WEBHOOK_SECRET = os.environ['NOTION_WEBHOOK_SECRET']12SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK_URL')1314NOTION_HEADERS = {15 'Authorization': f'Bearer {NOTION_TOKEN}',16 'Notion-Version': '2025-09-03',17 'Content-Type': 'application/json'18}1920def verify_sig(raw: bytes, sig: str) -> bool:21 d = hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()22 return hmac.compare_digest(f'sha256={d}', sig)2324def fetch_page(page_id):25 r = requests.get(f'https://api.notion.com/v1/pages/{page_id}', headers=NOTION_HEADERS)26 return r.json() if r.ok else None2728def post_comment(page_id, user_id, task_name):29 requests.post('https://api.notion.com/v1/comments', headers=NOTION_HEADERS, json={30 'parent': {'page_id': page_id},31 'rich_text': [32 {'type': 'text', 'text': {'content': f'Assigned to '}},33 {'type': 'mention', 'mention': {'type': 'user', 'user': {'id': user_id}}},34 {'type': 'text', 'text': {'content': f' — {task_name}. Please update status when you start.'}}35 ]36 })3738def notify_slack(user_name, task_name, page_id):39 if not SLACK_WEBHOOK:40 return41 url = f"https://notion.so/{page_id.replace('-','')}"42 requests.post(SLACK_WEBHOOK, json={43 'text': f':pencil: *{user_name}* assigned: {task_name} <{url}|Open>'44 })4546def process_event(payload):47 page_id = payload.get('entity', {}).get('id')48 if not page_id:49 return50 page = fetch_page(page_id)51 if not page:52 return53 title = page['properties'].get('Name', {}).get('title', [])54 task_name = title[0]['plain_text'] if title else 'Untitled'55 assignees = page['properties'].get('Assignee', {}).get('people', [])56 for a in assignees:57 try:58 post_comment(page_id, a['id'], task_name)59 notify_slack(a.get('name', 'Team member'), task_name, page_id)60 except Exception as e:61 print(f'Error processing assignee {a["id"]}: {e}')6263@app.route('/webhooks/notion', methods=['POST'])64def webhook():65 sig = request.headers.get('X-Notion-Signature', '')66 raw = request.get_data()67 if not verify_sig(raw, sig):68 return jsonify({'error': 'bad signature'}), 40169 payload = request.json70 threading.Thread(target=process_event, args=(payload,), daemon=True).start()71 return jsonify({'ok': True})7273if __name__ == '__main__':74 app.run(port=3000)Error handling
{"object":"error","status":403,"code":"restricted_resource","message":"Insufficient permissions for this endpoint."}The integration is missing a required capability. Most commonly: Insert comments is not enabled when posting to /v1/comments, or Read user information is not enabled when fetching /v1/users/{id}.
Go to notion.so/my-integrations, open the integration, and enable the missing capability (Insert comments, Read user information). Re-share the database if needed.
No retry — fix capabilities first.
{"error": "Invalid signature"}The X-Notion-Signature header does not match the expected HMAC-SHA256 computed from the raw request body using the webhook verification_token.
Ensure you are using the raw request body (before any JSON parsing) for signature computation. Verify you are using the verification_token from the webhook subscription response, not the integration token.
No retry — this is a security check. Return 401 to signal the payload is rejected.
{"object":"error","status":404,"code":"object_not_found","message":"Could not find page with ID: ..."}The page ID from the webhook payload no longer exists (deleted or moved) or the integration cannot access it.
Handle 404 gracefully in your webhook handler — the page may have been deleted between the event firing and your API call. Log and skip rather than throwing an error.
Do not retry 404 — the resource does not exist from the integration's perspective.
{"object":"error","status":429,"code":"rate_limited","message":"You have been rate limited."}Each webhook event triggers 2-3 follow-up API calls. During bulk assignment changes (e.g., reassigning 10 tasks at once), this compounds quickly past the 3 req/s limit.
Process webhook events sequentially with delays, or use a queue (Redis, SQS) to serialize event processing. Add 350ms delays between API calls in the event handler.
Honor the Retry-After header. Queue failed events for retry with exponential backoff.
{"object":"error","status":400,"code":"missing_version","message":"Notion-Version header is required."}The Notion-Version header was omitted. The webhook subscription endpoint specifically requires version 2026-03-01.
Add Notion-Version: 2026-03-01 for webhook management calls (/v1/webhooks). Use Notion-Version: 2025-09-03 for all other API calls. Keep separate header objects for each version.
No retry — fix the header and resend.
Rate Limits for Notion API
| Scope | Limit | Window |
|---|---|---|
| Per integration token | 3 requests average | per second (~2,700 per 15 minutes) |
| Follow-up calls per webhook event | 2-3 API calls | per event (fetch page + fetch user + post comment) |
1import time2import requests34def notion_call(method, url, headers, **kwargs):5 for attempt in range(5):6 resp = getattr(requests, method)(url, headers=headers, **kwargs)7 if resp.status_code == 429:8 wait = int(resp.headers.get('Retry-After', 2 ** attempt))9 time.sleep(wait)10 continue11 return resp12 raise Exception('Max retries exceeded')- Return HTTP 200 from the webhook handler immediately and process the event asynchronously in a background thread or queue
- Add 350ms delays between the follow-up API calls within each event handler to stay under 3 req/s
- During bulk operations (e.g., 20 tasks reassigned simultaneously), 20 webhook events each triggering 3 API calls = 60 requests in seconds — use a queue to serialize processing
- Cache user details (GET /v1/users/{id}) for short periods — user names and emails change rarely
- Monitor your webhook delivery logs for 429 responses from Notion — sustained throttling may indicate you need to reduce event subscriptions
Security checklist
- Always verify the X-Notion-Signature header using HMAC-SHA256 before processing any webhook payload
- Use hmac.compare_digest (Python) or crypto.timingSafeEqual (Node.js) to prevent timing attacks on signature comparison
- Webhook handler must use raw request body for signature verification — parsed JSON breaks the signature check
- Store NOTION_TOKEN and NOTION_WEBHOOK_SECRET in environment variables, never in source code
- Webhook endpoint must be HTTPS — Notion does not deliver to plain HTTP in production
- Return 200 immediately from the handler and process asynchronously — do not let long processing times cause Notion to retry delivery
- Enable only the integration capabilities you need (Insert comments, Read user information) — not all capabilities
Automation use cases
Assignment Change Detector
intermediateSubscribe to page.updated events and specifically check whether the Assignee people property changed between the event's previous and current states.
Deadline Reminder System
intermediateCombine a daily cron job with webhook event handling: poll for tasks due tomorrow and post reminder comments, while webhook events handle real-time status escalations.
Cross-Workspace Sync
advancedWhen a page is updated in one Notion workspace, mirror the status change to a linked page in a second workspace using two integration tokens.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; paid plans from $19.99/monthZapier's Notion integration can trigger on database row updates and post Slack messages, though it polls rather than using webhooks (5-15 minute delay).
- + No code required
- + Easy Slack/email connection
- + Handles retries automatically
- - Polling delay — not real-time
- - Cannot post Notion comments natively (requires workaround)
- - Cost scales with trigger volume
Make
Free tier available; paid plans from $9/monthMake supports Notion database watch triggers and can post comments via a Notion module, with near-real-time polling at 1-minute intervals on higher plans.
- + Comment posting supported
- + Lower cost than Zapier for high volume
- + 1-minute polling on Growth plan
- - Still polling-based, not true real-time
- - Notion module may lag API version updates
- - Steeper learning curve
n8n
Free self-hosted; cloud from €20/monthn8n has a Notion node and can process webhooks if you expose a public endpoint, making it the closest no-code alternative to the custom webhook handler.
- + Can receive real webhooks via n8n's built-in webhook node
- + Self-hostable
- + Notion comment creation supported
- - Requires server for webhook reception
- - More complex setup
- - Notion node API version support may vary
Best practices
- Pin Notion-Version: 2026-03-01 specifically for webhook management endpoints — the webhook subscription API requires this version
- Process webhook events asynchronously — always return 200 from the handler within 200ms
- Verify X-Notion-Signature on every incoming webhook using HMAC-SHA256 with your verification_token
- Cache user details to reduce follow-up API calls per event — user data changes infrequently
- Use a job queue (Redis + Bull, SQS) for high-volume workspaces where many events can fire simultaneously
- Test webhook handlers locally with ngrok before deploying to production — Notion requires a public HTTPS URL
- Log all webhook events with their entity IDs and timestamps for debugging and audit purposes
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Notion webhook handler in Python (Flask) that processes page.updated events. The integration is configured with Insert comments and Read user information capabilities. When I try to POST to /v1/comments after receiving a page.updated webhook, I get 403 restricted_resource. My handler code is: {paste handler here}. My integration capabilities are: Read content, Update content, Insert comments. The error detail is: {paste error body here}. What could cause this even with Insert comments enabled?
Build a Next.js webhook handler and monitoring dashboard for Notion team collaboration automation. The app needs: a POST /api/webhooks/notion route handler that verifies X-Notion-Signature using HMAC-SHA256, fetches updated pages from the Notion API with Notion-Version: 2025-09-03, posts threaded comments via POST /v1/comments when Assignee properties change, and sends Slack notifications. Also build a simple dashboard page showing the last 20 processed webhook events with page title, assignee name, timestamp, and status (success/error). Store events in a Supabase table. Use NOTION_TOKEN and NOTION_WEBHOOK_SECRET from environment variables.
Frequently asked questions
Are Notion webhooks available to all plans?
Notion's integration webhook system (created via POST /v1/webhooks) is in public beta as of mid-2026. It is available for internal integrations and requires Notion-Version: 2026-03-01 for the subscription endpoint. Webhook actions inside database automations (the no-code 'Send webhook' button feature) require a paid Notion plan.
Why does my webhook handler keep getting the same event multiple times?
Notion retries webhook delivery if it does not receive a 200 response. If your handler takes too long (processing the event synchronously) or returns a non-200 status, Notion retries. Fix: return 200 immediately after signature verification, then process the event asynchronously in a background thread or job queue.
The webhook payload does not contain the page content — why?
This is by design. Notion webhook payloads are intentionally sparse — they contain only entity IDs, event types, and timestamps. You must make a follow-up GET /v1/pages/{id} call to read the actual page content. This is documented in the webhook specification at developers.notion.com.
How do I detect whether the Assignee property specifically changed vs some other property?
Notion's page.updated event does not currently include a 'changed fields' list. Your handler must fetch the current page state and compare the Assignee people array against a previously stored snapshot. Store the last known assignee list per page ID in a database, then compare on each event to detect actual changes.
What happens when I hit the rate limit during webhook processing?
Notion returns 429 with a Retry-After header. During bulk assignment changes, multiple webhook events can fire simultaneously, each requiring 2-3 follow-up API calls. Implement a queue (Redis, SQS, or even a simple in-memory queue for low volume) to serialize event processing and honor the 3 req/s average limit.
Is the Notion API free?
Yes, the Notion REST API is free for all integration types. There is no paid API tier. The rate limit (3 req/s average) applies regardless of your Notion workspace plan.
Can I mention a user in a Notion comment if I only have their user ID?
Yes. In the rich_text array for POST /v1/comments, use {type: 'mention', mention: {type: 'user', user: {id: 'USER_ID'}}} — you only need the user ID, not the full user object. Notion resolves the user on its end and sends them an in-app notification.
Can RapidDev build a custom Notion collaboration automation for my team?
Yes. RapidDev has built 600+ apps including Notion workflow automations that integrate with Slack, email, and external systems. We can design a collaboration automation tailored to your team's structure and notification preferences. Reach out at rapidevelopers.com for a free consultation.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation