Gareth Wilson Gareth Wilson

Guide to Strapi Webhooks: Features and Best Practices

Published


Strapi is an open-source headless CMS. Teams model and edit content in Strapi and serve it to websites and apps through REST or GraphQL. Its webhooks tell other systems when that content changes, for example so a static site rebuilds as soon as an editor publishes.

This guide covers the events Strapi sends and their payloads, how to authenticate requests with a static header token, how Strapi's delivery behaves when your endpoint fails, and the practices that keep a receiver reliable in production. It describes Strapi 5.

What are Strapi webhooks?

A Strapi webhook is an HTTP POST with a JSON body, sent to a URL you configure when one of the events you selected happens. Each webhook has a name, a URL, optional headers and a list of events. Every request carries an X-Strapi-Event header naming the event, and the body repeats it in an event field.

Strapi does not sign webhook requests. To authenticate them, you add a header with a secret token, either on each webhook or once for all webhooks in config/server. The Strapi docs include a section on verifying HMAC signatures, but it is advice for a sender you build yourself, for example from a lifecycle hook. Strapi's own webhooks don't send a signature.

Strapi webhook features

FeatureDetails
ConfigurationAdmin panel Settings > Webhooks, or programmatically
Default headerswebhooks.defaultHeaders in config/server, overridden by headers set on a webhook
Method and formatPOST, Content-Type: application/json
DiscriminatorX-Strapi-Event header and the body's event field
AuthenticationA static header you choose, such as Authorization: Bearer <token>. No signature
PayloadEntry events send the fully populated entry, except entry.delete and entry.unpublish
Excluded dataPrivate fields and the User content-type
Delivery10 second timeout, no retries
TestingTrigger button on a webhook sends a trigger-test event

Events

EventFires when
entry.createAn entry is created
entry.updateAn entry is updated
entry.deleteAn entry is deleted
entry.publishAn entry is published (Draft & Publish only)
entry.unpublishAn entry is unpublished (Draft & Publish only)
entry.draft-discardThe draft version of an entry is discarded (Draft & Publish only). Not in the admin form, so the webhook has to be created programmatically
media.createA media file is uploaded
media.updateA media file is updated
media.deleteA media file is deleted
review-workflows.updateEntryStageContent moves between review stages. Enterprise edition only
releases.publishA release is published. Only on some Strapi plans

Entry events share one envelope: event, createdAt, model, uid and entry. Here is Strapi's entry.create example, with the repeatable component and dynamic zone shortened:

{
  "event": "entry.create",
  "createdAt": "2026-09-09T08:49:26.158Z",
  "model": "address",
  "uid": "api::address.address",
  "entry": {
    "id": 1,
    "documentId": "w7vfs319acmaxnurjk5vaaza",
    "city": "Paris",
    "postalCode": "75001",
    "createdAt": "2026-09-09T08:49:26.150Z",
    "updatedAt": "2026-09-09T08:49:26.150Z",
    "publishedAt": null,
    "openingHours": [
      { "id": 1, "day": "monday", "from": "09:00", "to": "18:00" }
    ],
    "blocks": [
      { "id": 1, "body": "Ring the bell twice.", "__component": "address.note" }
    ]
  }
}

A few details matter when you process these:

  • documentId identifies the document and id identifies the version the event is about. A draft and its published version share a documentId but have different id values, and discarding a draft creates a new version with a new id. Key your records on documentId.
  • For every entry event except entry.delete and entry.unpublish, Strapi reads the entry again before sending, so it arrives with all its relations, media, components and dynamic zones populated. You can't choose what is populated; the Strapi 4 webhooks.populateRelations option was removed in Strapi 5.
  • entry.delete and entry.unpublish send the entry as the request that deleted or unpublished it returned. From the admin panel, entry.delete carries only the entry's own fields.
  • One editor action can send more than one event. Publishing saves the draft first, so entry.update arrives just before entry.publish.

Media events use a different envelope, event, createdAt and media, with no model or uid. The media object is the file entry, including the formats generated for an image, and doesn't list the entries the file is attached to. releases.publish sends isPublished and release, and review-workflows.updateEntryStage sends model, uid, entity and workflow.

Inspect Strapi webhooks as they arrive. Set a Hookdeck Inspect URL on a webhook and click Trigger to see the exact headers and body Strapi sends, with no account or setup required.

Setting up Strapi webhooks

Generate a long random token first, for example with openssl rand -hex 32.

  1. In the admin panel, go to Settings > Webhooks and click Add new webhook.
  2. Enter a name and your endpoint's HTTPS URL.
  3. Under Headers, add Authorization with the value Bearer <your token>, or a custom header such as X-Strapi-Webhook-Token.
  4. Select the events you want.
  5. Save, then click Trigger to send a trigger-test event and check the result.

To add the same header to every webhook, set it once in config/server and keep the token in an environment variable:

// config/server.js
module.exports = ({ env }) => ({
  // ...your existing server settings
  webhooks: {
    defaultHeaders: {
      Authorization: `Bearer ${env('WEBHOOK_TOKEN')}`,
    },
  },
});

A header set on a webhook overrides the default header with the same name.

To put Hookdeck in front of your service, create a Hookdeck source with the Strapi type and use its URL as the webhook URL. The source asks for a Header Name and a Header Value: enter the header name you used and its whole value. Header names match case-insensitively, but the value must match exactly, so include Bearer if your value has it. Hookdeck still answers with a 200, but a request with a missing or wrong header is recorded as failed verification and never reaches your destination. The source accepts POST, which is the only method Strapi uses.

Securing Strapi webhooks

Read the header you chose and compare its whole value with your stored token in constant time. Reject the request if the header is missing or the token isn't configured.

const crypto = require('crypto');
const express = require('express');

const HEADER_NAME = process.env.STRAPI_WEBHOOK_HEADER || 'authorization';
// The whole expected value, such as "Bearer <token>".
const SECRET = process.env.STRAPI_WEBHOOK_SECRET;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/strapi', express.raw({ type: '*/*' }), (req, res) => {
  const received = req.get(HEADER_NAME); // case-insensitive lookup
  if (!SECRET || typeof received !== 'string' || !safeEqual(received, SECRET)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  switch (payload.event) {
    case 'entry.publish':
    case 'entry.unpublish':
    case 'entry.delete':
      // Rebuild or purge using payload.uid and payload.entry.documentId
      break;
    case 'media.create':
    case 'media.update':
    case 'media.delete':
      break;
    default:
      // Log and acknowledge anything else, including trigger-test.
  }

  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

HEADER_NAME = os.environ.get("STRAPI_WEBHOOK_HEADER", "Authorization")
SECRET = os.environ.get("STRAPI_WEBHOOK_SECRET", "")  # e.g. "Bearer <token>"


def verify_strapi_request(headers) -> bool:
    if not SECRET:
        return False  # never accept when unconfigured
    received = headers.get(HEADER_NAME)  # Starlette and Flask headers are case-insensitive
    if not received:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(received.encode("utf-8"), SECRET.encode("utf-8"))

A static header proves the sender knows the token. It does not cover the body and carries no timestamp, so a modified or replayed request still passes. Serve the endpoint over HTTPS, use a long random token, keep it out of source control with an environment variable, and rotate it if it might have leaked. If you need a signature, send the request yourself from a lifecycle hook and sign it there.

Make Strapi webhooks production-ready. Hookdeck Event Gateway checks your token header, stores every content event, and retries delivery to your service, which Strapi doesn't do.

Strapi webhook limitations and pain points

Failed deliveries are not retried

The Problem: A publish event that hits your endpoint during a deploy or outage is lost, and the site or index it should have updated stays stale.

Why It Happens: Strapi's webhook runner makes one attempt per event, with a 10 second timeout. A network error, a timeout or an error response ends that attempt, and the event is not sent again.

Workarounds:

  • Keep your endpoint fast and highly available, and respond before doing slow work such as a site rebuild.
  • Run a periodic reconciliation that reads recently updated entries from the Strapi API.

How Hookdeck Can Help: Hookdeck accepts and stores each event as it arrives, then delivers it to your service with retries you configure. You can replay events after an outage, and issues alert you when deliveries fail.

A static header is all you get

The Problem: Anyone who learns the token can send requests your endpoint accepts.

Why It Happens: Strapi sends the headers you configure but doesn't sign the body or add a timestamp.

Workarounds:

  • Use a long random token over HTTPS and rotate it on a schedule.
  • Treat each webhook as a signal and read the current entry from the Strapi API before acting on anything sensitive.

How Hookdeck Can Help: Hookdeck checks the header on every request and keeps those that fail, so you can see unauthenticated traffic without it reaching your service.

One editor action, several events

The Problem: Publishing one entry rebuilds your site twice.

Why It Happens: Publishing saves the draft first, so entry.update is sent just before entry.publish.

Workarounds:

  • Subscribe only to the events you act on. A static site usually needs entry.publish, entry.unpublish and entry.delete, not entry.update.
  • Debounce rebuilds so several events within a few seconds trigger one build.

How Hookdeck Can Help: Filters can drop events you don't need, and a max delivery rate on the destination stops a burst of edits from flooding your build system.

Large payloads

The Problem: Entries with many relations, components or media produce large bodies, and processing them slowly risks the 10 second timeout.

Why It Happens: Strapi populates the whole entry, and you can't limit what is populated.

Workarounds:

  • Raise your body size limit, acknowledge first, and process from a queue.
  • Use only uid and documentId from the payload and fetch exactly the fields you need from the API.

How Hookdeck Can Help: Hookdeck acknowledges Strapi straight away and transformations can trim the payload before it reaches your service.

Best practices

Key on documentId, not id

id changes between draft and published versions and when a draft is discarded. Store and look up content by uid plus documentId. See our guide to webhook idempotency.

Handle each envelope

Entry, media, release and review workflow events have different shapes. Route on event first, then read entry, media, release or entity accordingly, and ignore trigger-test.

Don't send secrets in content

Private fields aren't sent and the User content-type never triggers webhooks, but everything else in an entry is. Mark sensitive attributes private in the content-type schema.

Acknowledge fast

Return 200 once the request is authenticated and process it from a queue. See why to process webhooks asynchronously.

Conclusion

Strapi sends a JSON POST for content, media, release and review workflow changes, with the event in the X-Strapi-Event header and the body. It doesn't sign requests or retry them. Authenticate with a long random header token over HTTPS, compare it in constant time, key content on documentId, and respond within the 10 second timeout.

Hookdeck Event Gateway checks the token header, stores every event, and retries delivery so an outage doesn't leave your site or index out of date.

Get started with Hookdeck for free and handle Strapi webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.