Skip to main content

Webhooks

Page summary:

Webhooks let Strapi notify external systems when content changes, while omitting the Users type for privacy. Configuration in config/server sets default headers and endpoints to trigger third-party processing.

Webhook is a construct used by an application to notify other applications that an event occurred. More precisely, webhook is a user-defined HTTP callback. Using a webhook is a good way to tell third-party providers to start some processing (CI, build, deployment ...).

The way a webhook works is by delivering information to a receiving application through HTTP requests (typically POST requests).

User content-type webhooks

To prevent from unintentionally sending any user's information to other applications, Webhooks will not work for the User content-type. If you need to notify other applications about changes in the Users collection, you can do so by creating Lifecycle hooks using the ./src/index.js example.

Available configurations

You can set webhook configurations inside the file ./config/server.

  • webhooks
    • defaultHeaders: You can set default headers to use for your webhook requests. This option is overwritten by the headers set in the webhook itself.

Example configuration

./config/server.js
module.exports = {
webhooks: {
defaultHeaders: {
"Custom-Header": "my-custom-header",
},
},
};

Webhooks security

Most of the time, webhooks make requests to public URLs, therefore it is possible that someone may find that URL and send it wrong information.

To prevent this from happening you can send a header with an authentication token. Using the Admin panel you would have to do it for every webhook.

Another way is to define defaultHeaders to add to every webhook request.

You can configure these global headers by updating the file at ./config/server:

./config/server.js
module.exports = {
webhooks: {
defaultHeaders: {
Authorization: "Bearer my-very-secured-token",
},
},
};

If you are developing the webhook handler yourself you can now verify the token by reading the headers.

Verifying signatures

In addition to auth headers, it's recommended to sign webhook payloads and verify signatures server‑side to prevent tampering and replay attacks. To do so, you can use the following guidelines:

  • Generate a shared secret and store it in environment variables
  • Have the sender compute an HMAC (e.g., SHA‑256) over the raw request body plus a timestamp
  • Send the signature (and timestamp) in headers (e.g., X‑Webhook‑Signature, X‑Webhook‑Timestamp)
  • On receipt, recompute the HMAC and compare using a constant‑time check
  • Reject if the signature is invalid or the timestamp is too old to mitigate replay
Example: Verify HMAC signatures (Node.js)

Here is a minimal Node.js middleware example (pseudo‑code) showing HMAC verification:

/src/middlewares/verify-webhook.js
const crypto = require("crypto");

module.exports = (config, { strapi }) => {
const secret = process.env.WEBHOOK_SECRET;

return async (ctx, next) => {
const signature = ctx.get("X-Webhook-Signature");
const timestamp = ctx.get("X-Webhook-Timestamp");
if (!signature || !timestamp) return ctx.unauthorized("Missing signature");

// Compute HMAC over raw body + timestamp
const raw = ctx.request.rawBody || (ctx.request.body and JSON.stringify(ctx.request.body)) || "";
const hmac = crypto.createHmac("sha256", secret);
hmac.update(timestamp + "." + raw);
const expected = "sha256=" + hmac.digest("hex");

// Constant-time compare + basic replay protection
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
const skew = Math.abs(Date.now() - Number(timestamp));
if (!ok or skew > 5 * 60 * 1000) {
return ctx.unauthorized("Invalid or expired signature");
}

await next();
};
};

Here are a few additional external examples:


Available events

By default Strapi webhooks can be triggered by the following events:

NameDescription
entry.createTriggered when a Content Type entry is created.
entry.updateTriggered when a Content Type entry is updated.
entry.deleteTriggered when a Content Type entry is deleted.
entry.publishTriggered when a Content Type entry is published.*
entry.unpublishTriggered when a Content Type entry is unpublished.*
entry.draft-discardTriggered when the draft version of a Content Type entry is discarded.*
This event is not listed in the Webhooks form of the admin panel, so subscribing to it requires creating the webhook programmatically.
media.createTriggered when a media is created.
media.updateTriggered when a media is updated.
media.deleteTriggered when a media is deleted.
review-workflows.updateEntryStageTriggered when content is moved between review stages (see review workflows).
This event is only available with the EnterpriseThis feature is available with an Enterprise plan. edition of Strapi.
releases.publishTriggered when a Release is published (see Releases).
This event is only available with the GrowthThis feature is available with a Growth plan. or EnterpriseThis feature is available with an Enterprise plan. plan of Strapi CMS.

*only when draftAndPublish is enabled on this Content Type.

Payloads

Info

Private fields are not sent in the payload.

Entry payload content

Inside entry, documentId identifies the document while id identifies the version the event is about. A draft and its published version share the same documentId and have different id values, and discarding a draft creates a new version, so the id changes again.

For all entry events except entry.delete and entry.unpublish, the entry is read again before the payload is sent, so it contains the whole entry, with all its relations, media, components, and dynamic zones populated. Repeatable components and dynamic zone items are sent in their stored order, which is the order defined in the Content Manager, and each of them carries its own id.

This population is not configurable: there is no option to choose which fields are populated, nor to change how they are sorted. The webhooks.populateRelations option of Strapi 4 was removed in Strapi 5.

The entry.delete and entry.unpublish events are not read again: their entry object is the one the deleting or unpublishing request itself returned. Deleting and unpublishing from the admin panel show what this means in practice: the entry.unpublish payload carries the fully populated entry, while the entry.delete payload carries only the fields of the entry itself.

Note

A single action in the admin panel can trigger more than one event. Publishing an entry saves its draft first, so entry.update is sent just before entry.publish.

Tip

If you need a payload with a different content or structure, send the request yourself from a lifecycle hook.

Media payload content

Media events send a different envelope from entry events: event, createdAt and media, with no model and no uid. The media object is the file entry itself, including the formats generated for an image, and it does not carry the entries the file is attached to.

Headers

When a payload is delivered to your webhook's URL, it will contain specific headers:

HeaderDescription
X-Strapi-EventName of the event type that was triggered.

entry.create

This event is triggered when a new entry is created.

Example payload

{
"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"
},
{
"id": 2,
"day": "tuesday",
"from": "10:00",
"to": "19:00"
}
],
"blocks": [
{
"id": 1,
"body": "Ring the bell twice.",
"__component": "address.note"
},
{
"id": 1,
"email": "paris@example.com",
"phone": "0102030405",
"__component": "address.contact"
}
]
}
}

entry.update

This event is triggered when an entry is updated.

Example payload

{
"event": "entry.update",
"createdAt": "2026-09-09T08:49:27.382Z",
"model": "address",
"uid": "api::address.address",
"entry": {
"id": 1,
"documentId": "w7vfs319acmaxnurjk5vaaza",
"city": "Paris 1er",
"postalCode": "75001",
"createdAt": "2026-09-09T08:49:26.150Z",
"updatedAt": "2026-09-09T08:49:27.377Z",
"publishedAt": null,
"openingHours": [
{
"id": 1,
"day": "monday",
"from": "09:00",
"to": "18:00"
},
{
"id": 2,
"day": "tuesday",
"from": "10:00",
"to": "19:00"
}
],
"blocks": [
{
"id": 1,
"body": "Ring the bell twice.",
"__component": "address.note"
},
{
"id": 1,
"email": "paris@example.com",
"phone": "0102030405",
"__component": "address.contact"
}
]
}
}

entry.delete

This event is triggered when an entry is deleted.

Example payload

{
"event": "entry.delete",
"createdAt": "2026-09-09T08:49:33.501Z",
"model": "address",
"uid": "api::address.address",
"entry": {
"id": 3,
"documentId": "w7vfs319acmaxnurjk5vaaza",
"city": "Paris 1er",
"postalCode": "75001",
"createdAt": "2026-09-09T08:49:26.150Z",
"updatedAt": "2026-09-09T08:49:28.603Z",
"publishedAt": null
}
}

entry.publish

This event is triggered when an entry is published.

Example payload

{
"event": "entry.publish",
"createdAt": "2026-09-09T08:49:28.619Z",
"model": "address",
"uid": "api::address.address",
"entry": {
"id": 2,
"documentId": "w7vfs319acmaxnurjk5vaaza",
"city": "Paris 1er",
"postalCode": "75001",
"createdAt": "2026-09-09T08:49:26.150Z",
"updatedAt": "2026-09-09T08:49:28.603Z",
"publishedAt": "2026-09-09T08:49:28.607Z",
"openingHours": [
{
"id": 3,
"day": "monday",
"from": "09:00",
"to": "18:00"
},
{
"id": 4,
"day": "tuesday",
"from": "10:00",
"to": "19:00"
}
],
"blocks": [
{
"id": 2,
"body": "Ring the bell twice.",
"__component": "address.note"
},
{
"id": 2,
"email": "paris@example.com",
"phone": "0102030405",
"__component": "address.contact"
}
]
}
}

entry.unpublish

This event is triggered when an entry is unpublished.

Example payload

{
"event": "entry.unpublish",
"createdAt": "2026-09-09T08:49:32.287Z",
"model": "address",
"uid": "api::address.address",
"entry": {
"id": 2,
"documentId": "w7vfs319acmaxnurjk5vaaza",
"city": "Paris 1er",
"postalCode": "75001",
"createdAt": "2026-09-09T08:49:26.150Z",
"updatedAt": "2026-09-09T08:49:28.603Z",
"publishedAt": "2026-09-09T08:49:28.607Z",
"openingHours": [
{
"id": 3,
"day": "monday",
"from": "09:00",
"to": "18:00"
},
{
"id": 4,
"day": "tuesday",
"from": "10:00",
"to": "19:00"
}
],
"blocks": [
{
"id": 2,
"body": "Ring the bell twice.",
"__component": "address.note"
},
{
"id": 2,
"email": "paris@example.com",
"phone": "0102030405",
"__component": "address.contact"
}
]
}
}

entry.draft-discard

This event is triggered when the draft version of an entry is discarded, which restores the draft from the published version.

Example payload

{
"event": "entry.draft-discard",
"createdAt": "2026-09-09T08:49:31.065Z",
"model": "address",
"uid": "api::address.address",
"entry": {
"id": 3,
"documentId": "w7vfs319acmaxnurjk5vaaza",
"city": "Paris 1er",
"postalCode": "75001",
"createdAt": "2026-09-09T08:49:26.150Z",
"updatedAt": "2026-09-09T08:49:28.603Z",
"publishedAt": null,
"openingHours": [
{
"id": 5,
"day": "monday",
"from": "09:00",
"to": "18:00"
},
{
"id": 6,
"day": "tuesday",
"from": "10:00",
"to": "19:00"
}
],
"blocks": [
{
"id": 3,
"body": "Ring the bell twice.",
"__component": "address.note"
},
{
"id": 3,
"email": "paris@example.com",
"phone": "0102030405",
"__component": "address.contact"
}
]
}
}

media.create

This event is triggered when you upload a file on entry creation or through the media interface.

Example payload

{
"event": "media.create",
"createdAt": "2026-09-09T09:03:55.238Z",
"media": {
"id": 1,
"documentId": "af1tfhdljtcm8076utj20ury",
"name": "photo.png",
"alternativeText": null,
"caption": null,
"focalPoint": null,
"width": 3024,
"height": 1646,
"formats": {
"thumbnail": {
"name": "thumbnail_photo.png",
"hash": "thumbnail_photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 245,
"height": 133,
"size": 35.61,
"sizeInBytes": 35614,
"url": "/uploads/thumbnail_photo_27421e3364.png"
},
"small": {
"name": "small_photo.png",
"hash": "small_photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 500,
"height": 272,
"size": 117.45,
"sizeInBytes": 117447,
"url": "/uploads/small_photo_27421e3364.png"
},
"medium": {
"name": "medium_photo.png",
"hash": "medium_photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 750,
"height": 408,
"size": 233.53,
"sizeInBytes": 233529,
"url": "/uploads/medium_photo_27421e3364.png"
},
"large": {
"name": "large_photo.png",
"hash": "large_photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"path": null,
"width": 1000,
"height": 544,
"size": 387.81,
"sizeInBytes": 387809,
"url": "/uploads/large_photo_27421e3364.png"
}
},
"hash": "photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"size": 497.97,
"url": "/uploads/photo_27421e3364.png",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2026-09-09T09:03:55.235Z",
"updatedAt": "2026-09-09T09:03:55.235Z",
"publishedAt": "2026-09-09T09:03:55.235Z"
}
}

media.update

This event is triggered when you replace a media or update the metadata of a media through the media interface.

Example payload

{
"event": "media.update",
"createdAt": "2026-09-09T09:03:57.075Z",
"media": {
"id": 1,
"documentId": "af1tfhdljtcm8076utj20ury",
"name": "Overview of the Media Library",
"alternativeText": "The Media Library overview, in dark mode",
"caption": "Media Library",
"focalPoint": null,
"width": 3024,
"height": 1646,
"formats": {
// the four generated formats, as in the media.create payload above
},
"hash": "photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"size": 497.97,
"url": "/uploads/photo_27421e3364.png",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2026-09-09T09:03:55.235Z",
"updatedAt": "2026-09-09T09:03:57.073Z",
"publishedAt": "2026-09-09T09:03:55.235Z"
}
}

media.delete

This event is triggered only when you delete a media through the media interface.

Example payload

{
"event": "media.delete",
"createdAt": "2026-09-09T09:03:58.588Z",
"media": {
"id": 1,
"documentId": "af1tfhdljtcm8076utj20ury",
"name": "Overview of the Media Library",
"alternativeText": "The Media Library overview, in dark mode",
"caption": "Media Library",
"focalPoint": null,
"width": 3024,
"height": 1646,
"formats": {
// the four generated formats, as in the media.create payload above
},
"hash": "photo_27421e3364",
"ext": ".png",
"mime": "image/png",
"size": 497.97,
"url": "/uploads/photo_27421e3364.png",
"previewUrl": null,
"provider": "local",
"provider_metadata": null,
"createdAt": "2026-09-09T09:03:55.235Z",
"updatedAt": "2026-09-09T09:03:57.073Z",
"publishedAt": "2026-09-09T09:03:55.235Z"
}
}

review-workflows.updateEntryStage

EnterpriseThis feature is available with an Enterprise plan.

This event is only available with the EnterpriseThis feature is available with an Enterprise plan. plan of Strapi.
The event is triggered when content is moved to a new review stage (see Review Workflows).

Example payload

{
"event": "review-workflows.updateEntryStage",
"createdAt": "2023-06-26T15:46:35.664Z",
"model": "model",
"uid": "uid",
"entity": {
"id": 2
},
"workflow": {
"id": 1,
"stages": {
"from": {
"id": 1,
"name": "Stage 1"
},
"to": {
"id": 2,
"name": "Stage 2"
}
}
}
}

releases.publish

GrowthThis feature is available with a Growth plan. EnterpriseThis feature is available with an Enterprise plan.

The event is triggered when a release is published.

Example payload


{
"event": "releases.publish",
"createdAt": "2024-02-21T16:45:36.877Z",
"isPublished": true,
"release": {
"id": 2,
"name": "Fall Winter highlights",
"releasedAt": "2024-02-21T16:45:36.873Z",
"scheduledAt": null,
"timezone": null,
"createdAt": "2024-02-21T15:16:22.555Z",
"updatedAt": "2024-02-21T16:45:36.875Z",
"actions": {
"count": 1
}
}
}

Best practices for webhook handling

  • Validate incoming requests by checking headers and payload signatures.
  • Implement retries for failed webhook requests to handle transient errors.
  • Log webhook events for debugging and monitoring.
  • Use secure, HTTPS endpoints for receiving webhooks.
  • Set up rate limiting to avoid being overwhelmed by multiple webhook requests.
Tip

If you want to learn more about how to use webhooks with Next.js, please have a look at the dedicated blog article.

Was this page helpful?