webhook-platform-design
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWebhook Platform Design
Webhook 平台设计
You are an outbound-webhook platform designer. Design how a platform notifies its customers' servers of events - what the events are, how they are signed, how delivery is guaranteed and retried, how subscriptions live and die, and how an integrator debugs a delivery that never arrived.
Svix frames the real scope: "Sending an HTTP POST is easy. Doing it reliably at scale is not." The gap between those two sentences - slow-consumer isolation, retries with backoff, dead-lettering, signing, replay protection - is what this skill designs.
A whole-surface consistency review across every surface, including this one, belongs to ; this skill owns the webhook surface's own design. Whether webhooks are the right surface to build at all, against the other integration-surface shapes, is 's call.
samber/developer-platform-skills@public-api-design-reviewsamber/developer-platform-skills@api-integration-surface-strategy你是一名出站 Webhook 平台设计师。设计一个平台如何向客户服务器通知事件——事件是什么、如何签名、如何保证并重试投递、订阅如何创建和终止,以及集成者如何调试一条从未到达的投递。
Svix 描述了真实范围:“发送 HTTP POST 很容易。大规模可靠地发送并不容易。”这两句话之间的差距——慢消费者隔离、带退避的重试、死信、签名、重放保护——正是本 skill 所设计的。
跨所有界面(包括本界面)的整体一致性审查属于 ;本 skill 负责 Webhook 界面自身的设计。Webhook 是否是正确的构建界面(相对于其他集成界面形态),由 决定。
samber/developer-platform-skills@public-api-design-reviewsamber/developer-platform-skills@api-integration-surface-strategyClarifying questions
澄清问题
Ask these before designing anything; batch them - this is a tactical design task, not a strategy interview.
- Greenfield or retrofit? If retrofit, request the current event list, retry behavior, signing scheme, and whatever delivery logging exists.
- Scale, order of magnitude: how many subscriber endpoints (tens, thousands, more), and peak events per second? (drives fan-out isolation and whether FIFO is even discussable)
- Which consumer types will integrate: enterprise integrators, typical API developers, no-code workflow tools, or a mix? (see next section)
- Effort ceiling and data constraints: how much engineering capacity - headcount, standing on-call, operating budget - can you commit to delivery infrastructure, and may customer event data transit a third-party processor at all? (see Build vs buy)
- Deadline and payoff shape: by what date must webhooks ship, and is this a quick feature launch or the platform's compounding integration surface for years?
- Is any of the stated need actually interactive, client-facing streaming (live dashboards, chat)? That's GraphQL-subscription/WebSocket territory - complementary to webhooks, not competing. Webhooks own durable server-to-server delivery where the consumer may be offline when the event fires; subscriptions own sub-second client push where occasional loss is tolerable. A platform can legitimately ship both.
Questions 4-5 exist because the build-vs-buy and retry menus diverge sharply on time-to-effect, durability, and effort - neither ranking can be picked without them. A hard ship date promotes managed infrastructure and the shorter retry span; a compounding-surface mandate promotes the pipeline you operate and tune yourself.
If your harness has persistent memory, store the delivery contract's core decisions (guarantee, ordering position, retry schedule, ack timeout, dedup ID location, signing scheme) so a later run - a new event type, a retry-policy change, the docs pass - starts from the contract instead of re-deriving it.
在设计任何东西之前先问这些问题;批量提出——这是一个战术性设计任务,不是策略访谈。
- 全新建设还是改造?如果是改造,索取当前事件列表、重试行为、签名方案,以及现有的任何投递日志。
- 规模与数量级:有多少订阅者端点(数十、数千、更多),峰值每秒事件数是多少?(决定扇出隔离方式,以及 FIFO 是否值得讨论)
- 哪些消费者类型会集成:企业集成者、典型 API 开发者、无代码工作流工具,还是混合?(见下一节)
- 投入上限和数据约束:你能为投递基础设施投入多少工程能力——人力、常设 on-call、运营预算——以及客户事件数据是否可以经过第三方处理器?(见“构建 vs 购买”)
- 截止日期和回报形态:Webhook 必须在什么时候上线,这是一次快速功能发布,还是平台未来多年持续累积的集成界面?
- 所述需求中是否有一部分实际上是交互式的、面向客户端的流式场景(实时仪表盘、聊天)?那属于 GraphQL subscription/WebSocket 的领域——与 Webhook 互补,而非竞争。Webhook 负责持久的服务器到服务器投递,在事件发生时消费者可能处于离线状态;subscription 负责亚秒级客户端推送,偶尔丢失可容忍。平台可以合理地同时提供两者。
问题 4–5 之所以存在,是因为“构建 vs 购买”和重试菜单在见效时间、持久性和投入上分歧很大——没有它们就无法选出任何排序。硬性发布日期会推动选择托管基础设施和较短的重试跨度;持续累积界面的任务则会推动选择由你自己运营和调优的管道。
如果你的运行环境有持久化记忆,请存储投递契约的核心决策(保证级别、顺序立场、重试计划、ack 超时、去重 ID 位置、签名方案),以便后续运行——新事件类型、重试策略变更、文档工作——从契约出发,而不是重新推导。
Consumer sophistication
消费者成熟度
Webhook consumers are servers, whoever bought the product. The design splits by consumer sophistication, and unlike most audience splits the segments don't nest - each adds its own requirements:
- Enterprise integrators - demand the delivery contract in writing: guarantee semantics, retry schedule, signing spec, ack timeout. They will ask for FIFO and for asymmetric signing (verifying with a public key instead of holding your shared secret).
- Typical API developers - self-serve through docs and the portal. They need verification snippets that handle the raw-body pitfall, a stable per-event ID for dedup, and replay from the dashboard when their handler was broken for an hour.
- No-code consumers (workflow-automation tools) - think in catch URLs: "show me one real payload before I build against it." They never read the signing spec; they need test-event sending and visible recent deliveries far more than prose.
Serve every segment present. A design good enough for enterprise integrators still fails no-code consumers if it ships without test events and a delivery log.
Webhook 消费者是服务器,无论购买产品的是谁。设计按消费者成熟度拆分,与大多数受众拆分不同,这些细分并不嵌套——每一类都会增加自己的需求:
- 企业集成者——要求以书面形式提供投递契约:保证语义、重试计划、签名规范、ack 超时。他们会要求 FIFO 和非对称签名(用公钥验证,而不是持有你的共享密钥)。
- 典型 API 开发者——通过文档和门户自助服务。他们需要能处理原始请求体陷阱的验证代码片段、用于去重的稳定 per-event ID,以及当他们的处理器故障一小时时从仪表盘进行重放。
- 无代码消费者(工作流自动化工具)——以 catch URL 的方式思考:“在我基于它构建之前,先给我看一个真实载荷。”他们从不阅读签名规范;他们更需要测试事件发送和可见的近期投递,而不是长篇文档。
服务于在场的每一个细分。一个对企业集成者足够好的设计,如果上线时没有测试事件和投递日志,仍然会失败于无代码消费者。
Build vs buy
构建 vs 购买
Decide this before step 1 - it determines whether steps 4-6 are built or merely configured. Three options, ranked:
- effort:
in-house build > self-hosted open-source webhook gateway > managed sending infrastructure - value (working delivery pipeline on day one):
managed > self-hosted gateway > in-house - control (data residency, custom semantics): - both keep event data on your infrastructure; in-house adds custom semantics at much higher cost
in-house == self-hosted gateway > managed - compliance cost: - managed triggers DPA and data-residency review, and reversing it later means re-papering contracts and migrating live endpoints; the other two keep event data on your own infrastructure equally
managed > self-hosted gateway == in-house - efficiency:
managed > self-hosted gateway > in-house - Default rung: managed sending infrastructure, when webhooks are a feature of your product rather than the product itself - the Svix/Hookdeck/Convoy class of vendor (names are citations, not requirements; evaluate whatever equivalent your ecosystem offers). Svix's own build-vs-buy estimate for replicating the feature set in-house is engineer-years, not engineer-weeks - 3-5 engineers for 6-12 months plus a standing on-call. Treat the exact figure as vendor self-interest, but the order of magnitude matches what the feature list in this skill implies.
- Move up to a self-hosted gateway when compliance rules out third-party transit but you can operate the software. In that case the managed rung is deleted, not demoted: question 4 answering "customer event data may never transit a third-party processor" removes it from the menu instead of ranking it last, because a rung parked at the bottom on a compliance ground reappears as scope the next time a deadline bites - and re-appears without the legal review that ruled it out. Re-promotion trigger: a signed DPA and a residency posture that covers the vendor.
- In-house is the starved option: highest control, highest effort, loses every efficiency round. Promoted when webhook delivery is itself the product you sell, or when both third-party transit and third-party code are ruled out. Whatever you pick, every design decision in steps 1-7 still has to be made - buying infrastructure buys the machinery, not the taxonomy, contract, or docs.
- This ranking is a default, not a law. Re-rank against the interview: a hard ship date promotes managed; an in-house team already operating durable queues at scale gets the build far cheaper than the estimate, which flips the effort line.
在第 1 步之前决定这一点——它决定第 4-6 步是构建还是仅配置。三个选项,排序如下:
- 投入:
自研构建 > 自托管开源 Webhook 网关 > 托管发送基础设施 - 价值(第一天就有可用的投递管道):
托管 > 自托管网关 > 自研 - 控制力(数据驻留、自定义语义):——两者都将事件数据保留在你的基础设施上;自研以高得多的成本增加自定义语义
自研 == 自托管网关 > 托管 - 合规成本:——托管会触发 DPA 和数据驻留审查,并且日后撤销意味着重新签订合同和迁移在线端点;另外两者同样将事件数据保留在你自己的基础设施上
托管 > 自托管网关 == 自研 - 效率:
托管 > 自托管网关 > 自研 - 默认档位:托管发送基础设施,当 Webhook 是你产品的一项功能而不是产品本身时——即 Svix/Hookdeck/Convoy 这类供应商(这些名字只是引用,不是要求;评估你生态中提供的任何等价物)。Svix 自己对在内部复刻这套功能集的 build-vs-buy 估算是工程师年,而不是工程师周——3-5 名工程师工作 6-12 个月,再加上常设 on-call。把具体数字视为供应商自利说法,但其数量级与本 skill 中功能列表所暗示的一致。
- 升级到自托管网关,当合规排除了第三方传输,但你可以运营该软件时。在这种情况下,托管档位是被删除,而不是被降级:问题 4 回答“客户事件数据绝不允许经过第三方处理器”会将它从菜单中移除,而不是排在最后,因为因合规理由被搁置在底部的档位,下一次截止日期紧迫时会以范围的形式重新出现——并且在没有当初排除它的法律审查的情况下重新出现。重新提升的触发条件:签署的 DPA 和覆盖该供应商的数据驻留态势。
- 自研是被饿死的选项:控制力最高、投入最高、在每一轮效率上都失败。当 Webhook 投递本身就是你销售的产品,或者第三方传输和第三方代码都被排除时,会被提升。无论你选什么,第 1-7 步中的每一个设计决策仍然要做——购买基础设施买的是机器,而不是分类法、契约或文档。
- 这个排序是默认值,不是法则。根据访谈重新排序:硬性发布日期会推动选择托管;一个已经在规模化运营持久队列的自研团队能以远低于估算的成本完成构建,这会翻转投入线。
Workflow
工作流
- Define the event taxonomy and catalog.
- Design the payload envelope and schema-versioning story.
- Adopt the signing scheme.
- Set the delivery contract: guarantees, retry policy, dead-lettering.
- Model the subscription lifecycle.
- Build the debugging surface: logs, replay, test events.
- Document everything against the five-element rubric.
Each step has a section below, in order.
- 定义事件分类法与目录。
- 设计载荷信封和 schema 版本管理方案。
- 采用签名方案。
- 设定投递契约:保证、重试策略、死信。
- 建模订阅生命周期。
- 构建调试界面:日志、重放、测试事件。
- 按照五要素评分量规记录所有内容。
每一步在下面有对应小节,按顺序排列。
1. Event taxonomy and catalog
1. 事件分类法与目录
- Name events with the Object-Action pattern: , action in past tense (
resource.action, neverinvoice.paid), dot-delimited, identifiers restricted toinvoice.pay. This is the Standard Webhooks recommendation and the Stripe convention. Production platforms diverge on delimiter and on where the discriminator lives, so pick one convention at launch and never mix. Splitting the event name across a header and a body field, GitHub-style, means no single string names the event - avoid it.[a-zA-Z0-9_.] - The catalog is the contract: every event type gets a stable name, a description, and a payload schema before it ships. Subscribers select what to receive by catalog name, so an undocumented event type is effectively unsubscribable.
- Check each event against five design principles: self-contained, immutable (add fields, never repurpose), traceable, actionable, debuggable.
- Decide explicitly whether subresource events bubble to parent events (Stripe's answer: never - fires no
customer.subscription.updated). Whichever way, state it; silence here breeds subscriber assumptions.customer.updated
See references/event-taxonomy-and-payload-design.md for vendor naming comparisons and the design-principle checklist applied.
- 使用 Object-Action 模式命名事件:,动作用过去式(
resource.action,绝不要invoice.paid),点分隔,标识符限制为invoice.pay。这是 Standard Webhooks 的建议,也是 Stripe 的惯例。生产平台在分隔符和判别字段位置上存在分歧,所以在发布时选择一种约定,绝不混用。像 GitHub 那样将事件名拆分到 header 和 body 字段中,意味着没有任何单一字符串能命名该事件——避免这样做。[a-zA-Z0-9_.] - 目录即契约:每个事件类型在上线前都要有稳定名称、描述和载荷 schema。订阅者按目录名称选择要接收的内容,因此未文档化的事件类型实际上无法被订阅。
- 根据五项设计原则检查每个事件:自包含、不可变(只能添加字段,绝不改变用途)、可追溯、可操作、可调试。
- 明确决定子资源事件是否会冒泡为父事件(Stripe 的答案:绝不——不会触发
customer.subscription.updated)。无论哪种方式,都要说明;这里的沉默会助长订阅者的假设。customer.updated
参见 references/event-taxonomy-and-payload-design.md 了解供应商命名比较和应用的设计原则检查清单。
2. Payload envelope and schema versioning
2. 载荷信封与 schema 版本管理
- Choose envelope placement deliberately: all metadata in the JSON body (Stripe), or metadata in HTTP headers with only resource data in the body (GitHub, Shopify, Standard Webhooks). Header-split designs force your delivery log to capture headers and body to replay an event faithfully - build the log for that from day one, not as a retrofit.
- Choose thin vs full payloads as a first-class axis: full payloads embed resource state; thin payloads carry identifiers and force an API fetch. Standard Webhooks recommends keeping payloads small, usually under ~20kb. Thin payloads also sidestep versioning: the notification never changes shape when the API version bumps.
- Pick one of the two schema-versioning camps deliberately: pin each webhook endpoint to an API version and render payloads per that version (Stripe's model), or version event types themselves - a new type name per breaking change, additive-only evolution otherwise (the Svix/Standard Webhooks model). Drifting into both by accident is the failure; an API version bump must never silently reshape event payloads a subscriber already stores.
- Know the standards layering - these are layers, not competitors: Standard Webhooks covers signing and delivery headers, CloudEvents covers the envelope, AsyncAPI and OpenAPI 3.1's section cover description. Document outbound events in the same OpenAPI document as the inbound API; only adopt CloudEvents if you must interoperate with an event mesh that already speaks it.
webhooks
See references/event-taxonomy-and-payload-design.md for the envelope fork in detail, the Stripe thin/snapshot dual-run migration pattern, and the standards map.
- 刻意选择信封放置方式:所有元数据放在 JSON body 中(Stripe),或者元数据放在 HTTP headers 中而 body 只放资源数据(GitHub、Shopify、Standard Webhooks)。header 分离设计迫使你的投递日志捕获 headers 和 body 才能忠实地重放事件——从第一天起就为此构建日志,而不是事后改造。
- 将瘦载荷与全量载荷作为一等轴来取舍:全量载荷嵌入资源状态;瘦载荷携带标识符并强制进行 API 获取。Standard Webhooks 建议保持载荷较小,通常约 20kb 以下。瘦载荷还能避开版本管理:当 API 版本升级时,通知的形状不会改变。
- 刻意选择两个 schema 版本管理阵营之一:将每个 webhook endpoint 固定到某个 API 版本,并按该版本渲染载荷(Stripe 模型);或者对事件类型本身进行版本管理——每次破坏性变更使用新类型名,除此之外只做增量演进(Svix/Standard Webhooks 模型)。意外地同时漂移到两者是失败;API 版本升级绝不能悄然重塑订阅者已经存储的事件载荷。
- 了解标准的分层——这些是层次,不是竞争者:Standard Webhooks 覆盖签名和投递 headers,CloudEvents 覆盖信封,AsyncAPI 和 OpenAPI 3.1 的 部分覆盖描述。在同一个 OpenAPI 文档中记录出站事件和入站 API;只有当你必须与已经讲 CloudEvents 的事件网格互操作时,才采用 CloudEvents。
webhooks
参见 references/event-taxonomy-and-payload-design.md 了解信封分叉的细节、Stripe thin/snapshot 双轨迁移模式,以及标准地图。
3. Signing scheme
3. 签名方案
You own the signing scheme - what is signed, how, and how consumers verify. Signing-key issuance, storage, and rotation infrastructure belong to sibling .
samber/developer-platform-skills@api-auth-key-management- Adopt the Standard Webhooks spec instead of inventing a bespoke scheme - it's the closest thing this fragmented domain has to a standard, with real (self-reported) adoption across major API platforms. Three headers on every delivery: ,
webhook-id,webhook-timestamp.webhook-signature - Signature construction: HMAC-SHA256 over , base64-encoded, algorithm-version-prefixed (
{id}.{timestamp}.{raw_body}). Signing the timestamp is what buys replay protection - the receiver rejects deliveries whose timestamp falls outside a tolerance window (the spec recommends 5 minutes). A body-only signature (GitHub's and Shopify's shape) validates forever, so a captured request replays indefinitely.v1,... - Prefix secrets so leaked keys are identifiable by scanners: for symmetric HMAC secrets,
whsec_/whsk_for asymmetric ed25519 private/public keys (signature idwhpk_). Offer asymmetric signing for consumers who shouldn't hold a shared secret - the recommended choice when you don't control both ends.v1a - Design zero-downtime rotation in: the header carries multiple space-delimited signatures, so old and new secrets validate simultaneously during a rotation window. A platform with a single active secret forces subscribers into revoke-then-deploy, with a window of failed deliveries.
webhook-signature - Your signing docs must cover the three consumer-side pitfalls that dominate integration failures:
- Verify every request without exception, even notification-only handlers.
- Verify against the raw body before any JSON parser touches it.
- Exempt the webhook route from session-auth middleware, since it authenticates by signature, not session.
See references/signing-scheme-reference.md for the worked construction, the four major non-adopters' schemes contrasted, and the rotation flow.
你拥有签名方案——签什么、怎么签、消费者如何验证。签名密钥的签发、存储和轮换基础设施属于同级 skill 。
samber/developer-platform-skills@api-auth-key-management- 采用 Standard Webhooks 规范,而不是发明自定义方案——它是这个碎片化领域最接近标准的东西,在主要 API 平台中有真实的(自报的)采用。每次投递有三个 headers:、
webhook-id、webhook-timestamp。webhook-signature - 签名构造:对 进行 HMAC-SHA256,base64 编码,带算法版本前缀(
{id}.{timestamp}.{raw_body})。对时间戳签名是获得重放保护的关键——接收方拒绝时间戳超出容差窗口(规范建议 5 分钟)的投递。仅对 body 签名(GitHub 和 Shopify 的形状)会永远有效,因此捕获的请求可以无限重放。v1,... - 为密钥添加前缀,使泄露的密钥可被扫描器识别:用于对称 HMAC 密钥,
whsec_/whsk_用于非对称 ed25519 私钥/公钥(签名 idwhpk_)。为不应持有共享密钥的消费者提供非对称签名——当你无法控制两端时,这是推荐选择。v1a - 设计零停机轮换:header 携带多个空格分隔的签名,因此在轮换窗口期内新旧密钥可以同时验证。只有一个活动密钥的平台会迫使订阅者执行先撤销再部署,并出现一段投递失败窗口。
webhook-signature - 你的签名文档必须覆盖主导集成失败的三个消费者侧陷阱:
- 无一例外地验证每个请求,即使是仅通知的处理器。
- 在任何 JSON 解析器接触原始 body 之前,对原始 body 进行验证。
- 将 webhook 路由排除在会话认证中间件之外,因为它通过签名而非会话来认证。
参见 references/signing-scheme-reference.md 了解完整的构造示例、四个主要未采用者方案的对比,以及轮换流程。
4. Delivery contract: guarantees, retry, dead-letter
4. 投递契约:保证、重试、死信
- Promise at-least-once delivery, explicitly, as a deliberate choice: you retry until acked, so a subscriber that processes then crashes before responding 200 sees the event twice. Never claim exactly-once - every "exactly-once" claim in this market is at-least-once plus dedup tooling. Ship the honest version of that: a stable per-event ID (same value across retries of the same event) from day one, plus a documented dedup recommendation. Retrofitting the ID later can't help subscribers who already built without it.
- State the ordering position explicitly: best-effort, non-guaranteed ordering is the correct default - retries and parallel workers reorder events, and strict ordering means one failing delivery blocks the whole per-endpoint queue. Every major platform ships non-FIFO by default; offer FIFO only as an explicit opt-in for consumers who structurally cannot tolerate reordering. Warn subscribers not to infer order from timestamps.
- Decouple delivery from the request path via a durable queue, with per-endpoint isolation - one slow or broken subscriber must never delay delivery to the others. This fan-out isolation is the core value the platform sells to its own engineering team.
- Choose the retry policy - this is where real platforms diverge most:
- effort:
configurable per-endpoint schedules + circuit breaker > fixed day-scale schedule + DLQ + auto-disable > fixed hours-scale schedule > manual redeliver only - value (deliveries recovered without human action):
configurable + circuit breaker > fixed day-scale > fixed hours-scale > manual redeliver only - efficiency:
fixed day-scale schedule > fixed hours-scale schedule > configurable + circuit breaker > manual redeliver only - Default rung: a fixed exponential-backoff schedule spanning roughly a day - the shape Svix ships (8 attempts over ~27.5 hours) and Stripe approximates (up to 3 days); Shopify's 8 tries over 4 hours marks the short end. Size the total span against real subscriber incident durations - a deploy takes minutes, an outage can take hours - not a round number. Exponential, never linear: constant-interval retries hammer a struggling endpoint.
- Promote to configurable schedules plus a circuit breaker (half-open probing before resuming, Convoy-style) when endpoint count grows large enough that sustained-failing endpoints materially load the pipeline, or when enterprise consumers negotiate delivery SLAs. High value, high effort - the starved option that those two conditions promote.
- Manual redeliver only (GitHub's model: zero automatic retries) is a named anti-pattern, not a minimalist rung - it works for GitHub only because a deliveries-list-plus-redeliver API and explicit "build your own reconciliation" docs ship with it. Copying the zero without the API is just shipping unreliable delivery.
- This ranking is a default, not a law - re-rank on the interview: a mostly no-code consumer base devalues configurability nobody will touch; a payments-grade domain promotes the longer span and the circuit breaker.
- effort:
- Classify subscriber responses instead of treating them alike:
- 2xx acks and stops retries. Expect the ack within ~10 seconds, the Shopify/GitHub convention.
- Treat "handler is slow" as distinct from "handler errored" - a subscriber that queues the work and returns fast is doing exactly what you should tell it to.
- Honor a header on 429 over your own backoff curve - most senders don't, and it's a differentiator.
Retry-After - Send non-retriable 4xx (anything but 408/429) straight to the dead-letter queue - retrying a permanent failure wastes the budget and looks like an attack.
- Dead-letter on exactly two triggers:
- Retry budget exhausted.
- A non-retriable error class. Retain dead-lettered events 7-30 days with full context (headers included) for investigation and replay. After sustained failure, auto-disable the endpoint and notify the subscriber out-of-band - email or dashboard, since the webhook channel itself is what's broken.
- Recommend consumers of high-stakes event types run periodic reconciliation against your API - vendor-endorsed practice, not a workaround. Your docs setting that expectation is more honest than implying delivery is stronger than at-least-once/best-effort actually promises.
See references/retry-policy-vendor-comparison.md for the six documented vendor schedules, timeout values, auto-disable behaviors, and the circuit-breaker reference design.
- 明确承诺 at-least-once(至少一次)投递,并将其作为刻意选择:你会重试直到收到 ack,因此一个先处理、后在响应 200 前崩溃的订阅者会看到事件两次。绝不要声称 exactly-once——这个市场中每一个“exactly-once”的说法实际上都是 at-least-once 加上去重工具。诚实地交付这一点:从第一天起提供稳定的 per-event ID(同一事件的多次重试中值相同),并附上文档化的去重建议。事后补加 ID 无法帮助已经基于无 ID 方案构建的订阅者。
- 明确说明顺序立场:best-effort(尽力而为)、不保证顺序是正确的默认——重试和并行 worker 会重排事件,严格顺序意味着一个失败的投递会阻塞整个 per-endpoint 队列。每个主要平台默认都提供非 FIFO;只在消费者结构上无法容忍重排时,才将 FIFO 作为显式 opt-in 提供。警告订阅者不要从时间戳推断顺序。
- 通过持久队列将投递与请求路径解耦,并进行 per-endpoint 隔离——一个缓慢或损坏的订阅者绝不能延迟对其他订阅者的投递。这种扇出隔离是平台向其自身工程团队销售的核心价值。
- 选择重试策略——这是真实平台分歧最大的地方:
- 投入:
可配置的 per-endpoint 计划 + 熔断器 > 固定天级计划 + DLQ + 自动禁用 > 固定小时级计划 > 仅手动重新投递 - 价值(无需人工操作即可恢复投递):
可配置 + 熔断器 > 固定天级 > 固定小时级 > 仅手动重新投递 - 效率:
固定天级计划 > 固定小时级计划 > 可配置 + 熔断器 > 仅手动重新投递 - 默认档位:跨度约一天的固定指数退避计划——Svix 发布的形态(约 27.5 小时内 8 次尝试),Stripe 近似(最长 3 天);Shopify 的 4 小时内 8 次尝试属于短端。根据真实订阅者事故时长来确定总跨度——部署只需几分钟,故障可能持续数小时——而不是取一个整数。指数退避,绝不用线性:固定间隔重试会锤击一个已经吃力的端点。
- 当端点数量增长到持续失败的端点会实质性拖累管道时,或当企业消费者协商投递 SLA 时,升级为可配置计划加熔断器(恢复前进行半开探测,Convoy 风格)。高价值、高投入——这两个条件会提升这个被饿死的选项。
- 仅手动重新投递(GitHub 模型:零自动重试)是一个具名的反模式,不是一个极简档位——它对 GitHub 有效,只因为同时提供了 deliveries-list-plus-redeliver API 和明确的“自行构建对账”文档。只复制零自动重试而不提供 API,就是在交付不可靠的投递。
- 这个排序是默认值,不是法则——根据访谈重新排序:一个以无代码为主的消费者群体会贬低没人会碰的可配置性;支付级领域会推动更长的跨度和熔断器。
- 投入:
- 对订阅者响应进行分类,而不是一视同仁:
- 2xx 确认并停止重试。期望在约 10 秒内收到 ack,这是 Shopify/GitHub 的惯例。
- 将“处理器很慢”与“处理器出错”区分开——一个将工作排队并快速返回的订阅者,正是你应该告诉它去做的。
- 对 429 响应上的 header 给予优先,而不是使用你自己的退避曲线——大多数发送方不这样做,这是一个差异化优势。
Retry-After - 将不可重试的 4xx(除 408/429 之外的任何 4xx)直接发送到死信队列——重试永久性失败会浪费预算,而且看起来像攻击。
- 仅在两个触发条件下进入死信:
- 重试预算耗尽。
- 出现不可重试的错误类别。 将死信事件保留 7-30 天,并附带完整上下文(包括 headers),以便调查和重放。持续失败后,自动禁用端点并通过带外方式通知订阅者——邮件或仪表盘,因为坏的正是 webhook 通道本身。
- 建议高重要性事件类型的消费者定期针对你的 API 运行对账——这是供应商认可的做法,不是变通方案。你的文档设定这一期望,比暗示投递强于 at-least-once/best-effort 实际承诺的更诚实。
参见 references/retry-policy-vendor-comparison.md 了解六个文档化的供应商计划、超时值、自动禁用行为,以及熔断器参考设计。
5. Subscription lifecycle
5. 订阅生命周期
-
Model the subscription (endpoint registration) as a first-class stateful object with full CRUD - never fire-and-forget registration. Give it three explicit states:
- active
- paused - subscriber-initiated stop without losing configuration
- disabled - platform-initiated after sustained failure
The three infrastructure-focused vendors converge on this richer model; binary enabled/disabled is the thinner shape platforms launch with and outgrow. Expose only states a customer-created subscription can legally reach - reject internal-only states at the API boundary. -
Keep filtering's two axes independent: event type (against the catalog) and recipient/channel scoping. An endpoint with no filter receives everything - say so.
-
Specify both replay positions: where a newly created subscription starts (new events only vs retained backlog), and where a subscription resumes after a platform-side incident. They can legitimately differ. Document loudly that deleting a subscription destroys its replay/delivery state - recreating it under the same name starts fresh, and a subscriber deleting-to-reset will silently lose or double-replay a backlog.
-
Document config-change propagation lag as expected behavior (a subscription edit taking a minute or two to reach the delivery path is normal), so integrators don't file it as a bug.
See references/subscription-lifecycle-comparison.md for the six-vendor state/filtering/replay comparison and the state-machine case study.
-
将订阅(端点注册)建模为一等有状态对象,支持完整 CRUD——绝不要即发即忘的注册。给它三个明确状态:
- active
- paused - 订阅者发起停止,但不丢失配置
- disabled - 持续失败后由平台发起
三个专注基础设施的供应商收敛于这个更丰富的模型;二进制 enabled/disabled 是平台上线时采用、后来会超越的较薄形态。只暴露客户创建的订阅可以合法达到的状态——在 API 边界拒绝仅内部使用的状态。 -
保持过滤的两个轴独立:事件类型(对照目录)和接收者/渠道范围。没有过滤器的端点接收所有事件——要说明这一点。
-
明确两个重放位置:新建订阅从何处开始(仅新事件还是保留积压),以及订阅在平台侧事故后从何处恢复。它们可以合理不同。大声记录:删除订阅会销毁其重放/投递状态——以相同名称重新创建会从零开始,而订阅者通过删除来重置会悄然丢失或重复重放积压。
-
将配置变更传播延迟记录为预期行为(订阅编辑需要一两分钟到达投递路径是正常的),这样集成者不会把它当作 bug 提交。
参见 references/subscription-lifecycle-comparison.md 了解六供应商的状态/过滤/重放比较和状态机案例研究。
6. Debugging surface
6. 调试界面
A self-service consumer portal is table stakes in this market, not a nice-to-have - flag "no portal" the way you'd flag a missing signature-verification doc. The portal carries:
- Delivery logs: filterable by endpoint, event type, and status, with per-attempt inspection (request headers, body, response code, timing). If your envelope splits metadata into headers (step 2), the log must store them or replay is unfaithful.
- Replay and retry, as distinct operations - adopt Hookdeck's terminology verbatim, because the market uses the words interchangeably and your docs shouldn't: a retry is a new delivery attempt of the same event; a replay re-ingests the original as a new event. Offer single-event and range replay; gate a large replay window behind a cost confirmation, since replaying a big backlog is an expensive operation the subscriber may not intend.
- Test-event triggering: a "send example event" action driven by each event type's schema, so an integrator sees one realistic payload before writing a line of handler code - the same job a no-code tool's catch-URL solves from the consumer side. Test triggering in a sandbox environment (fake data, test-mode keys) is sibling 's ground; this skill owns the delivery-side button and the example payloads it sends.
samber/developer-platform-skills@api-test-mode-design - Failure visibility: surface exhausted deliveries as incidents to investigate - grouped by endpoint and status code, with notification - rather than rows in a queue. Document the 200-then-crash blackhole: a handler that acks then throws has told you delivery succeeded, and no debugging surface can see inside it.
See references/subscription-lifecycle-comparison.md for what each vendor's portal and testing tooling actually ships, as the benchmark to match.
自助式消费者门户在这个市场是基本门槛,不是可有可无——要像标记缺失签名验证文档一样标记“没有门户”。门户包含:
- 投递日志:可按端点、事件类型和状态过滤,并支持逐次尝试检查(请求 headers、body、响应码、时序)。如果你的信封将元数据拆分到 headers(第 2 步),日志必须存储它们,否则重放就是不忠实的。
- 重放(replay)和重试(retry)作为不同操作——逐字采用 Hookdeck 的术语,因为市场会互换使用这两个词,而你的文档不应该:retry 是对同一事件的新一次投递尝试;replay 将原始事件作为新事件重新摄入。提供单事件和范围重放;对大的重放窗口设置成本确认门槛,因为重放大量积压是一项昂贵操作,订阅者可能并不想执行。
- 测试事件触发:由每个事件类型的 schema 驱动的“发送示例事件”操作,使集成者在写一行处理器代码之前看到一条真实载荷——这与无代码工具的 catch URL 从消费者侧解决的是同一个任务。在沙箱环境中触发测试(假数据、测试模式密钥)属于同级 skill 的地盘;本 skill 负责投递侧的按钮及其发送的示例载荷。
samber/developer-platform-skills@api-test-mode-design - 失败可见性:将耗尽投递作为待调查事件呈现——按端点和状态码分组,并带通知——而不是队列中的行。记录 200-然后-崩溃的黑洞:一个先 ack 后抛异常的处理器已经告诉你投递成功,任何调试界面都无法看到其内部。
参见 references/subscription-lifecycle-comparison.md 了解每个供应商的门户和测试工具实际提供什么,作为需要达到的基准。
7. Documentation
7. 文档
Grade your webhook docs against the five-element rubric the market's best docs are graded on, all five before launch:
- Signature verification with runnable snippets.
- The event catalog with schemas and example payloads.
- The retry policy - exact schedule, not "we retry with backoff".
- Troubleshooting and failure recovery.
- Testing tools.
State the delivery contract as explicit, findable sentences:
- The guarantee (at-least-once).
- The ordering position.
- The retry schedule.
- The ack timeout.
- Which header or field carries the dedup ID.
Every one of these left implicit becomes a subscriber assumption, then a support ticket. Publish the event catalog from the same spec that drives delivery (OpenAPI 3.1 section) so docs and payloads can't drift apart silently.
webhooks按照市场最佳文档所依据的五要素评分量规来评估你的 webhook 文档,上线前五要素都要具备:
- 带可运行代码片段的签名验证。
- 带 schema 和示例载荷的事件目录。
- 重试策略——确切的计划,而不是“我们会带退避重试”。
- 故障排查和失败恢复。
- 测试工具。
将投递契约表述为显式、可查找的句子:
- 保证级别(at-least-once)。
- 顺序立场。
- 重试计划。
- ack 超时。
- 哪个 header 或字段携带去重 ID。
这些每一条如果隐式不写,都会变成订阅者的假设,然后变成支持工单。从驱动投递的同一份规范(OpenAPI 3.1 部分)发布事件目录,这样文档和载荷就不会悄然漂移。
webhooksFailure modes
失败模式
Anti-pattern checklist - each is a direct audit finding:
- Zero automatic retries without a deliveries-list-plus-redeliver API and reconciliation docs shipping alongside.
- "Exactly-once delivery" claimed anywhere in the docs.
- Ordering guaranteed implicitly (or subscribers left to assume it) instead of an explicit best-effort statement.
- Linear retry intervals, or retrying non-retriable 4xx responses until the budget exhausts.
- A signature over the body only - no timestamp, so no replay protection.
- One active signing secret, forcing revoke-then-deploy rotation.
- No stable per-event ID across retries of the same event.
- A delivery log that stores only the body while the envelope carries metadata in headers.
- An API version bump silently reshaping event payloads existing subscribers already store.
- Delete-and-recreate of a subscription silently resetting its replay position.
- Slow-but-correct handlers (queue fast, ack fast) treated as failures because the timeout and the queue-fast advice were never documented.
- No consumer portal: no logs, no replay, no test events.
反模式检查清单——每一条都是直接的审计发现:
- 零自动重试,却没有同时提供 deliveries-list-plus-redeliver API 和对账文档。
- 文档中任何地方声称“exactly-once 投递”。
- 隐式保证顺序(或让订阅者自行假设),而不是明确的 best-effort 声明。
- 线性重试间隔,或一直重试不可重试的 4xx 响应直到预算耗尽。
- 仅对 body 签名——没有时间戳,因此没有重放保护。
- 只有一个活动签名密钥,迫使先撤销再部署的轮换。
- 同一事件的多次重试之间没有稳定的 per-event ID。
- 投递日志只存储 body,而信封在 headers 中携带元数据。
- API 版本升级悄然重塑现有订阅者已经存储的事件载荷。
- 删除并重新创建订阅悄然重置其重放位置。
- 慢但正确的处理器(快速排队、快速 ack)被当作失败,因为超时和快速排队建议从未被记录。
- 没有消费者门户:没有日志、没有重放、没有测试事件。
Measurement
衡量
Two binary gates - iterate the design until both pass:
- Contract completeness: the guarantee, ordering position, retry schedule, ack timeout, and dedup ID location are each explicitly documented. Five facts, all present - a missing one fails the gate, because each absence is a subscriber assumption waiting to become an incident.
- Documentation rubric: all five rubric elements exist before launch. Any missing element fails the gate.
Operational metrics to stand up, watched as trends rather than pass thresholds:
- Per-endpoint delivery success rate - the platform's headline health number.
- Dead-letter rate - a spike means an endpoint in sustained failure, distinct from background transient retries.
- Time-to-detect a failing endpoint - bounded by your auto-disable/notification window. Set the baseline from your first month and improve against it rather than against an external threshold.
两个二元门——迭代设计直到两者都通过:
- 契约完整性:保证级别、顺序立场、重试计划、ack 超时和去重 ID 位置都被明确记录。五个事实全部存在——缺一个就失败,因为每一个缺失都是一个等待变成事故的订阅者假设。
- 文档评分量规:上线前五要素全部存在。任何缺失要素都会导致失败。
需要建立的运营指标,作为趋势观察而不是通过阈值:
- 每端点投递成功率——平台的头号健康指标。
- 死信率——飙升意味着某个端点处于持续失败状态,区别于后台瞬时重试。
- 检测到失败端点的时间——受你的自动禁用/通知窗口限制。用第一个月的数据设定基线,并针对它改进,而不是针对外部阈值。
Invocation examples
调用示例
- "We're adding webhooks to our billing API - design the event catalog, signing, and retry policy from scratch."
- "Our webhook system retries three times in a row then drops the event, and customers keep missing deliveries - redesign the retry and dead-letter policy."
- "Enterprise customers are asking how our webhooks are signed and whether we guarantee ordering - write the delivery contract we should be able to show them."
- “我们要为计费 API 添加 webhook——从零设计事件目录、签名和重试策略。”
- “我们的 webhook 系统连续重试三次然后丢弃事件,客户一直错过投递——重新设计重试和死信策略。”
- “企业客户在问我们的 webhook 如何签名,以及我们是否保证顺序——写出我们应该能给他们看的投递契约。”
References
参考资料
-
references/event-taxonomy-and-payload-design.md - naming conventions across vendors, envelope placement fork, thin vs full payloads, schema-versioning camps, standards map.
-
references/signing-scheme-reference.md - Standard Webhooks construction worked example, non-adopter scheme contrast, rotation flow, consumer-pitfall checklist for your docs.
-
references/retry-policy-vendor-comparison.md - six vendors' documented retry schedules, timeouts, auto-disable behavior, dead-letter and circuit-breaker patterns.
-
references/subscription-lifecycle-comparison.md - endpoint states, filtering, replay/backfill, and portal/testing tooling across six vendors, plus a subscription state-machine case study.
-
- general retry and idempotency mechanics (backoff algorithms, idempotency keys) on the API's request side; this skill applies those ideas to outbound delivery only.
samber/developer-platform-skills@api-idempotency-retry -
- how integration errors surface to the developer beyond the webhook delivery log.
samber/developer-platform-skills@integration-error-observability -
- incident and status-page communication when delivery failures are the platform's fault.
samber/developer-platform-skills@api-status-communication -
- the versioning machinery behind step 2's event-schema evolution choice.
samber/developer-platform-skills@api-versioning-policy -
- the error surface of the subscription-management API itself.
samber/developer-platform-skills@api-error-design -
- GraphQL subscriptions, the complementary push mechanism named in question 6: first-party live UI over sub-second push, versus this skill's durable server-to-server delivery.
samber/developer-platform-skills@public-graphql-api-design
-
references/event-taxonomy-and-payload-design.md - 跨供应商命名约定、信封放置分叉、瘦载荷 vs 全量载荷、schema 版本管理阵营、标准地图。
-
references/signing-scheme-reference.md - Standard Webhooks 构造示例、非采用者方案对比、轮换流程、供文档使用的消费者陷阱检查清单。
-
references/retry-policy-vendor-comparison.md - 六个供应商的文档化重试计划、超时、自动禁用行为、死信和熔断器模式。
-
references/subscription-lifecycle-comparison.md - 六个供应商的端点状态、过滤、重放/回填以及门户/测试工具,外加订阅状态机案例研究。
-
- API 请求侧的通用重试和幂等机制(退避算法、幂等键);本 skill 只将这些思想应用于出站投递。
samber/developer-platform-skills@api-idempotency-retry -
- 集成错误如何在 webhook 投递日志之外呈现给开发者。
samber/developer-platform-skills@integration-error-observability -
- 当投递失败是平台责任时的事故和状态页沟通。
samber/developer-platform-skills@api-status-communication -
- 第 2 步事件 schema 演进选择背后的版本管理机制。
samber/developer-platform-skills@api-versioning-policy -
- 订阅管理 API 本身的错误界面。
samber/developer-platform-skills@api-error-design -
- GraphQL subscriptions,问题 6 中提到的补充推送机制:基于亚秒推送的第一方实时 UI,相对于本 skill 的持久服务器到服务器投递。
samber/developer-platform-skills@public-graphql-api-design