当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > OpenAI Responses API Webhook 怎么验签:原始请求体、时间窗口与 response.completed 处理

OpenAI Responses API Webhook 怎么验签:原始请求体、时间窗口与 response.completed 处理

来源:17golang原创 2026-08-16 15:39:15 0浏览 收藏

把 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

OpenAI Webhook 原始请求体经过签名校验后再解析为 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 响应。

response.completed 事件按 webhook-id 和 response id 去重后查询结果并更新业务状态的流程图
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 幂等处理。这样即使回调重复、处理进程重启,或者一次查询暂时失败,也能把补偿范围限制在一条可追踪的响应记录内。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Gemini Files API 怎么管理上传文件:ACTIVE 状态、48 小时过期与主动删除Gemini Files API 怎么管理上传文件:ACTIVE 状态、48 小时过期与主动删除
上一篇
Gemini Files API 怎么管理上传文件:ACTIVE 状态、48 小时过期与主动删除
MCP 工具注解怎么做安全验收:readOnlyHint、destructiveHint 与幂等边界
下一篇
MCP 工具注解怎么做安全验收:readOnlyHint、destructiveHint 与幂等边界
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ljg-skills -
    ljg-skills
    ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
    4891次使用
  • MELO音乐 - AI 音乐生成平台,支持多模态创作能力
    MELO音乐
    MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
    4474次使用
  • UniScribe - AI 免费在线音视频转文字平台
    UniScribe
    UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
    4414次使用
  • 剧云 - 免费 AI 智能中文剧本创作平台
    剧云
    剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
    4651次使用
  • 万象有声 - AI 一站式有声内容创作平台
    万象有声
    万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
    4606次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码