Webhook

处理邮件投递 Webhook

在 Python 应用中接收 Rovela 投递事件。涵盖 HMAC 签名验证、快速确认避免重试,以及可靠的后台事件处理。

当邮件送达、退信或被标记为垃圾邮件时,Rovela 会向你配置的 Webhook URL 发送 POST 请求。请求头中包含事件类型,请求体中包含事件详情。本指南说明如何验证请求确实来自 Rovela、如何快速确认以避免重试,以及如何在不丢失事件的情况下处理它。

为什么签名验证很重要

任何人都可以向你的 Webhook URL 发送 POST 请求。如果不做验证,伪造的请求可能让系统误以为邮件已送达,或者基于假数据触发下游操作。Rovela 对每个 Webhook 做 HMAC-SHA256 签名,签名在 x-resend-signature 请求头中,格式为 t=<时间戳>,v1=<十六进制摘要>,其中摘要是对「时间戳 + 英文句点 + 原始请求体」拼接字符串的签名结果。在信任内容之前,先验证签名。

验证签名

在 JSON 解析之前读取原始请求体。解析 x-resend-signature 请求头——格式为 t=<时间戳>,v1=<十六进制摘要>——提取时间戳和摘要,然后用 Webhook 密钥对字符串「<时间戳>.<原始请求体>」做 HMAC-SHA256。使用 hmac.compare_digest 而不是 ==,避免时序攻击逐字节泄露密钥。拒绝时间戳超过五分钟的请求,以防止重放攻击。

import hmac
import hashlib
import time

def verify_signature(raw_body: bytes, sig_header: str, secret: bytes) -> bool:
    # x-resend-signature format: t=<unix_timestamp>,v1=<hmac_sha256_hex>
    parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
    ts, v1 = parts.get("t", ""), parts.get("v1", "")
    if not ts or not v1:
        return False
    # Reject timestamps older than 5 minutes to block replay attacks
    if abs(time.time() - int(ts)) > 300:
        return False
    payload = ts.encode() + b"." + raw_body
    expected = hmac.new(secret, payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

先确认,再处理

Rovela 会重试 10 秒内未返回 2xx 的 Webhook。如果你的处理器要做耗时操作——写数据库、调其他 API——请先确认,再从队列或后台任务处理。先返回 2xx,再处理。超时的 Webhook 会被重试,重试可能产生重复事件,所以你的处理逻辑也要保证幂等。

# The server retries a webhook that does not answer 2xx within 10s.
# Acknowledge fast, do the work after, from a queue or a thread.
@app.post("/webhooks/rovela")
def rovela_webhook(req: Request):
    raw = asyncio.run(req.body())  # raw bytes, not parsed JSON
    sig = req.headers.get("x-resend-signature", "")
    if not verify_signature(raw, sig, SECRET):
        raise HTTPException(401)
    enqueue(req.headers["x-resend-event-type"], raw)  # handle later
    return {"ok": True}  # 2xx — acknowledged

处理事件类型

x-resend-event-type 请求头标明事件类型:email.delivered(已送达)、email.bounced(退信)、email.complained(投诉)、email.opened(已读)、email.clicked(点击)等。在处理器中根据事件类型分支处理。请求体中的 email_id 与发送 API 返回的 id 对应,可以将事件与原始邮件关联起来。

常见问题

如果 Rovela 发送 Webhook 时我的服务器宕机了怎么办?
Rovela 会以指数退避策略重试最多 5 次。只要你的端点在这个窗口内恢复,事件就会被送达。如果始终未成功,事件会被记录为失败,仍可在仪表盘的事件日志中查看。
是否需要对每种事件类型都验证签名?
是的。验证的是真实性,而非内容。退信事件和已读事件携带不同的数据,但签名验证步骤相同,都应优先执行。
可以用 FastAPI 以外的框架吗?
可以。签名验证是纯 Python 代码,在 Flask、Django、Starlette 或任何框架中都能使用。只有读取原始请求体和返回响应的方式不同。
如何在本地测试 Webhook?
用 ngrok 等隧道工具将本地服务暴露到公网,然后将该 URL 注册为 Webhook 端点。你发出的任何邮件或触发的任何事件,都会实时到达你的本地处理器。

准备好发送第一封邮件了吗?

Rovela 提供简洁的 HTTP API、专属域名业务邮箱、幂等键和投递 Webhook——免费套餐,无需绑卡。