Skip to main content
RapidDev - Software Development Agency
API AutomationsNotionBearer Token

How to Automate Notion Team Collaboration using the API

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.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesNotionMay 2026RapidDev Engineering Team
TL;DR

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

Auth

Bearer Token

Rate limit

3 requests/second

Format

JSON

SDK

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.

Base URLhttps://api.notion.com

Setting 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.

  1. 1Go to notion.so/my-integrations (workspace owner required)
  2. 2Create or open your integration and enable: Read content, Insert comments, Read user information
  3. 3Copy the Internal Integration Secret (ntn_ prefix)
  4. 4Share the target database with the integration via '...' > Connections
  5. 5Set up a public HTTPS endpoint for webhook delivery (use ngrok for local testing)
  6. 6Store credentials: export NOTION_TOKEN=ntn_your_token
  7. 7After creating the webhook subscription (next section), store the verification_token for signature verification
auth.py
1import os
2import hmac
3import hashlib
4
5NOTION_TOKEN = os.environ['NOTION_TOKEN']
6WEBHOOK_SECRET = os.environ.get('NOTION_WEBHOOK_SECRET', '')
7
8HEADERS = {
9 'Authorization': f'Bearer {NOTION_TOKEN}',
10 'Notion-Version': '2025-09-03',
11 'Content-Type': 'application/json'
12}
13
14def 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.sha256
20 ).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

POST/v1/webhooks

Subscribe 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.

ParameterTypeRequiredDescription
urlstringrequiredHTTPS URL that will receive webhook event deliveries.
eventsarrayrequiredArray of event types to subscribe to: page.created, page.updated, page.content_updated, database.row_added, comment.created.
resourceobjectrequiredThe page or database to subscribe to: {type: 'database', id: 'DATABASE_ID'} or {type: 'page', id: 'PAGE_ID'}.

Request

json
1{"url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"resource":{"type":"database","id":"DATABASE_ID"}}

Response

json
1{"id":"webhook-id","url":"https://yourapp.com/webhooks/notion","events":["page.updated"],"verification_token":"abc123","active":false}
GET/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.

ParameterTypeRequiredDescription
page_idstringrequiredUUID of the page to retrieve. Available in the webhook payload as entity.id.

Response

json
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"}}}}
POST/v1/comments

Create 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.

ParameterTypeRequiredDescription
parentobjectrequired{page_id: 'PAGE_ID'} to comment on a page, or {block_id: 'BLOCK_ID'} for inline block discussion.
rich_textarrayrequiredArray of rich text objects forming the comment body. Use mention type objects to tag users.

Request

json
1{"parent":{"page_id":"PAGE_ID"},"rich_text":[{"type":"text","text":{"content":"Assigned to "}},{"type":"mention","mention":{"type":"user","user":{"id":"USER_ID"}}}]}

Response

json
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"}}}]}
GET/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.

ParameterTypeRequiredDescription
user_idstringrequiredThe Notion user UUID. Found in people property values and webhook payload actor fields.

Response

json
1{"object":"user","id":"user-id","type":"person","name":"Alice Smith","person":{"email":"alice@example.com"}}

Step-by-step automation

1

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.

request.sh
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.

2

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.

request.sh
1# Webhook handler example verify signature and fetch page
2curl -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.

3

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.

request.sh
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.

4

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.

request.sh
1# First fetch user details
2curl -X GET 'https://api.notion.com/v1/users/USER_ID' \
3 -H 'Authorization: Bearer $NOTION_TOKEN' \
4 -H 'Notion-Version: 2025-09-03'
5
6# Then send Slack notification
7curl -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.

automate_notion_collab.py
1import os
2import hmac
3import hashlib
4import threading
5import requests
6from flask import Flask, request, jsonify
7
8app = Flask(__name__)
9
10NOTION_TOKEN = os.environ['NOTION_TOKEN']
11WEBHOOK_SECRET = os.environ['NOTION_WEBHOOK_SECRET']
12SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK_URL')
13
14NOTION_HEADERS = {
15 'Authorization': f'Bearer {NOTION_TOKEN}',
16 'Notion-Version': '2025-09-03',
17 'Content-Type': 'application/json'
18}
19
20def 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)
23
24def 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 None
27
28def 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 })
37
38def notify_slack(user_name, task_name, page_id):
39 if not SLACK_WEBHOOK:
40 return
41 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 })
45
46def process_event(payload):
47 page_id = payload.get('entity', {}).get('id')
48 if not page_id:
49 return
50 page = fetch_page(page_id)
51 if not page:
52 return
53 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}')
62
63@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'}), 401
69 payload = request.json
70 threading.Thread(target=process_event, args=(payload,), daemon=True).start()
71 return jsonify({'ok': True})
72
73if __name__ == '__main__':
74 app.run(port=3000)

Error handling

403{"object":"error","status":403,"code":"restricted_resource","message":"Insufficient permissions for this endpoint."}
Cause

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}.

Fix

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.

Retry strategy

No retry — fix capabilities first.

401{"error": "Invalid signature"}
Cause

The X-Notion-Signature header does not match the expected HMAC-SHA256 computed from the raw request body using the webhook verification_token.

Fix

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.

Retry strategy

No retry — this is a security check. Return 401 to signal the payload is rejected.

404{"object":"error","status":404,"code":"object_not_found","message":"Could not find page with ID: ..."}
Cause

The page ID from the webhook payload no longer exists (deleted or moved) or the integration cannot access it.

Fix

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.

Retry strategy

Do not retry 404 — the resource does not exist from the integration's perspective.

429{"object":"error","status":429,"code":"rate_limited","message":"You have been rate limited."}
Cause

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.

Fix

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.

Retry strategy

Honor the Retry-After header. Queue failed events for retry with exponential backoff.

400{"object":"error","status":400,"code":"missing_version","message":"Notion-Version header is required."}
Cause

The Notion-Version header was omitted. The webhook subscription endpoint specifically requires version 2026-03-01.

Fix

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.

Retry strategy

No retry — fix the header and resend.

Rate Limits for Notion API

ScopeLimitWindow
Per integration token3 requests averageper second (~2,700 per 15 minutes)
Follow-up calls per webhook event2-3 API callsper event (fetch page + fetch user + post comment)
retry-handler.ts
1import time
2import requests
3
4def 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 continue
11 return resp
12 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

intermediate

Subscribe to page.updated events and specifically check whether the Assignee people property changed between the event's previous and current states.

Deadline Reminder System

intermediate

Combine 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

advanced

When 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/month

Zapier's Notion integration can trigger on database row updates and post Slack messages, though it polls rather than using webhooks (5-15 minute delay).

Pros
  • + No code required
  • + Easy Slack/email connection
  • + Handles retries automatically
Cons
  • - 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/month

Make 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.

Pros
  • + Comment posting supported
  • + Lower cost than Zapier for high volume
  • + 1-minute polling on Growth plan
Cons
  • - Still polling-based, not true real-time
  • - Notion module may lag API version updates
  • - Steeper learning curve

n8n

Free self-hosted; cloud from €20/month

n8n 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.

Pros
  • + Can receive real webhooks via n8n's built-in webhook node
  • + Self-hostable
  • + Notion comment creation supported
Cons
  • - 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.

ChatGPT / Claude Prompt

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?

Lovable / V0 Prompt

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.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.