Use this file to discover all available pages before exploring further.
Webhooks expose an HTTP endpoint that external services can call. Use them to push events from external systems into Notion, such as a GitHub push, Stripe event, Zendesk ticket update, or any service that can send an HTTP webhook.
The execute function receives an array of WebhookEvent objects. The array currently contains one event, but may contain multiple events in the future.
Property
Type
Description
deliveryId
string
Unique ID for this Notion delivery. It is stable across retries for the same inbound request.
body
Record<string, unknown>
Parsed JSON body. If the request body is not a JSON object, this is {}.
rawBody
string
Original request body as a string. Use this for signature verification.
headers
Record<string, string>
Request headers. Header names are lowercased.
method
string
HTTP method used by the sender. Webhook URLs accept POST requests.
Use the external provider’s own event ID for idempotency when the payload includes one.
deliveryId is useful when Notion retries running your worker, but a provider may
redeliver the same event as a new HTTP request.
Use the CLI to print the URLs for a deployed worker:
ntn workers webhooks list
For scripts, use JSON or tab-separated output:
ntn workers webhooks list --jsonntn workers webhooks list --plain
Treat webhook URLs as secrets. Anyone with the full URL can send events to the
webhook endpoint unless you add provider-specific signature verification inside your worker.
Most webhook providers can sign requests with a shared secret. Store the signing secret as a worker secret, verify each request using event.rawBody and event.headers, and throw WebhookVerificationError when verification fails:
import * as crypto from "node:crypto";import { WebhookVerificationError, Worker } from "@notionhq/workers";const worker = new Worker();export default worker;/** * Verify a GitHub webhook signature. * GitHub sends the HMAC-SHA256 signature in the X-Hub-Signature-256 header * as "sha256={hex}". The raw body must be used for verification. */function verifyGitHubSignature( rawBody: string, headers: Record<string, string>,): void { const secret = process.env.GITHUB_WEBHOOK_SECRET; if (!secret) { throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not configured"); } const signature = headers["x-hub-signature-256"]; if (!signature?.startsWith("sha256=")) { throw new WebhookVerificationError("Invalid GitHub signature"); } const expected = `sha256=${crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex")}`; if (signature.length !== expected.length) { throw new WebhookVerificationError("Invalid GitHub signature"); } // Use timing-safe comparison to prevent timing attacks. if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { throw new WebhookVerificationError("Invalid GitHub signature"); }}worker.webhook("onGithubPush", { title: "GitHub Push Webhook", description: "Handles push events from GitHub repositories", execute: async (events) => { for (const event of events) { verifyGitHubSignature(event.rawBody, event.headers); console.log("Verified GitHub event:", event.body); } },});
Set the secret before deploying or push it from your local .env file:
ntn workers env set GITHUB_WEBHOOK_SECRET=your-secret
See Secrets for more ways to manage worker environment variables.
After 5 consecutive WebhookVerificationError failures, Notion blocks that
webhook before running your handler. Redeploy the worker to reset the failure
counter.
When a webhook request reaches Notion, Notion validates the URL, enqueues the event, and responds with 202 Accepted. Your worker runs asynchronously after the HTTP response is sent.If your handler throws WebhookVerificationError, Notion records a verification failure and does not retry that event. If your handler throws another error, Notion retries the worker run up to 3 times.Successful runs reset the consecutive verification failure counter.
Webhook handlers receive the same context object as other capabilities, including context.notion, the Notion API SDK client:
worker.webhook("createPageFromWebhook", { title: "Create Page From Webhook", description: "Creates a page when an external event is received", execute: async (events, { notion }) => { const databaseId = process.env.MY_WEBHOOK_DATABASE_ID; if (!databaseId) { throw new Error("MY_WEBHOOK_DATABASE_ID is not configured"); } for (const event of events) { const externalId = typeof event.body.id === "string" ? event.body.id : event.deliveryId; await notion.pages.create({ parent: { database_id: databaseId }, properties: { Name: { title: [ { text: { content: `Webhook event ${externalId}`, }, }, ], }, }, }); } },});
For webhooks, context.notion is not automatically authenticated. To call the Notion API, create an internal integration, give it access to the relevant pages or databases, and store the integration token in NOTION_API_TOKEN:
ntn workers env set NOTION_API_TOKEN=secret_xxx
At runtime, context.notion reads process.env.NOTION_API_TOKEN and uses it as the Notion API client token.For more information about creating an integration token for a worker, see Using Notion API from a worker.