Webhooks
M Express 会向你的接收端发送配送事件。投递至少一次,并可能乱序到达,因此接收、验签、持久 去重、排序、确认和异步处理共同构成完整的集成契约。
接收前准备
准备能接收 HTTPS POST、在解析 JSON 前保留原始字节、并持久保存事件记录的服务端接收器。 按 key ID 管理签名密钥;密钥不得进入浏览器、移动端、日志或支持截图。
接收端实现流程
- 在解析 JSON 前,读取完全未改动的原始请求字节。
- 根据 key ID 选择当前有效的签名密钥。
- 对原始字节计算并校验 HMAC-SHA256 签名。
- 在产生副作用前,持久记录
event_id。 - 对重复的
event_id不再产生第二次业务处理。 - 快速返回正常大小的
2xx,再执行较慢的下游工作。
客户可见状态应按 payload 的 scanned_at 排列,不应按请求抵达的先后排序。先持久记录 event_id,尽快确认,再异步执行较慢的下游工作。
读取请求并验签
请求包含 Content-Type: application/json,以及以下四个 webhook 请求头:
| Header | 作用 |
|---|---|
X-Delivery-Webhook-Id | 用于去重的事件标识。 |
X-Delivery-Webhook-Timestamp | 参与 HMAC 输入的 Unix epoch 秒数。 |
X-Delivery-Webhook-Key-Id | 指明应使用哪个有效签名密钥。 |
X-Delivery-Webhook-Signature | 带有 v1= 前缀的 HMAC-SHA256 值。 |
http
POST <your receiver URL>
Content-Type: application/json
X-Delivery-Webhook-Id: evt_...
X-Delivery-Webhook-Timestamp: 1751422800
X-Delivery-Webhook-Key-Id: whk_...
X-Delivery-Webhook-Signature: v1=...HMAC-SHA256 的准确输入为:
text
<timestamp>.<event_id>.<raw_body>raw_body 是未改动的请求数据。无效签名必须在解析或处理 JSON 前被拒绝。以下示例是契约示例, 展示验签操作而不是完整的 HTTP 接收器。
js
import crypto from "node:crypto";
function validWebhook({ timestamp, eventId, signature, secret, rawBody }) {
const input = Buffer.concat([
Buffer.from(`${timestamp}.${eventId}.`, "utf8"),
rawBody,
]);
const expected = `v1=${crypto.createHmac("sha256", secret).update(input).digest("hex")}`;
const received = Buffer.from(signature, "utf8");
const wanted = Buffer.from(expected, "utf8");
return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}python
import hashlib
import hmac
def valid_webhook(timestamp, event_id, signature, secret, raw_body):
signing_input = b".".join((
timestamp.encode("ascii"), event_id.encode("utf-8"), raw_body,
))
expected = "v1=" + hmac.new(
secret.encode("utf-8"), signing_input, hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)安全确认
| 接收端结果 | 投递结果 |
|---|---|
任意正常大小的 2xx | 已确认。 |
408、425、429 或 5xx | 可重试。 |
3xx 或其他 4xx | 永久失败。 |
任意状态下响应体大于 8192 字节 | 永久失败。 |
在执行副作用前先持久记录 event_id,再在耗时的下游处理前返回确认。这样即使 worker 延迟或重启, 重试行为仍然安全。
事件目录
webhook.test 用于验收你的接收端;其余条目都是业务事件。pod.completed 只是事件名,不通过 本公开指南提供 POD 媒体。
| Event | Code |
|---|---|
webhook.test | EVT000 |
shipment.created | EVT010 |
shipment.updated | EVT020 |
package.pickup_scanned | EVT100 |
package.inbound_scanned | EVT200 |
package.loaded | EVT300 |
package.delivery_scanned | EVT350 |
pod.completed | EVT400 |
shipment.delivered | EVT410 |
shipment.partially_delivered | EVT420 |
shipment.exception | EVT500 |
shipment.return_requested | EVT600 |
shipment.returned_to_sender | EVT700 |
验证清单
- [ ] 在 JSON 解析前保留原始请求字节。
- [ ] 四个 header 齐全,并用
<timestamp>.<event_id>.<raw_body>进行常量时间 HMAC 比较。 - [ ] 在产生业务副作用前已持久、幂等地记录
event_id。 - [ ] 客户可见顺序使用
scanned_at,而非抵达顺序。 - [ ] 确认响应为
2xx且响应体不超过8192字节。 - [ ] 上线前已成功完成
webhook.test/EVT000验收。