Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.notion.com/llms.txt

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.

Basic webhook

Define a webhook capability on your worker like this:
import { Worker } from "@notionhq/workers";

const worker = new Worker();
export default worker;

worker.webhook("onExternalEvent", {
  title: "External Event Handler",
  description: "Processes incoming webhook requests",
  execute: async (events) => {
    for (const event of events) {
      console.log("Delivery:", event.deliveryId);
      console.log("Method:", event.method);
      console.log("Body:", event.body);
    }
  },
});
After you deploy, Notion creates a URL for each webhook capability. Give that URL to the external service as its webhook destination:
ntn workers deploy
ntn workers webhooks list

The event object

The execute function receives an array of WebhookEvent objects. The array currently contains one event, but may contain multiple events in the future.
PropertyTypeDescription
deliveryIdstringUnique ID for this Notion delivery. It is stable across retries for the same inbound request.
bodyRecord<string, unknown>Parsed JSON body. If the request body is not a JSON object, this is {}.
rawBodystringOriginal request body as a string. Use this for signature verification.
headersRecord<string, string>Request headers. Header names are lowercased.
methodstringHTTP 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.

Webhook URLs

Webhook URLs include a unique ID that acts as a shared secret:
https://www.notion.so/webhooks/worker/{spaceId}/{workerId}/{uniqueWebhookId}/{webhookName}
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 --json
ntn 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.

Verify requests

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.

Execution and retries

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.

Use Notion from a webhook

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 the Notion API from a worker.

Inspect runs

Use worker run logs to debug webhook executions:
ntn workers runs list
ntn workers runs logs <run-id>
To find recent webhook runs quickly:
ntn workers runs list --plain | grep webhook