Guide to Strapi Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Admin panel Settings > Webhooks, or programmatically |
| Default headers | webhooks.defaultHeaders in config/server, overridden by headers set on a webhook |
| Method and format | POST, Content-Type: application/json |
| Discriminator | X-Strapi-Event header and the body's event field |
| Authentication | A static header you choose, such as Authorization: Bearer <token>. No signature |
| Payload | Entry events send the fully populated entry, except entry.delete and entry.unpublish |
| Excluded data | Private fields and the User content-type |
| Delivery | 10 second timeout, no retries |
| Testing | Trigger button on a webhook sends a trigger-test event |
Events
| Event | Fires when |
|---|---|
entry.create | An entry is created |
entry.update | An entry is updated |
entry.delete | An entry is deleted |
entry.publish | An entry is published (Draft & Publish only) |
entry.unpublish | An entry is unpublished (Draft & Publish only) |
entry.draft-discard | The draft version of an entry is discarded (Draft & Publish only). Not in the admin form, so the webhook has to be created programmatically |
media.create | A media file is uploaded |
media.update | A media file is updated |
media.delete | A media file is deleted |
review-workflows.updateEntryStage | Content moves between review stages. Enterprise edition only |
releases.publish | A 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:
documentIdidentifies the document andididentifies the version the event is about. A draft and its published version share adocumentIdbut have differentidvalues, and discarding a draft creates a new version with a newid. Key your records ondocumentId.- For every entry event except
entry.deleteandentry.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 4webhooks.populateRelationsoption was removed in Strapi 5. entry.deleteandentry.unpublishsend the entry as the request that deleted or unpublished it returned. From the admin panel,entry.deletecarries only the entry's own fields.- One editor action can send more than one event. Publishing saves the draft first, so
entry.updatearrives just beforeentry.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.
- In the admin panel, go to Settings > Webhooks and click Add new webhook.
- Enter a name and your endpoint's HTTPS URL.
- Under Headers, add
Authorizationwith the valueBearer <your token>, or a custom header such asX-Strapi-Webhook-Token. - Select the events you want.
- Save, then click Trigger to send a
trigger-testevent 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.unpublishandentry.delete, notentry.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
uidanddocumentIdfrom 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.