关于 webhook-platform-design
面向平台团队的出站 Webhook 设计技能,涵盖事件、签名、投递保障、重试、订阅生命周期和故障排查。它将消费方需求与建设或采购路径纳入同一套分阶段设计流程。
主要功能
- 01事件分类与目录设计
- 02载荷封装和模式版本管理
- 03签名机制设计
- 04投递保证、重试与死信处理
- 05订阅生命周期和投递调试
适用场景
- 01规划新 Webhook 平台
- 02完善事件投递与重试契约
- 03设计订阅者调试和重放能力
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ developer-platform-skills --skill webhook-platform-design安装后在 Claude Code 中输入/webhook-platform-design确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/event-taxonomy-and-payload-design.mdretry-policy-vendor-comparison.mdsigning-scheme-reference.mdsubscription-lifecycle-comparison.md详细介绍
Webhook 平台设计
你是一名出站 Webhook 平台设计师。设计平台如何将事件通知发送到客户的服务器——事件有哪些、如何签名、如何保证送达并重试、订阅如何创建和终止,以及集成方如何排查未能送达的通知。
Svix 指出了真正的工作范围:“发送 HTTP POST 很容易,难的是在大规模场景下可靠地完成。”这两句话之间的差距——慢速消费者隔离、退避重试、死信处理、签名、防重放——正是这项技能要设计的内容。
对包括本项在内的所有界面进行全面一致性审查,属于 samber/developer-platform-skills@public-api-design-review 的职责;本技能负责 Webhook 界面自身的设计。至于 Webhook 是否是值得构建的界面,以及它与其他集成界面形式相比如何,则由 samber/developer-platform-skills@api-integration-surface-strategy 决定。
记忆(建议): 如果记忆保存在文件中,可考虑使用 developer-platform-context.md;如果使用其他记忆系统,则以该系统为准。该文件仅供参考,并非强制要求。请将不同任务的信息放在不同章节中。完成的任务应予以删除。任务信息应注明日期;一般项目背景无需注明日期。不同任务的访谈回答可能有所不同。
澄清问题
在开始任何设计前先询问这些问题,并集中提问——这是一项战术设计任务,而不是战略访谈。
- 是从零开始,还是改造现有系统?如果是改造,请索取当前事件列表、重试行为、签名方案,以及现有的任何投递日志。
- 规模大约是多少:有多少订阅端点(几十、几千,还是更多),峰值每秒多少个事件?(这会影响扇出隔离,以及是否有必要讨论 FIFO。)
- 哪些类型的使用方会集成:企业集成方、一般 API 开发者、无代码工作流工具,还是混合类型?(见下一节。)
- 工作量上限和数据约束:你能为投递基础设施投入多少工程资源——团队人数、常态化值班能力、运营预算?客户事件数据是否可以经由第三方处理方传输?(见“自建还是购买”。)
- 截止日期和收益形态:Webhook 必须在什么日期前上线?这是快速推出的一项功能,还是未来多年持续累积价值的集成界面?
- 需求中是否有一部分实际上需要面向客户端的交互式流式传输(实时仪表板、聊天)?这属于 GraphQL 订阅/WebSocket 的范畴——与 Webhook 互补,而非竞争。Webhook 负责持久的服务器到服务器投递,事件触发时使用方可能处于离线状态;订阅负责亚秒级客户端推送,可容忍偶尔丢失。平台完全可以同时提供两者。
之所以提出问题 4-5,是因为自建与购买的选择以及重试方案在生效时间、持久性和工作量方面差异很大——没有这些信息,无法确定任何一种方案。严格的上线日期更适合托管基础设施和较短的重试周期;以长期累积价值为目标的集成界面,则更适合由你自行运营和调优的管道。
如果你的工具环境支持持久化记忆,请保存投递契约的核心决策(保证级别、顺序位置、重试计划、确认超时、去重 ID 位置、签名方案),这样后续运行——添加新事件类型、更改重试策略、整理文档——就能以该契约为起点,而不必重新推导。
消费者成熟度
Webhook 消费者是服务器,无论谁购买了产品。设计会根据消费者的成熟度划分,而与大多数受众划分不同,这些细分并非层层嵌套——每一类都有自己的要求:
- 企业集成方——要求以书面形式明确交付约定:保证语义、重试计划、签名规范、确认超时。他们会要求 FIFO,以及非对称签名(使用公钥验证,而不是持有你们的共享密钥)。
- 典型 API 开发者——通过文档和门户自助完成集成。他们需要能处理原始请求体陷阱的验证代码片段、用于去重的稳定事件 ID,以及在处理程序故障一小时后从控制面板重放事件的功能。
- 无代码消费者(工作流自动化工具)——以捕获 URL 的方式思考:“在我开始集成前,先给我看一个真实的负载。”他们从不阅读签名规范;对他们来说,发送测试事件和查看最近的投递记录远比文字说明重要。
要服务所有实际存在的细分群体。即使某项设计足以满足企业集成方的需求,如果没有测试事件和投递日志,也无法满足无代码消费者。
自建还是购买
在第 1 步之前就要做出决定——这将决定第 4-6 步是自行构建还是仅进行配置。有三种选择,排名如下:
- 工作量:
自建 > 可自托管的开源 Webhook 网关 > 托管发送基础设施 - 价值(首日即可投入使用的交付管道):
托管 > 可自托管网关 > 自建 - 控制力(数据驻留、自定义语义):
自建 == 可自托管网关 > 托管——两者都会将事件数据留在你的基础设施上;自建还能提供自定义语义,但成本高得多 - 合规成本:
托管 > 可自托管网关 == 自建——托管服务会触发 DPA 和数据驻留审查,之后若要改用其他方案,就意味着必须重新签订合同并迁移正在运行的端点;另外两种方案都能同等地将事件数据保留在自己的基础设施上 - 效率:
托管 > 可自托管网关 > 自建 - 默认选择:托管发送基础设施,适用于 Webhook 是产品的一项功能,而非产品本身的情况——Svix、Hookdeck、Convoy 这一类供应商(提及名称仅作参考,并非指定要求;请评估你所在生态中提供同类服务的供应商)。Svix 自行估算,在内部复刻其功能集需要投入数个工程师年,而非数个工程师周——3-5 名工程师耗时 6-12 个月,此外还需要长期安排值班人员。请将具体数字视为供应商的自身利益陈述:实际构建本身更接近数个工程师月,且大部分时间会花在评审和负载测试上。长期值班需求不会减少,因此内部自建仍是成本最高的选择。
- 合规要求排除第三方传输,但你能运维软件时,转而选择可自托管网关。在这种情况下,托管方案应当删除,而不是降级:如果问题 4 的答案是“客户事件数据绝不能经由第三方处理商传输”,就应将该方案从选项中移除,而不是排在最后,因为因合规原因而被搁置的方案,会在下次期限迫近时再次进入范围——而这次不会经过当初将其排除的法律审查。重新纳入的触发条件:已签署 DPA,且数据驻留方案涵盖该供应商。
- 自建是资源不足的选择:控制力最高,工作量最大,每一轮效率评比都落后。若 Webhook 交付本身就是你销售的产品,或第三方传输和第三方代码都被排除,则应提升其优先级。无论选择哪种方案,第 1-7 步中的每项设计决策仍须做出——购买基础设施只是买下运行机制,并不会替你确定分类体系、约定或文档。
- 这个排名是默认方案,不是铁律。请根据访谈结果重新排序:严格的交付期限会让托管方案优先;而已经在大规模运行持久化队列的内部团队,构建成本会远低于估算值,从而颠倒工作量排名。
工作流
- 定义事件分类和目录。
- 设计负载封装和架构版本控制方案。
- 采用签名方案。
- 设定交付约定:保证、重试策略、死信处理。
- 建模订阅生命周期。
- 构建调试界面:日志、重放、测试事件。
- 根据五要素标准记录所有内容。
以下各节按顺序介绍每个步骤。
1. 事件分类和目录
- 使用“对象-动作”模式命名事件:
resource.action,动作使用过去时(invoice.paid,绝不使用invoice.pay),以点分隔,标识符仅限于[a-zA-Z0-9_.]。这是 Standard Webhooks 的建议,也是 Stripe 的惯例。生产平台在分隔符和判别字段的位置上各不相同,因此应在发布时选定一种约定,并始终保持一致。像 GitHub 那样将事件名称拆分到标头和正文中,意味着没有单一字符串可以命名该事件——应避免这样做。 - 目录就是契约:每种事件类型在发布前都必须有稳定的名称、说明和负载架构。订阅者按目录名称选择要接收的内容,因此,未记录的事件类型实际上无法订阅。
- 根据五项设计原则检查每个事件:自包含、不可变(添加字段,绝不重新赋予字段用途)、可追踪、可操作、可调试。
- 明确决定子资源事件是否向上冒泡为父级事件(Stripe 的做法是绝不冒泡——
customer.subscription.updated触发时不会触发customer.updated)。无论采用哪种方式,都要明确说明;对此保持沉默会导致订阅者自行假设。
有关供应商命名方式对比及应用这些设计原则的检查清单,请参阅 references/event-taxonomy-and-payload-design.md。
2. 负载封装和架构版本控制
- 审慎选择封装的位置:所有元数据都放在 JSON 正文中(Stripe),或将元数据放在 HTTP 标头中,而正文中仅包含资源数据(GitHub、Shopify、Standard Webhooks)。采用标头拆分设计时,交付日志必须同时记录标头和正文,才能忠实地重放事件——从第一天起就按此构建日志,而不是事后补救。
- 将精简负载与完整负载的选择视为首要设计维度:完整负载会嵌入资源状态;精简负载只携带标识符,并要求通过 API 获取数据。Standard Webhooks 建议保持负载较小,通常低于 ~20kb。精简负载也能绕开版本控制问题:API 版本升级时,通知的结构不会改变。
- 审慎选择两种架构版本控制方案之一:将每个 Webhook 端点固定到一个 API 版本,并按该版本生成负载(Stripe 的模式);或对事件类型本身进行版本控制——每次发生不兼容更改时使用新的类型名称,其他情况下仅进行增量演进(Svix/Standard Webhooks 的模式)。意外地同时采用两种方案才是问题所在;API 版本升级绝不能悄然改变订阅者已存储的事件负载结构。
- 了解标准的分层方式——它们各司其职,并非相互竞争:Standard Webhooks 负责签名和交付标头,CloudEvents 负责封装,AsyncAPI 和 OpenAPI 3.1 的
webhooks部分负责描述。在同一份 OpenAPI 文档中记录出站事件和入站 API;只有在必须与已支持 CloudEvents 的事件网格互操作时,才采用 CloudEvents。
有关封装方案的详细说明、Stripe 精简负载/快照双轨迁移模式以及标准对照图,请参阅 references/event-taxonomy-and-payload-design.md。
3. 签名方案
签名方案由你负责设计——签署什么内容、如何签署,以及使用方如何验证。签名密钥的签发、存储和轮换基础设施归同级项目 samber/developer-platform-skills@api-auth-key-management 所有。
- 采用 Standard Webhooks 规范,不要另行发明专有方案——在这个碎片化的领域中,它最接近行业标准,并且已被主要 API 平台广泛采用(依据自我报告)。每次投递都要包含三个标头:
webhook-id、webhook-timestamp、webhook-signature。 - 签名构造:对
{id}.{timestamp}.{raw_body}计算 HMAC-SHA256,再进行 base64 编码,并为算法添加版本前缀(v1,...)。对时间戳签名,才能防止重放——接收方会拒绝时间戳超出容差窗口的投递(规范建议为 5 分钟)。仅对请求体签名(GitHub 和 Shopify 采用的方式)会永久有效,因此捕获到的请求可以被无限期重放。 - 为密钥添加前缀,方便扫描工具识别泄露的密钥:对称 HMAC 密钥使用
whsec_,非对称 ed25519 私钥/公钥使用whsk_/whpk_(签名 IDv1a)。提供非对称签名,供不应持有共享密钥的使用方使用——当你无法控制通信双方时,推荐采用此方案。 - 将零停机轮换纳入设计:
webhook-signature标头可携带多个以空格分隔的签名,因此在轮换窗口内,新旧密钥都可以同时验证。平台若只允许一个有效密钥,就会迫使订阅者先撤销密钥再部署,从而导致一段时间内投递失败。 - 签名文档必须涵盖导致集成失败的三个主要使用方问题:
- 无一例外地验证每个请求,包括仅处理通知的处理程序。
- 在任何 JSON 解析器处理请求体之前,先使用原始请求体验证。
- 将 Webhook 路由从会话身份验证中间件中排除,因为它通过签名而非会话进行身份验证。
请参阅 references/signing-scheme-reference.md,了解完整的构造示例、与四个主要未采用该规范的平台的方案对比,以及轮换流程。
4. 投递约定:保证、重试、死信
- 明确承诺至少一次投递,并说明这是经过深思熟虑的选择:持续重试,直到收到确认,因此订阅方处理事件后、在返回 200 之前崩溃时,会看到该事件两次。绝不要声称恰好一次——这个市场上所有“恰好一次”的说法,本质上都是至少一次加上去重工具。从一开始就提供诚实的实现:稳定的事件 ID(同一事件的所有重试都使用相同的值),并附上去重建议。之后再补上 ID,无法帮助已经在没有 ID 的情况下完成集成的订阅方。
- 明确说明排序策略:默认采用尽力而为、不保证顺序才是正确做法——重试和并行工作线程会打乱事件顺序,而严格排序意味着一次失败的投递会阻塞整个端点队列。所有主要平台默认都不保证先进先出;只有对顺序有严格要求的使用方,才提供明确选择启用的先进先出模式。提醒订阅方不要根据时间戳推断顺序。
- 通过持久化队列将投递与请求路径解耦,并按端点隔离——一个缓慢或故障的订阅方绝不能延迟其他订阅方的投递。这种扇出隔离是平台向自家工程团队提供的核心价值。
- 选择重试策略——这是实际平台之间差异最大的方面:
- 工作量:
可配置的每端点计划 + 断路器 > 固定的天级计划 + DLQ + 自动停用 > 固定的小时级计划 > 仅手动重新投递 - 价值(无需人工操作即可恢复的投递):
可配置计划 + 断路器 > 固定的天级计划 > 固定的小时级计划 > 仅手动重新投递 - 效率:
固定的天级计划 > 固定的小时级计划 > 可配置计划 + 断路器 > 仅手动重新投递 - 默认级别:采用固定的指数退避计划,总跨度约为一天——Svix 采用的方案(约 27.5 小时内尝试 8 次),以及 Stripe 的近似方案(最长 3 天);Shopify 在 4 小时内尝试 8 次,属于较短的一端。应根据订阅方实际故障持续时间来确定总跨度——部署需要几分钟,服务中断可能持续数小时——而不是随便选个整数。必须采用指数退避,不能线性退避:固定间隔重试会给正在苦苦支撑的端点造成压力。
- 当端点数量增长到持续故障的端点会对投递管道造成明显负载,或者企业使用方协商投递 SLA 时,升级为可配置计划加断路器(恢复前先以半开状态进行探测,Convoy 风格)。价值高、工作量大——只有满足这两个条件后才升级到这一尚未普及的选项。
- 仅手动重新投递(GitHub 的模式:完全不自动重试)是明确的反模式,而非精简方案——GitHub 之所以可行,是因为它同时提供投递列表、重新投递 API,以及明确说明“自行构建对账流程”的文档。只照搬不重试的做法,却不提供 API,就是在交付不可靠的投递。
- 这个排序是默认方案,不是铁律——应根据访谈结果重新排序:如果使用方大多不会编写代码,就应降低可配置性的重要性,因为没人会去调整;如果属于支付级领域,就应优先采用更长的重试跨度和断路器。
- 工作量:
- 对订阅方的响应进行分类,而不是一概而论:
- 收到 2xx 后确认并停止重试。按照 Shopify/GitHub 的惯例,预计在约 10 秒内收到确认。
- 区分“处理程序较慢”和“处理程序报错”——订阅方将工作排入队列后快速返回,正是你应该建议它们采用的做法。
- 对 429 响应优先遵循
Retry-After标头,而不是使用自己的退避曲线——大多数发送方都不会这样做,因此这是一项差异化优势。 - 对不可重试的 4xx(除 408/429 外的所有状态码),应直接发送到死信队列——对永久性故障进行重试会浪费预算,还会显得像攻击行为。
- 仅在以下两种情况下写入死信:
- 重试预算耗尽。
- 出现不可重试的错误类别。 将死信事件保留 7-30 天,包含用于调查和重放的完整上下文(包括标头)。持续故障一段时间后,自动停用端点,并通过其他渠道通知订阅方——例如电子邮件或控制面板,因为有问题的正是 Webhook 通道。
- 建议高风险事件类型的使用方定期与你的 API 对账——这是供应商认可的实践,而非权宜之计。文档中明确提出这一预期,比暗示投递能力强于实际上承诺的至少一次/尽力而为更诚实。 参阅 references/retry-policy-vendor-comparison.md,了解六家供应商文档中记载的重试计划、超时值、自动禁用行为,以及断路器参考设计。
5. 订阅生命周期
-
将订阅(端点注册)建模为具有完整 CRUD 操作的一级有状态对象——绝不要以即发即弃的方式注册。明确设置以下三种状态:
- active
- paused —— 订阅方主动停止,但保留配置
- disabled —— 持续失败后由平台主动禁用
三家专注于基础设施的供应商都采用了这种更丰富的模型;二元的启用/禁用状态是平台初期采用、随后会逐渐淘汰的简化模式。在 API 边界仅公开客户创建的订阅依法可进入的状态——拒绝内部专用状态。
-
保持过滤的两个维度相互独立:事件类型(依据目录)和接收方/渠道范围。没有筛选条件的端点会接收所有内容——明确说明这一点。
-
明确说明两个重放起点:新建订阅从何处开始(仅接收新事件,还是包含保留的积压事件),以及平台端发生故障后订阅从何处恢复。两者完全可以不同。醒目地说明,删除订阅会销毁其重放/投递状态——使用相同名称重新创建订阅会从头开始,而订阅方若通过删除再创建来重置,就会在不知情的情况下丢失积压事件或重复重放。
-
说明配置更改传播延迟属于预期行为(订阅编辑需要一两分钟才能到达投递路径是正常现象),避免集成方将其当作 bug 报告。
参阅 references/subscription-lifecycle-comparison.md,了解六家供应商在状态、筛选和重放方面的对比,以及状态机案例研究。
6. 调试界面
自助式消费者门户是这个市场的基本要求,而非锦上添花——应像标记缺少签名验证文档那样,将“无门户”标记出来。门户应提供:
- 投递日志:可按端点、事件类型和状态筛选,并可检查每次尝试(请求标头、正文、响应代码、耗时)。如果你的封装格式将元数据拆分到标头中(第 2 步),日志就必须存储这些标头,否则重放将无法忠实还原。
- 重放和重试是不同操作——原样采用 Hookdeck 的术语,因为业界会混用这两个词,而你的文档不应如此:_重试_是对同一事件发起新的投递尝试;_重放_则是将原始事件重新摄取为一个新事件。支持单个事件重放和范围重放;大范围重放前应要求用户确认成本,因为重放大量积压事件是一项成本高昂的操作,订阅方可能并无此意。
- 触发测试事件:提供“发送示例事件”操作,并依据每种事件类型的架构生成事件,让集成方在编写一行处理器代码之前就能看到一份真实的载荷——这与无代码工具通过接收 URL 为消费者解决的问题相同。沙盒环境中的测试触发(使用假数据、测试模式密钥)属于兄弟技能
samber/developer-platform-skills@api-test-mode-design的职责;本技能负责投递端按钮及其发送的示例载荷。 - 故障可见性:将已耗尽投递次数的事件按端点和状态代码分组,并发送通知,将其作为待调查的事件呈现,而不是队列中的行。说明“先返回 200 后崩溃”的黑洞情况:处理器确认成功后又抛出异常,相当于告诉你投递成功;任何调试界面都无法查看其内部情况。
参阅 references/subscription-lifecycle-comparison.md,了解各家供应商实际提供的门户和测试工具,以此作为对标基准。
7. 文档
按照业界最佳文档所采用的五项标准评估你的 webhook 文档,并确保发布前五项全部达标:
- 提供可运行代码片段的签名验证。
- 提供包含架构和示例负载的事件目录。
- 重试策略——明确的时间安排,而不是“我们会采用退避机制重试”。
- 故障排查与故障恢复。
- 测试工具。
将交付约定写成明确、易查找的句子:
- 交付保证(至少一次)。
- 顺序保证。
- 重试时间安排。
- 确认超时时间。
- 携带去重 ID 的标头或字段。
以上任何一项含糊不清,都会变成订阅者的假设,继而成为支持工单。应从驱动交付的同一份规范中发布事件目录(OpenAPI 3.1 的 webhooks 部分),避免文档与负载在不知不觉中出现偏差。
故障模式
反模式检查清单——每一项都可直接作为审计发现:
- 没有自动重试,同时也没有提供投递列表与重新投递 API 以及配套的对账文档。
- 在文档中的任何位置声称“恰好一次交付”。
- 隐含保证顺序(或让订阅者自行假设),而非明确说明顺序尽力而为。
- 重试间隔固定不变,或对不可重试的 4xx 响应持续重试,直到耗尽重试次数。
- 签名仅针对请求体,不包含时间戳,因此无法防止重放。
- 只有一个有效签名密钥,导致轮换时必须先撤销,再部署。
- 同一事件在重试期间没有稳定不变的事件 ID。
- 投递日志只存储请求体,而信封中的元数据却放在标头中。
- API 版本升级时,悄无声息地改变现有订阅者已经存储的事件负载结构。
- 删除并重新创建订阅时,悄无声息地重置其重放位置。
- 处理程序虽慢但正确(快速入队、快速确认),却因为从未记录超时时间和快速入队建议而被视为失败。
- 没有订阅者门户:没有日志、重放或测试事件。
衡量标准
有两项二元门槛——反复迭代设计,直到两项均通过:
- 约定完整性:明确记录交付保证、顺序保证、重试时间安排、确认超时时间和去重 ID 位置。五项事实必须全部齐备——缺少任何一项都无法通过门槛,因为每一项缺失都会成为订阅者的假设,并可能引发事故。
- 文档评分标准:发布前必须具备全部五项标准中的内容。缺少任何一项都无法通过门槛。
需要建立的运营指标,应观察其趋势,而非设定通过门槛:
- 每个端点的投递成功率——平台的核心健康指标。
- 死信率——该指标激增意味着某个端点持续故障,与背景中的临时重试不同。
- 故障端点的发现时间——上限由自动禁用/通知窗口决定。以第一个月的数据为基线,并以此为参照持续改进,而不是对照外部阈值。
调用示例
- “我们要为计费 API 添加 webhook——请从头设计事件目录、签名和重试策略。”
- “我们的 webhook 系统连续重试三次后就丢弃事件,客户总是漏收投递——请重新设计重试和死信策略。”
- “企业客户想知道我们的 webhook 如何签名,以及是否保证顺序——请撰写我们应当能向他们展示的交付约定。”
参考资料
-
references/event-taxonomy-and-payload-design.md - 跨供应商命名约定、信封位置分歧、精简与完整负载、模式版本管理路线,以及标准概览。
-
references/signing-scheme-reference.md - Standard Webhooks 构建实例、非采用者的方案对比、轮换流程,以及供你在文档中使用的使用者避坑清单。
-
references/retry-policy-vendor-comparison.md - 六家供应商公开的重试计划、超时、自动禁用行为、死信和断路器模式。
-
references/subscription-lifecycle-comparison.md - 六家供应商的端点状态、筛选、重放/回填及门户/测试工具,另附订阅状态机案例研究。
-
samber/developer-platform-skills@api-idempotency-retry- API 请求侧的一般重试和幂等机制(退避算法、幂等键);此技能仅将这些思路应用于出站交付。 -
samber/developer-platform-skills@integration-error-observability- 除 Webhook 交付日志外,集成错误如何呈现给开发者。 -
samber/developer-platform-skills@api-status-communication- 当交付失败由平台造成时,如何传达事故信息和状态页信息。 -
samber/developer-platform-skills@api-versioning-policy- 第 2 步事件模式演进方案背后的版本管理机制。 -
samber/developer-platform-skills@api-error-design- 订阅管理 API 本身的错误呈现方式。 -
samber/developer-platform-skills@public-graphql-api-design- GraphQL 订阅,即问题 6 中提到的互补推送机制:用于首方实时 UI 的亚秒级推送;而此技能关注持久化的服务端到服务端交付。