跳转到内容

Webhook 最佳实践

验证签名,将事件入队(数据库记录、队列、后台任务),然后立即返回 2xx。SpacePost 最多等待 15 秒,处理程序过慢会导致重试和重复投递。

投递至少会发生一次。请保存每次已处理投递的 webhook-id(唯一约束就足够了),并跳过重复的投递。

const inserted = await db.insert(processedWebhooks).values({ id: webhookId }).onConflictDoNothing().returning();
if (inserted.length === 0) return new Response("duplicate", { status: 200 });

使用原始请求体,拒绝时间戳超过 5 分钟的请求,并以恒定时间比较签名(SDK 和各类库都会这样做)。参见验证签名。

事件可能乱序到达,尤其是在重试之后。请将载荷中的 created_at 与你已存储的时间比较,并且只应用能让状态向前推进的更改。SpacePost 自身的 last_event 也遵循同样的规则。

事件 典型处理方式
email.bounced 在你的数据库中将该地址标记为无效,并停止向它发送。
email.complained 立即让该用户退订非必要邮件。
email.delivery_delayed 通常无需处理;如果是时效性很强的邮件(重置密码),则发出告警。
email.failed 记录 data.failed.reason,发出告警,必要时修复后重试。
email.suppressed 有收件人在你的抑制列表中;请在你的管理界面中展示。

只订阅你会处理的事件。事件越少,流量越小,失败也越少。

  • 使用有效证书,通过 HTTPS 提供服务。
  • 不要记录密钥或包含个人数据的完整载荷。
  • 使用专用路径,不承担其他功能,并返回通用的响应体。
  • 将 signing_secret 保存在密钥管理器中,而不是源代码仓库里。

请关注控制台中的投递日志,或对反复失败设置告警:如果你的端点宕机超过约 27 小时,所有重试次数都会用完,之后你就必须手动重放。