OpenAI Responses API Webhook 怎么验签:原始请求体、时间窗口与 response.completed 处理
把 OpenAI Responses API 的长任务放到后台后,服务端一般没必要一直占着连接做轮询。Webhook 可以把 response.completed、失败或取消事件推回业务接口,但回调一到就直接更新订单、发通知,风险也跟着来了:请求体可能被中间件改写,重复投递可能让同一任务被重复执行两次,没做验签的请求绝对不能直接进入业务逻辑分支。
可靠的处理顺序是:先读取原始请求体,再用 OpenAI SDK 验证签名,验证通过后解析事件;随后用事件 ID 或响应 ID 做幂等判断,最后才查询 Responses API 并更新业务状态。
要点速览
request.data或原始字节必须先保存,不能先解析 JSON 再验签。- 官方 SDK 的
webhooks.unwrap()会同时完成签名校验和事件解析。 - 签名头包含事件标识与时间信息,默认时间容忍窗口为 5 分钟,服务端时钟要保证可靠。
response.completed只说明响应生成完成,业务结果仍应按data.id查询并幂等落库。
先把回调链路拆成三段
一条可维护的链路至少分成三个部分:入口层接收 HTTP body,安全层验证签名,业务层根据事件类型处理结果。入口层不要把 body 交给会自动格式化 JSON 的中间件之后就丢掉原文,因为签名校验针对的是接收到的原始内容。
OpenAI 的 Webhook 事件使用 webhook-id 标识一次投递,webhook-timestamp 表示投递时间,webhook-signature 携带签名。事件类型则放在 JSON 的 type 字段中;后台 Responses 完成时,常见类型是 response.completed。

Python 中保留原始 body 再验签
下面用 Flask 写一个最小入口示例。request.get_data() 取到的是请求到达时的原始字节;先把它交给 client.webhooks.unwrap(),成功后 SDK 才会返回已解析的事件对象。
import os
from flask import Flask, request
from openai import OpenAI
app = Flask(__name__)
client = OpenAI()
secret = os.environ["OPENAI_WEBHOOK_SECRET"]
@app.post("/hooks/openai")
def receive_openai_hook():
raw_body = request.get_data()
try:
event = client.webhooks.unwrap(
raw_body,
request.headers,
secret=secret,
)
except Exception:
return {"message": "invalid signature"}, 400
if event.type == "response.completed":
response_id = event.data.id
# 这里交给幂等队列,避免在 HTTP 请求中做长事务
print("verified response", response_id)
elif event.type in {"response.failed", "response.cancelled"}:
print("verified non-success event", event.type)
else:
print("ignored event", event.type)
return {"ok": True}, 200
如果希望把验证和解析拆开,可以先调用 verify_signature(),再对同一份原始 body 做 JSON 解析。两种写法都遵循同一个原则:原始 body 不能在验签前被重新序列化。
时间窗口不是装饰:它决定重放边界
签名正确不代表请求永远有效。官方 SDK 会检查时间戳,默认容忍窗口是 300 秒;超过窗口的旧请求应直接拒绝,服务器时间明显超前也会导致校验失败。容器里的时钟漂移、代理缓存回放、手工重放旧 body,都会在这个环节被拦截。
排查时可以按这个顺序走:
- 记录
webhook-id、接收时间和 HTTP 状态,不要记录签名密钥。 - 比对应用节点与可信时间源的差异,重点排查跨可用区节点。
- 确认反向代理没有缓存 POST 请求,也没有把旧请求重新投递到业务入口。
- 需要人工补偿时重新发起业务查询,不要把历史 body 原样当成新回调处理。
response.completed 到业务落库还差一步
收到 response.completed 后,先用 webhook-id 做投递级幂等,再用 event.data.id 做响应级幂等。前者防范同一条 Webhook 重复到达,后者防范业务重试、补偿任务和多次通知共同处理同一个 Responses 响应。

def handle_completed(event, store, responses_client):
delivery_key = event.id
response_id = event.data.id
if store.has_delivery(delivery_key):
return "duplicate delivery"
store.save_delivery(delivery_key, response_id)
if store.has_response(response_id):
return "already applied"
response = responses_client.responses.retrieve(response_id)
store.save_response(
response_id=response.id,
status=response.status,
output_text=response.output_text,
)
return "applied"
这里把查询结果落库放在幂等记录之后,实际项目还需要事务或唯一索引兜底。如果处理过程可能超过网关超时,入口只负责验签并写入内部队列,消费者再完成查询和业务更新。
四个容易误判的失败现场
| 现象 | 优先检查 | 处理方向 |
|---|---|---|
| 验签全部失败 | body 是否被解析后重组 | 改为读取原始字节 |
| 偶发时间戳过期 | 节点时钟、代理缓存 | 修正时间同步并禁用 POST 缓存 |
| 同一响应落库两次 | 是否只按 HTTP 请求次数判断 | 给 webhook-id 与 response id 建唯一约束 |
| 收到完成事件但查不到结果 | 查询时机与租户配置 | 进入短暂重试队列,保留原事件 |
常见问题
Webhook 验签必须自己实现 HMAC 吗?
不必优先自己拼接签名算法。Python、Node.js、Go 等官方 SDK 都提供了验证入口;只有在特殊运行环境下,才考虑使用 Standard Webhooks 兼容库,并严格保留原始 body 和三个签名相关请求头。
为什么先 json.loads 再验签会失败?
JSON 重新序列化可能改变空格、转义或字段顺序,得到的字节串就不再是服务端签名时的原文。应先验证收到的原始字符串或字节,再做解析。
response.completed 可以直接当成业务成功吗?
不能。它表示 Responses 响应完成,业务还要读取响应状态和输出内容,并根据自身规则判断是否可以更新订单、消息或任务记录。
重复 Webhook 要返回什么状态?
只要签名有效,重复事件可以快速返回成功,避免发送方持续重试;真正的业务去重由数据库唯一约束或幂等存储完成。
把安全门槛放在业务分支之前
Webhook 入口最值得坚持的顺序只有一句话:原始 body 保留好,签名先验,事件再解析,结果按两个 ID 幂等处理。这样即使回调重复、处理进程重启,或者一次查询暂时失败,也能把补偿范围限制在一条可追踪的响应记录内。
Gemini Files API 怎么管理上传文件:ACTIVE 状态、48 小时过期与主动删除
- 上一篇
- Gemini Files API 怎么管理上传文件:ACTIVE 状态、48 小时过期与主动删除
- 下一篇
- MCP 工具注解怎么做安全验收:readOnlyHint、destructiveHint 与幂等边界
-
- 科技周边 · 人工智能 | 2天前 |
- Tokenizer padding_side 影响批量生成的对齐
- 206浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 | python · 人工智能 · 数据迭代 Hugging Face Datasets streaming IterableDataset 大语料
- Datasets streaming 读取大语料的迭代方式
- 337浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 | python · Transformers generate 停止条件 批量输出
- Transformers generate 的停止条件与批量输出
- 133浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 | 人工智能 · LoRa 显存优化 bitsandbytes QLoRA 4-bit量化
- bitsandbytes 量化模型配合 LoRA 训练的显存边界
- 249浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 | 人工智能 · LoRa PEFT load_adapter set_adapter 适配器切换
- PEFT LoRA 适配器按任务切换的加载方案
- 268浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 | 人工智能 · LoRa PEFT merge_and_unload 适配器合并
- PEFT 适配器合并后为什么输出会变化
- 221浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 |
- Tokenizer 左填充和右填充应该怎么选
- 399浏览 收藏
-
- 科技周边 · 人工智能 | 2天前 |
- Safetensors 为什么支持按需读取权重切片
- 258浏览 收藏
-
- 科技周边 · 人工智能 | 3天前 |
- MLflow Model Alias 怎么替代固定版本号部署
- 360浏览 收藏
-
- 科技周边 · 人工智能 | 3天前 | 索引优化 · 向量数据库 · 向量检索 FAISS Index Factory IVF PQ
- FAISS Index Factory 字符串怎么组合索引结构
- 100浏览 收藏
-
- 科技周边 · 人工智能 | 3天前 |
- 知识库切片重叠率怎么影响检索结果
- 268浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 280次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 334次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 329次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 299次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 120次使用
-
- 本地大模型反复输出同一句话怎么调整生成参数
- 2026-09-06 501浏览
-
- Python 调用大模型时如何用结构化输出校验 JSON:从解析失败到可重试
- 2026-08-29 501浏览
-
- AI写作工具免费版安装教程(含豆包Clawdbot)
- 2026-05-30 501浏览
-
- WPS AI能自动生成PPT吗?输入主题一键制作演示文稿
- 2026-05-27 501浏览
-
- Canva手机闪退解决方法及适配指南
- 2026-05-25 501浏览
