Skip to main content
{headless}
Dashboard
API ReferenceWebhooks5 min read

Webhooks

Receive signed HTTP notifications when content changes in your project.

What are Webhooks?

Webhooks let your external systems react when events occur in your CMS — an entry is published, a media asset is uploaded, a content type changes. Each delivery is a POST request carrying a JSON payload and an HMAC-SHA256 signature you can verify.

Creating a Webhook

  1. Go to Project Settings > Webhooks
  2. Click Create Webhook
  3. Enter the destination URL (must be https://)
  4. Select the events to subscribe to (see Event Types)
  5. Click Create
  6. Copy the Signing Secret — you need it to verify deliveries

Event Types

Event names are past tense. Subscribing to a name not in this list is rejected.

EventFires when
entry.createdA new entry is created
entry.updatedAn existing entry is saved
entry.deletedAn entry is permanently deleted
entry.publishedAn entry is published
entry.unpublishedA published entry returns to draft
media.uploadedA media asset is uploaded
media.updatedMedia metadata changes
media.deletedA media asset is deleted
content_type.createdA content type is created
content_type.updatedA content type is changed
content_type.deletedA content type is deleted

Webhook Payload

Every payload shares the same envelope. Event-specific fields live under data — never at the top level.

FieldTypeDescription
idstringUnique event ID (evt_…), use for idempotency
eventstringEvent type from the table above
timestampstringISO 8601 time the event occurred
projectIdstringID of the project the event belongs to
apiVersionstringPayload structure version (2025-01-01)
dataobjectEvent-specific data

An entry.published delivery:

JSON
{
  "id": "evt_aa98ec719c394b6585ffc6671930f3a4",
  "event": "entry.published",
  "timestamp": "2026-08-27T04:17:15.992Z",
  "projectId": "cmtayp46j0001l104grq7pfuv",
  "apiVersion": "2025-01-01",
  "data": {
    "entry": {
      "id": "entry_789",
      "contentTypeApiId": "blogPost",
      "contentTypeName": "Blog Post",
      "locale": "en",
      "slug": "my-blog-post",
      "status": "PUBLISHED",
      "version": 3,
      "data": {
        "title": "My Blog Post",
        "body": "..."
      },
      "createdAt": "2026-08-20T09:00:00.000Z",
      "updatedAt": "2026-08-27T04:17:15.980Z",
      "publishedAt": "2026-08-27T04:17:15.980Z"
    },
    "isFirstPublish": true
  }
}

To route on content type, read data.entry.contentTypeApiId. There is no top-level contentType object and no project object.

Payload by event

Eventdata contains
entry.createdentry
entry.updatedentry, changedFields
entry.deletedentryId, contentTypeApiId, locale, slug
entry.publishedentry, isFirstPublish
entry.unpublishedentry
media.uploadedasset
media.updatedasset, changedFields
media.deletedassetId, filename, url
content_type.createdcontentType
content_type.updatedcontentType, changedFields
content_type.deletedcontentTypeId, apiId, name

The entry object carries the fields shown above. asset carries id, filename, url, mimeType, size, width, height, alt, folderId, createdAt. contentType carries id, apiId, name, description, isSingle, draftEnabled, fieldCount, createdAt, updatedAt.

Webhook Security

Delivery Headers

Every delivery includes three headers:

HeaderExamplePurpose
X-Webhook-Signaturev1=abc123…HMAC-SHA256 signature, v1= + hex digest
X-Webhook-Timestamp1706123456Unix time (seconds) the request was sent
X-Webhook-Event-Idevt_abc123Unique event ID, for idempotency

Header names are case-insensitive; most frameworks lowercase them (x-webhook-signature).

Signature Verification

The signature is computed over the timestamp and the raw body joined by a period — not the payload alone:

Text
HMAC-SHA256(`${timestamp}.${rawBody}`, your_signing_secret)

The X-Webhook-Signature header is that hex digest prefixed with v1=. Strip the v1= prefix before comparing, and verify against the raw request body; re-serializing parsed JSON can change byte-for-byte content and break the comparison.

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
  // 1. Reject replays outside a 5-minute window
  const now = Math.floor(Date.now() / 1000);
  if (!timestamp || Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return false;
  }

  // 2. Signature must carry the v1 prefix
  if (!signature || !signature.startsWith('v1=')) {
    return false;
  }
  const providedSig = signature.slice(3);

  // 3. Compute the expected signature over `timestamp.rawBody`
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  // 4. Compare timing-safely (equal lengths required)
  const provided = Buffer.from(providedSig, 'hex');
  const expected = Buffer.from(expectedSig, 'hex');
  if (provided.length !== expected.length) {
    return false;
  }
  return crypto.timingSafeEqual(provided, expected);
}

An Express endpoint, using the raw body:

JavaScript
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');

  const ok = verifyWebhookSignature(
    rawBody,
    req.headers['x-webhook-signature'],
    req.headers['x-webhook-timestamp'],
    process.env.WEBHOOK_SECRET
  );

  if (!ok) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(rawBody);
  console.log('Verified:', event.event, event.id);

  res.status(200).json({ received: true });
});

In a Next.js Route Handler, read the raw body with await request.text().

Getting Your Secret

  1. Go to Project Settings > Webhooks
  2. Click on your webhook
  3. Copy the Signing Secret

The same webhook detail view has a Setup Guide tab with this verification code, and a Deliveries tab showing what was actually sent.

Required Response

Return a 2xx status code within 10 seconds. Any other status, or a timeout, counts as a failed delivery and is retried. Acknowledge first and do slow work asynchronously.

Retry Policy

Failed deliveries are retried up to 6 times with exponential backoff:

AttemptDelay after previous
11 minute
25 minutes
330 minutes
42 hours
58 hours
624 hours

After the sixth retry fails, the delivery is marked failed and not retried again. Because retries redeliver the same event, use X-Webhook-Event-Id (or the payload's id) to make your handler idempotent.

Testing Webhooks

  1. Click on a webhook
  2. Click Send Test
  3. A test payload is sent to your URL
  4. Check the delivery log for the result

The test event is signed exactly like a real delivery, so it exercises your verification code. Its body differs from real events: event is test, it carries createdAt rather than timestamp, and data holds a message and the webhook that sent it.

JSON
{
  "id": "evt_aa98ec719c394b6585ffc6671930f3a4",
  "event": "test",
  "apiVersion": "2025-01-01",
  "createdAt": "2026-08-27T04:17:15.992Z",
  "projectId": "cmtayp46j0001l104grq7pfuv",
  "data": {
    "message": "This is a test webhook event",
    "timestamp": "2026-08-27T04:17:15.992Z",
    "webhook": { "id": "wh_123", "name": "Production rebuild" }
  }
}

Handle test explicitly, or ignore unknown event types, so a test send does not fall through your routing logic.

Delivery Logs

View recent webhook deliveries:

  1. Go to Project Settings > Webhooks
  2. Click on a webhook
  3. View the Delivery Log
  4. See status, response code, and timing

URL Requirements

  • Endpoints must use https://
  • URLs that resolve to private or reserved IP ranges are rejected at delivery time (loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, fd00::/8, ::1)

Local endpoints are therefore not reachable directly — use a tunnel that gives you a public HTTPS hostname.

Common Use Cases

Static Site Rebuild

Trigger a rebuild when content changes:

JSON
{
  "url": "https://api.netlify.com/build_hooks/xxx",
  "events": ["entry.published", "entry.unpublished"]
}

Cache Invalidation

Clear CDN cache when content updates:

JSON
{
  "url": "https://your-api.com/invalidate-cache",
  "events": ["entry.updated", "entry.deleted"]
}

Slack Notifications

Send to a relay that posts to Slack when content is published:

JSON
{
  "url": "https://your-api.com/notify-slack",
  "events": ["entry.published"]
}

Slack's incoming-webhook URLs expect Slack's own message format, so point the webhook at your own endpoint and have it reshape the payload.

Disabling Webhooks

To temporarily disable a webhook:

  1. Go to Project Settings > Webhooks
  2. Click on the webhook
  3. Toggle Active to off
  4. Click Save

The webhook will not fire until re-enabled. Deliveries already queued for a webhook that is disabled before they are sent are marked failed.