先验签、再持久化、后返回 2xx,耗时任务交给 Worker。不要把 API Key 放进浏览器,不要先解析 JSON 后再验签,也不要因为收到一次已签名请求就认为事件只会处理一次。OpenAI 当前说明:非 2xx 或数秒内未响应会触发指数退避重试,最长可持续 72 小时;少数事件可能重复投递。
官方资料(2026-10-30 核对):Webhooks guide · Webhook event reference · OpenAI Node SDK Webhooks · OpenAI Python SDK verification
OpenAI Webhooks 解决什么问题?
Webhook 是 OpenAI 在订阅事件发生时向你的公开服务器发送的 HTTP POST。它适合后台 Response 完成、失败或取消等异步状态通知,避免应用持续高频轮询。事件通常只包含事件类型和资源 ID;业务系统仍应使用自己的服务端凭证查询资源的最终状态和完整结果。
| 机制 | 方向 | 适合 | 关键风险 |
|---|---|---|---|
| Webhook | OpenAI → 你的服务器 | 异步事件通知 | 伪造、重复、延迟、乱序 |
| 轮询 | 你的服务器 → OpenAI | 补偿、对账、低频状态检查 | 过度请求、429、成本 |
| SSE Streaming | OpenAI → 原请求连接 | 边生成边展示 | 连接中断、半截输出 |
| Function Calling | 模型 → 你的工具循环 | 模型请求应用执行动作 | 权限、参数和副作用幂等 |
单个长任务的创建、轮询和取消见 Background Mode 教程;增量展示见 API Streaming;模型请求外部动作见 Function Calling。
第一步:在 Dashboard 创建项目 Webhook 端点
- 进入 OpenAI Dashboard 当前项目的 Webhook 设置。
- 填写只供管理识别的端点名称。
- 填写服务器可公开访问的最终 HTTPS URL,例如
https://app.example.com/api/openai/webhook。 - 只选择业务真正处理的事件类型,减少无关流量和日志。
- 创建后立即把 signing secret 写入生产 Secret Manager 或部署平台环境变量。
官方说明创建端点后会给出签名 Secret,之后不能再次查看。不要放进 Git、前端变量、日志、工单或截图;丢失或泄露时应轮换,而不是继续复用。
Dashboard 支持发送样例测试事件。测试能验证网络和处理分支,但样例成功不代表生产幂等、数据库事务、队列积压和密钥轮换已经正确。
第二步:保留原始请求体,再调用官方 SDK 验签
请求包含 webhook-id、webhook-timestamp 和 webhook-signature。签名覆盖原始请求体;JSON 中间件解析后再序列化可能改变字节,因此必须先读取原始文本或字节。
import OpenAI, { InvalidWebhookSignatureError } from "openai";
const client = new OpenAI({
webhookSecret: process.env.OPENAI_WEBHOOK_SECRET
});
export async function POST(request) {
const rawBody = await request.text();
let event;
try {
event = await client.webhooks.unwrap(rawBody, request.headers);
} catch (error) {
if (error instanceof InvalidWebhookSignatureError) {
return new Response("invalid signature", { status: 400 });
}
return new Response("invalid webhook", { status: 400 });
}
// 只有这里之后,event 和 webhook-id 才可以被信任
const deliveryId = request.headers.get("webhook-id");
await storeVerifiedDeliveryAtomically({
deliveryId,
eventId: event.id,
eventType: event.type,
resourceId: event.data?.id,
rawBody
});
return new Response("ok", { status: 200 });
}unwrap() 会先验证再解析。若要自己做 Schema 校验或路由,可以先 await client.webhooks.verifySignature(rawBody, headers),成功后再 JSON.parse。Node SDK 当前默认时间戳容差为 300 秒;验签依赖服务器时钟,必须保持 NTP 同步。不要为了“修复偶发失败”把容差无限放大。
Express、Next.js 与 Serverless 最容易错在哪里?
| 环境 | 正确做法 | 常见错误 |
|---|---|---|
| Web API / Next Route Handler | await request.text(),原始 headers 直接传 SDK | 先 request.json() |
| Express | 此路由用 express.text({ type: "application/json" }) | 全局 express.json() 先消费正文 |
| Serverless / Edge | 确认运行时支持 SDK 所需 Web Crypto,并保留原始正文 | 平台适配器只暴露解析后的对象 |
| 反向代理 | 原样转发正文和三个签名头 | 修改正文、移除头或先跳转 |
验签失败时先检查 Secret 是否属于当前端点和环境、正文是否被解析或压缩、三个头是否被代理保留,以及服务器时钟。不要记录完整签名 Secret 或用户敏感正文来排错。
第三步:用 webhook-id 做持久去重
官方明确说明少数情况下同一事件可能重复投递,并建议把 webhook-id 作为幂等键。签名验真不等于 exactly-once:同一份合法请求可以多次通过验签。
BEGIN;
INSERT INTO webhook_inbox
(endpoint, delivery_id, event_id, event_type, resource_id, status)
VALUES
('openai-main', :webhook_id, :event_id, :event_type, :resource_id, 'queued')
ON CONFLICT (endpoint, delivery_id) DO NOTHING;
-- 只有新记录才写入 outbox/queue job
INSERT INTO outbox (...) SELECT ... WHERE inserted = true;
COMMIT;- 唯一键至少包含端点或环境与
webhook-id,避免测试和生产互相碰撞。 - “记录已处理”与业务副作用最好位于同一事务或可靠 outbox 流程。
- 重复投递已经成功处理时返回 2xx,不要故意返回错误让它继续重试。
- 资源终态更新也要幂等,例如按 response ID 只允许状态单向进入终态。
- 不要仅用进程内 Set 或短 TTL 缓存;多实例和重启后会失效。
第四步:先可靠入队,再在数秒内返回 2xx
OpenAI 建议端点快速返回成功状态,并把非平凡处理交给后台 Worker。正确边界不是“收到请求立即 200”,而是“验签成功,且事件已可靠落库或入队后立即 200”。
- 读取原始正文和签名头。
- 验签并解析事件。
- 原子写入幂等收件箱与 outbox/队列。
- 返回 200 或其他 2xx。
- Worker 获取任务,按事件类型查询资源并执行幂等业务。
不要在接收器中同步下载大结果、调用多个外部系统、发送邮件或等待人工流程。这些操作会超过数秒,导致 OpenAI 认为失败并重投。反过来,在未落库前先 200,进程随后崩溃会造成无法自动补投的业务丢失。
第五步:按事件类型路由,再查询真实资源终态
当前参考列出 Response completed、failed、cancelled 等事件。事件的 data.id 指向资源;Worker 应使用正确 Project 的服务端 API Key 查询完整对象,并依据返回的真实状态更新业务。
async function processWebhook(job) {
switch (job.eventType) {
case "response.completed":
case "response.failed":
case "response.cancelled": {
const response = await client.responses.retrieve(job.resourceId);
await applyResponseStateIdempotently(response);
break;
}
default:
await markIgnoredButAcknowledged(job.deliveryId);
}
}- 事件类型使用 allowlist;未知新类型记录后安全忽略,不要默认执行副作用。
- 终态查询失败按 429、5xx、网络错误和权限错误分类,使用 Worker 自己的有限重试。
- 资源归属要绑定用户、Project 和内部任务,不能只凭传入 ID 越权读取。
- 处理完成记录 delivery ID、event ID、资源 ID、耗时与结果;敏感正文脱敏。
失败、重试和 3xx 应怎样处理?
如果端点未在数秒内返回 2xx,OpenAI 会指数退避重试,当前最长持续 72 小时。3xx 不会被跟随,也被视为失败,因此端点 URL 必须直接指向最终处理地址。
| 返回 | 含义 | 建议 |
|---|---|---|
| 2xx | 已可靠接收 | 只有验签且持久化成功后返回 |
| 400 | 签名/格式无效 | 拒绝副作用;检查 Secret、原始正文、头和时钟 |
| 429 | 接收器过载 | OpenAI 会重试,但应扩容、限并发并保护数据库 |
| 500/503 | 临时服务失败 | 修复依赖;依赖重复投递时仍须幂等 |
| 3xx | 不会跟随跳转 | Dashboard 改为最终 URL |
72 小时是投递尝试窗口,不是事件必须在 72 小时内完成全部业务的承诺。业务系统应监控最后成功接收时间、失败率、重复率、队列积压和最老任务年龄,并设置补偿查询。
轮换 Secret 与多环境部署
- 开发、预览和生产使用不同端点与 Secret,不让测试事件进入生产数据库。
- 环境变量命名清楚,并在部署后确认对应端点,不输出 Secret 值。
- 泄露或丢失后在 Dashboard 轮换 Secret,并以短暂双验证窗口完成无停机切换;具体能力以当前 Dashboard 为准。
- 先让新版本能验证新 Secret,再切换发送端,最后移除旧 Secret。
- 变更期间监控验签失败,避免时钟漂移或代理改写被误判为密钥问题。
本地测试与上线验收
- 使用公开 HTTPS 隧道或云开发环境,把最终 URL 配到测试项目。
- 从 Dashboard 发送样例事件,确认原始正文、三个签名头和 2xx。
- 重复发送同一已验签 payload,验证唯一约束只产生一个业务任务。
- 故意修改正文、签名和时间戳,确保 4xx 且无副作用。
- 让队列 Worker 暂停,确认接收器仍能可靠入队并快速 2xx。
- 模拟数据库失败,确认端点非 2xx,恢复后重复投递只处理一次。
- 使用真实 background Response 验证 completed/failed/cancelled 分支与资源查询。
- 检查日志、告警与 Dashboard,不包含 API Key、签名 Secret 或敏感正文。
Webhook 不替代 API 故障分类。Worker 查询资源遇到超时、500 或 503 时,继续按 API 5xx 与超时指南处理;遇到 429 看 速率限制指南。
上线前检查清单
- Dashboard 使用最终 HTTPS URL,不依赖 3xx。
- signing secret 存放在服务端 Secret Manager,未进入 Git 和前端。
- Webhook 路由保留原始正文和三个签名头。
- 官方 SDK 验签在 JSON 解析和业务逻辑之前完成。
- 服务器时钟同步,时间戳容差没有被任意放大。
webhook-id有跨实例持久唯一约束。- 验签与可靠持久化成功后数秒内返回 2xx。
- Worker 按资源 ID 查询真实终态,所有副作用幂等。
- 重复、乱序、延迟、未知事件、队列失败与 Secret 轮换均有测试。
- 监控接收成功率、重复率、积压、最老任务和补偿轮询。
常见问题
OpenAI Webhook 端点在哪里创建?
在 OpenAI Dashboard 的项目 Webhook 设置中创建端点,填写便于识别的名称、服务器可公开访问的最终 HTTPS URL,并选择需要订阅的事件类型。创建后立即保存 signing secret;官方说明它不会再次显示。
为什么 Webhook 验签必须使用原始请求体?
签名针对收到的原始字节计算。先用 JSON 中间件解析再重新序列化,可能改变空格、顺序或编码,导致合法请求验签失败。应先读取原始文本或字节,与原始 headers 一起交给官方 SDK 的 unwrap 或 verifySignature。
OpenAI Webhook 验签失败应该返回什么?
缺少签名头、时间戳不在容差内或签名无效时,应拒绝处理并返回 4xx,同时记录不含 Secret 和敏感正文的诊断信息。不要在验签失败后仍执行数据库、支付、发信或其他副作用。
OpenAI Webhook 为什么会重复投递?
官方说明少数情况下同一事件可能重复发送。验签只证明请求来源和时间新鲜度,不提供 exactly-once;应把经过验签的 webhook-id 作为投递幂等键,使用数据库唯一约束或同等的持久去重机制。
Webhook 返回失败后 OpenAI 会重试多久?
如果端点没有在数秒内返回成功的 2xx,OpenAI 会用指数退避重试,当前官方说明最长持续 72 小时。3xx 重定向不会被跟随,也会按失败处理,因此 Dashboard 中必须填写最终 URL。
什么时候应该向 Webhook 返回 200?
应在验签成功,并且事件已被持久写入幂等收件箱或可靠队列后尽快返回 2xx。不要等待模型结果下载或长业务流程;但也不要在尚未可靠保存事件前就返回成功,否则进程崩溃可能导致事件丢失。
Webhook 可以完全替代轮询吗?
不一定。Webhook 适合降低持续轮询,但应用仍应保存资源 ID,并在 Worker 中通过官方 API 查询最终 Response、Batch 或其他资源状态。对账、补偿和超过业务等待时间的任务仍可使用低频轮询或定期核对。
本文核对日期为 2026-10-30。OpenAI 事件类型、SDK 方法和投递策略可能调整,上线前请再次读取官方 Webhooks 指南与当前 SDK 文档。