Skip to main content

网络钩子

🌐 Webhooks

Page summary:

Webhooks 让 Strapi 在内容发生变化时通知外部系统,同时为了隐私省略 Users 类型。在 config/server 中的配置设置了默认的头信息和触发第三方处理的端点。

Webhook 是一种由应用使用的结构,用于通知其他应用某个事件已经发生。更准确地说,webhook 是用户定义的 HTTP 回调。使用 webhook 是告知第三方提供商开始某些处理(持续集成、构建、部署……)的好方法。

🌐 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 ...).

Webhook 的工作方式是通过 HTTP 请求(通常是 POST 请求)向接收应用传递信息。

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

用户内容类型的网页钩子​

🌐 User content-type webhooks

为了防止无意中将任何用户的信息发送到其他应用,Webhooks 将不会对用户内容类型起作用。如果你需要向其他应用通知用户集合的更改,可以通过使用 ./src/index.js 示例创建 生命周期钩子 来实现。

🌐 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

你可以在文件 ./config/server 中设置 webhook 配置。

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

  • webhooks
    • defaultHeaders:你可以为你的 webhook 请求设置默认头部。此选项会被 webhook 本身设置的头部覆盖。

示例配置

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

Webhooks 安全​

🌐 Webhooks security

大多数时候,Webhook 会向公共 URL 发出请求,因此有人可能会找到该 URL 并向其发送错误信息。

🌐 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.

为了防止这种情况发生,你可以发送带有身份验证令牌的头信息。使用管理面板时,你必须对每个 webhook 都这样做。

🌐 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.

另一种方法是定义 defaultHeaders 以添加到每个 webhook 请求中。

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

你可以通过更新 ./config/server 文件来配置这些全局头信息:

🌐 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",
},
},
};

如果你自己开发 Webhook 处理程序,你现在可以通过读取标头来验证令牌。

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

验证签名​

🌐 Verifying signatures

除了认证头之外,建议对 webhook 负载进行签名并在服务器端验证签名,以防止篡改和重放攻击。为此,你可以使用以下指南:

🌐 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:

  • 生成共享密钥并将其存储在环境变量中。
  • 让发送方对原始请求正文加上时间戳计算 HMAC(例如,SHA-256)。
  • 在头信息中发送签名(和时间戳)(例如,X‑Webhook‑Signature,X‑Webhook‑Timestamp)
  • 收到请求后,重新计算 HMAC 并使用恒定时间检查进行比较。
  • 如果签名无效或时间戳过旧,无法避免重放攻击,则拒绝请求。
示例:验证 HMAC 签名(Node.js)

这是一个最小的 Node.js 中间件示例(伪代码),显示 HMAC 验证:

/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

默认情况下,Strapi webhook 可以由以下事件触发:

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

名称描述
entry.create当创建内容类型条目时触发。
entry.update当内容类型条目被更新时触发。
entry.delete当内容类型条目被删除时触发。
['entry.publish'](#entrypublish)当发布内容类型条目时触发。*
entry.unpublish当内容类型条目被取消发布时触发。*
['entry.draft-discard'](#entry-draft-discard)当内容类型条目的草稿版本被丢弃时触发。*
此事件未列在管理员面板的Webhooks形式中,因此订阅该事件需通过程序创建webhook。
media.create当媒体被创建时触发。
media.update当媒体被更新时触发。
media.delete当媒体被删除时触发。
review-workflows.updateEntryStage当内容在审核阶段之间移动时触发(参见 审核工作流程)。
此事件仅适用于 Strapi 的 EnterpriseThis feature is available with an Enterprise plan. 版本。
releases.publish当发布 Release 时触发(参见 Releases)。
此事件仅在 Strapi CMS 的 GrowthThis feature is available with a Growth plan. 或 EnterpriseThis feature is available with an Enterprise plan. 计划中可用。

*仅当此内容类型上的 draftAndPublish 已启用时。

有效载荷​

🌐 Payloads

Info

私有字段不会在有效负载中发送。

🌐 Private fields are not sent in the payload.

入口有效载荷内容​

🌐 Entry payload content

在 entry 内,documentId 用来标识文档,而 id 用来标识该事件所涉及的版本。草稿和它的已发布版本有相同的 documentId,但 id 不同,丢弃草稿会创建一个新版本,所以 id 又会发生变化。

🌐 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.

对于除了 entry.delete 和 entry.unpublish 之外的所有 entry 事件,在发送载荷之前会再次读取该 entry,因此它包含整个 entry,以及它的所有关联、媒体、组件和动态区。可重复组件和动态区的条目会按它们在内容管理器中定义的顺序发送,并且每个条目都有自己的 id。

🌐 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.

这个人群是不可配置的:没有选项可以选择填充哪些字段,也无法更改它们的排序方式。Strapi 4 的 webhooks.populateRelations 选项在 Strapi 5 中被移除了。

🌐 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.

entry.delete 和 entry.unpublish 事件不会被再次读取:它们的 entry 对象就是删除或撤销发布请求本身返回的那个。从管理面板上进行删除和撤销发布可以看到这在实际中的意义:entry.unpublish 的数据负载包含完整的条目,而 entry.delete 的数据负载只包含条目本身的字段。

🌐 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

管理面板中的一个操作可能会触发多个事件。发布条目会先保存其草稿,因此 entry.update 会在 entry.publish 之前发送。

🌐 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

媒体事件发送的信封不同于条目事件:event、createdAt 和 media,没有 model 也没有 uid。media 对象就是文件条目本身,包括为图片生成的 formats,它不包含文件附加的条目。

🌐 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

当有效负载传递到你的 webhook 的 URL 时,它将包含特定标头:

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

标题描述
X-Strapi-Event被触发的事件类型的名称。

entry.create​

创建新条目时会触发此事件。

🌐 This event is triggered when a new entry is created.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

示例有效负载

{
"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.

此事件仅适用于 Strapi 的 EnterpriseThis feature is available with an Enterprise plan. 计划。
当内容被移动到新的审核阶段时,此事件将被触发(参见 审核工作流)。

示例有效负载

{
"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.

当发布release时,将触发该事件。

🌐 The event is triggered when a release is published.

示例有效负载


{
"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
}
}
}

Webhook 处理的最佳实践​

🌐 Best practices for webhook handling

  • 通过检查标头和有效负载签名来验证传入请求。
  • 对失败的 webhook 请求实现重试以处理瞬态错误。
  • 记录 webhook 事件以进行调试和监控。
  • 使用安全的 HTTPS 端点接收 webhook。
  • 设置速率限制以避免被多个 webhook 请求淹没。
Tip

如果你想了解更多关于如何在 Next.js 中使用 webhooks 的信息,请查看专门的博客文章。

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