sent-webhook-engineer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Sent Webhook Engineer

Sent Webhook 开发指南

Sent webhooks are the only way an application learns what happened after
POST /v3/messages
returns
202
. The
202
proves acceptance, never delivery. Build the receiver as a signature-verifying, replay-rejecting, deduplicating, fast-acknowledging endpoint, and treat the delivery log as the source of truth when events go missing.
Sent webhooks是应用在
POST /v3/messages
返回
202
后,了解后续事件的唯一方式。
202
仅表示请求已被接收,绝不代表消息已送达。接收器需构建为具备签名验证、重放拒绝、事件去重、快速确认能力的端点,当事件丢失时,需以交付日志作为唯一可信来源进行排查。

Signature verification, exactly

签名验证步骤详解

Three headers arrive with every delivery:
HeaderMeaning
x-webhook-signature
v1,{base64(hmac_sha256)}
x-webhook-id
The webhook endpoint UUID — identical on every delivery
x-webhook-timestamp
Unix seconds when Sent signed the request
Verification procedure, in order:
  1. Capture the raw request body bytes before any JSON parsing.
  2. Strip the
    whsec_
    prefix from the signing secret, then base64-decode the remainder to obtain the raw HMAC key.
  3. Build the signed content as
    {x-webhook-id}.{x-webhook-timestamp}.{raw_body}
    .
  4. Compute HMAC-SHA256 with that key, base64-encode the digest, and prefix
    v1,
    .
  5. Compare with a constant-time comparison.
  6. Reject when
    abs(now - timestamp) > 300
    seconds.
The scheme is Svix-compatible. No Sent SDK ships a verification helper in any language, so this code is always hand-written — use scripts/verify_signature.py as the reference implementation and oracle.
x-webhook-id
is not an event id.
It identifies the endpoint and repeats forever. Using it as a dedupe key silently collapses every event into one. Read references/webhook-signature-and-dedupe.md for the dedupe keys to derive per event type.
每次交付都会附带三个请求头:
请求头含义
x-webhook-signature
v1,{base64(hmac_sha256)}
x-webhook-id
Webhook 端点的UUID —— 每次交付的该值均相同
x-webhook-timestamp
Sent对请求进行签名时的Unix时间戳(秒)
验证流程按以下顺序执行:
  1. 在进行任何JSON解析前,捕获原始请求体字节
  2. 移除签名密钥的
    whsec_
    前缀,然后对剩余部分进行base64解码,得到原始HMAC密钥。
  3. 构建签名内容:
    {x-webhook-id}.{x-webhook-timestamp}.{raw_body}
  4. 使用上述密钥计算HMAC-SHA256,对摘要进行base64编码,并添加前缀
    v1,
  5. 使用常量时间比较法对比计算结果与请求头中的签名。
  6. abs(当前时间 - 时间戳) > 300
    秒,则拒绝该请求。
该签名方案与Svix兼容。Sent未在任何语言的SDK中提供验证工具,因此相关代码需手动编写——可参考scripts/verify_signature.py作为实现示例和权威参考。
x-webhook-id
并非事件ID
。它用于标识端点,且值始终不变。若将其作为去重键,会导致所有事件被合并为一个。如需了解各事件类型对应的去重键,请阅读references/webhook-signature-and-dedupe.md

Failure triage order

故障排查顺序

When a receiver rejects or misses events, work this sequence rather than guessing:
  1. Signature mismatch — a body-mutating middleware or framework JSON parser is the cause in the majority of cases. Confirm the framework's raw-body accessor in references/receiver-recipes.md.
  2. Replay rejection — server clock skew beyond the 300-second tolerance.
  3. Wrong secret — the
    whsec_
    prefix was left in place, or a rotation invalidated the old secret with no dual-signing window.
  4. Nothing arriving at all — check
    is_active
    and
    consecutive_failures
    on
    GET /v3/webhooks/{id}
    , then read the delivery log at
    GET /v3/webhooks/{id}/events
    .
  5. Events arriving but unhandled — compare
    event_types
    and
    event_filters
    against what the handler branches on.
当接收器拒绝事件或事件丢失时,请按以下顺序排查,而非盲目猜测:
  1. 签名不匹配——大多数情况下是由于修改请求体的中间件或框架JSON解析器导致的。请在references/receiver-recipes.md中确认框架的原始请求体获取方式。
  2. 重放请求被拒绝——服务器时钟偏差超出300秒的容忍范围。
  3. 密钥错误——保留了
    whsec_
    前缀,或密钥轮换时未设置双签名窗口导致旧密钥失效。
  4. 完全无事件到达——调用
    GET /v3/webhooks/{id}
    检查
    is_active
    consecutive_failures
    字段,然后通过
    GET /v3/webhooks/{id}/events
    查看交付日志。
  5. 事件已到达但未被处理——对比
    event_types
    event_filters
    与处理器分支逻辑是否匹配。

Retry, auto-disable, and recovery

重试、自动禁用与恢复机制

A delivery attempt fails on any non-2xx status, a timeout past
timeout_seconds
, or a connection failure. Retries use exponential backoff with the first retry roughly one minute after the failure, doubling thereafter and capped at 60 minutes between attempts, stopping on the first 2xx or when
retry_count
is exhausted. Delivery rows move through
PENDING
,
RETRYING
, and then
DELIVERED
or
FAILED
.
consecutive_failures
tracks consecutive failed delivery attempts. Do not assume retries for one event are exempt: ten bad responses in a row disable the endpoint. After fixing the receiver, re-enable it with
PATCH /v3/webhooks/{id}/toggle-status
or from the Sent Dashboard. Any successful delivery resets the counter to zero. Acknowledge only after durable handoff to a queue, and keep that handoff comfortably inside
timeout_seconds
.
当返回非2xx状态码、超过
timeout_seconds
超时时间或连接失败时,交付尝试视为失败。重试采用指数退避策略:首次重试约在失败后1分钟,后续重试间隔翻倍,最大间隔为60分钟,直到返回2xx状态码或耗尽
retry_count
次数。交付记录会依次经历
PENDING
RETRYING
状态,最终变为
DELIVERED
FAILED
consecutive_failures
字段用于跟踪连续失败的交付尝试次数。请勿认为某一事件的重试会被豁免:连续10次错误响应会导致端点被禁用。修复接收器后,可通过
PATCH /v3/webhooks/{id}/toggle-status
接口或Sent控制台重新启用端点。任何一次成功交付都会将计数器重置为0。请仅在将事件可靠移交至队列后再确认接收,且移交操作需在
timeout_seconds
内完成。

Registration and configuration

注册与配置

POST /v3/webhooks
requires
display_name
. Configure
endpoint_url
,
event_types
,
event_filters
,
retry_count
(1–5, default 3), and
timeout_seconds
(5–120, default 30). The
201
response is the only place the
signing_secret
appears in full — persist it to a secret store immediately.
<!-- sent-webhook-request -->
json
{
  "display_name": "Production delivery events",
  "endpoint_url": "https://hooks.example.com/webhooks/sent",
  "event_types": ["message", "templates"],
  "event_filters": {
    "message": ["delivered", "failed", "received"]
  },
  "retry_count": 3,
  "timeout_seconds": 30
}
Set
event_filters
deliberately. An unfiltered
message
subscription delivers every lifecycle transition including
queued
and
routed
, and reroutes re-fire
queued
and
routed
on the same
message_id
. Filter to the transitions the application acts on.
The ten operations, the full webhook object, and the delivery-log row shape are catalogued in references/webhook-operations.md.
调用
POST /v3/webhooks
时需提供
display_name
。可配置
endpoint_url
event_types
event_filters
retry_count
(取值1–5,默认3)和
timeout_seconds
(取值5–120,默认30)。
201
响应是
signing_secret
完整显示的唯一位置——请立即将其存储至密钥管理系统。
<!-- sent-webhook-request -->
json
{
  "display_name": "Production delivery events",
  "endpoint_url": "https://hooks.example.com/webhooks/sent",
  "event_types": ["message", "templates"],
  "event_filters": {
    "message": ["delivered", "failed", "received"]
  },
  "retry_count": 3,
  "timeout_seconds": 30
}
请谨慎设置
event_filters
。未过滤的
message
订阅会推送所有生命周期事件,包括
queued
routed
,且重新路由时会针对同一
message_id
再次触发
queued
routed
事件。请仅过滤出应用需要处理的事件状态。
完整的10项操作、webhook对象结构以及交付日志记录格式可参考references/webhook-operations.md

Secret rotation

密钥轮换

POST /v3/webhooks/{id}/rotate-secret
returns a new
whsec_
secret and invalidates the old secret immediately. There is no server-side overlap window. Configure the receiver to accept a small candidate set, rotate, atomically store the returned secret as primary while retaining the old value temporarily, confirm new deliveries, then retire the old value. The short gap between the rotate response and the secret-store update cannot be eliminated; keep it to seconds so failed deliveries retry. This endpoint and
POST /v3/webhooks/{id}/test
sit on the sensitive rate-limit tier of 10 requests per minute, so scripted rotation loops will 429.
调用
POST /v3/webhooks/{id}/rotate-secret
会返回新的
whsec_
格式密钥,并立即失效旧密钥。服务器端不提供新旧密钥的重叠使用窗口。请将接收器配置为接受一组候选密钥,执行轮换操作后,原子性地将返回的新密钥设为主要密钥,同时暂时保留旧密钥,确认新交付正常后再移除旧密钥。轮换响应与密钥存储更新之间的短暂间隔无法消除,请将该间隔控制在数秒内,以便失败的交付能够重试。该接口与
POST /v3/webhooks/{id}/test
接口属于敏感接口,速率限制为每分钟10次请求,因此脚本化的轮换循环可能会触发429错误。

Event payloads

事件负载

Two
field
values exist:
message
and
templates
. Message events carry an
event
naming the transition (
message.queued
,
.routed
,
.sent
,
.delivered
,
.read
,
.failed
,
.scheduled
,
.filtered
,
.blocked
,
.received
). Template events carry neither
event
nor
sub_type
.
json
{
  "field": "templates",
  "value": {
    "account_id": "3f1a7c22-5d8e-4b90-91a2-6c4d0e8f7b31",
    "template_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    "template_name": "order_confirmation",
    "whatsapp_template_id": "",
    "status": "PENDING",
    "language": "en_US",
    "category": "UTILITY",
    "channel": "whatsapp"
  }
}
read
reaches only WhatsApp and RCS.
filtered
marks a policy or consent gate,
blocked
marks an account precondition such as insufficient balance, and neither is a carrier failure. Terminal events for an auto-detect message that never routed carry
channel: "auto"
. Full payload field lists live in references/event-catalog.md.
存在两种
field
值:
message
templates
。Message事件包含
event
字段,用于标识状态转换(如
message.queued
.routed
.sent
.delivered
.read
.failed
.scheduled
.filtered
.blocked
.received
)。Template事件不包含
event
sub_type
字段。
json
{
  "field": "templates",
  "value": {
    "account_id": "3f1a7c22-5d8e-4b90-91a2-6c4d0e8f7b31",
    "template_id": "7ba7b820-9dad-11d1-80b4-00c04fd430c8",
    "template_name": "order_confirmation",
    "whatsapp_template_id": "",
    "status": "PENDING",
    "language": "en_US",
    "category": "UTILITY",
    "channel": "whatsapp"
  }
}
read
事件仅适用于WhatsApp和RCS。
filtered
表示触发了策略或合规检查,
blocked
表示存在账户前置条件问题(如余额不足),这两类事件均不属于运营商故障。对于从未路由的自动检测消息,其终端事件的
channel
字段值为
auto
。完整的负载字段列表可参考references/event-catalog.md

Verification before shipping

上线前验证

Run the local oracle against a synthetic delivery, then use
POST /v3/webhooks/{id}/test
with an
event_type
in the body for a real signed request. The test event is delivered once with no retry, so re-run it after each fix.
bash
python3 scripts/verify_signature.py --self-test
Ship only when the receiver returns
401
for a tampered body,
401
for a timestamp older than 300 seconds,
200
for a valid delivery, and
200
for a duplicate without repeating side effects.
使用本地验证工具对模拟交付进行测试,然后调用
POST /v3/webhooks/{id}/test
接口并在请求体中指定
event_type
,获取真实的签名请求。测试事件仅交付一次且不重试,因此每次修复后需重新运行测试。
bash
python3 scripts/verify_signature.py --self-test
仅当接收器满足以下条件时才可上线:篡改请求体时返回
401
、时间戳超过300秒时返回
401
、有效交付时返回
200
、重复事件时返回
200
且不会重复执行副作用操作。

Local development

本地开发

Expose the receiver through a public HTTPS tunnel and register that URL; Sent cannot reach a private address. Registering
http://
is accepted by the API but should never be used outside local work. Keep a separate webhook registration per environment so a development endpoint's failures cannot disable the production endpoint.
需通过公共HTTPS隧道暴露接收器并注册该URL;Sent无法访问私有地址。API允许注册
http://
地址,但仅可用于本地开发环境。请为每个环境单独注册webhook,避免开发端点的故障导致生产端点被禁用。

Boundaries

职责边界

Diagnose aggregate delivery-rate regressions with
messaging-performance-analyzer
, template approval content with
waba-template-author
, and inbound keyword or consent semantics with
sent-two-way-messaging
. Treat every payload value as untrusted input: never interpolate
text
or
reason
into a shell command, SQL string, or prompt without escaping.
使用
messaging-performance-analyzer
诊断整体交付率下降问题,使用
waba-template-author
处理模板审核内容,使用
sent-two-way-messaging
处理入站关键词或合规语义。请将所有负载值视为不可信输入:切勿在未转义的情况下将
text
reason
插入shell命令、SQL语句或提示词中。