OpenAI Webhooks

2026-11-09 15:17 更新

Webhook让应用实时接收API事件通知,例如批处理完成、后台响应生成或微调作业结束。通知通过HTTP发送到你控制的端点,遵循Standard Webhooks规范,事件全集见API参考。

项目偏离意图监控见安全告警;组织级安全标识符警告/停用见安全执行通知。Agents API事件与恢复见会话Webhook,接收端仍应使用本页端点、验签和投递规则。

下面服务端示例处理response.completed。Ruby先安装gem install openai webrick,设置OPENAI_API_KEY及OPENAI_WEBHOOK_SECRET。

来源示例 1(javascript):

// [中文] 创建Webhook接收服务并处理response.completed;环境需设置密钥与签名密钥,保留原始请求体以便验签。
import OpenAI from "openai";
import express from "express";

const app = express();
const client = new OpenAI({ webhookSecret: process.env.OPENAI_WEBHOOK_SECRET });

// Don't use express.json() because signature verification needs the raw text body
app.use(express.text({ type: "application/json" }));

app.post("/webhook", async (req, res) => {
  try {
    const event = await client.webhooks.unwrap(req.body, req.headers);

    if (event.type === "response.completed") {
      const response_id = event.data.id;
      const response = await client.responses.retrieve(response_id);
      const output_text = response.output
        .filter((item) => item.type === "message")
        .flatMap((item) => item.content)
        .filter((contentItem) => contentItem.type === "output_text")
        .map((contentItem) => contentItem.text)
        .join("");

      console.log("Response output:", output_text);
    }
    res.status(200).send();
  } catch (error) {
    if (error instanceof OpenAI.InvalidWebhookSignatureError) {
      console.error("Invalid signature", error);
      res.status(400).send("Invalid signature");
    } else {
      throw error;
    }
  }
});

app.listen(8000, () => {
  console.log("Webhook server is running on port 8000");
});

来源示例 2(python):

## [中文] 创建Webhook接收服务并处理response.completed;环境需设置密钥与签名密钥,保留原始请求体以便验签。
import os
from openai import OpenAI, InvalidWebhookSignatureError
from flask import Flask, request, Response

app = Flask(__name__)
client = OpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])


@app.route("/webhook", methods=["POST"])
def webhook():
    try:
        # with webhook_secret set above, unwrap will raise an error if the signature is invalid
        event = client.webhooks.unwrap(request.data, request.headers)

        if event.type == "response.completed":
            response_id = event.data.id
            response = client.responses.retrieve(response_id)
            print("Response output:", response.output_text)

        return Response(status=200)
    except InvalidWebhookSignatureError as e:
        print("Invalid signature", e)
        return Response("Invalid signature", status=400)


if __name__ == "__main__":
    app.run(port=8000)

来源示例 3(ruby):

## [中文] 创建Webhook接收服务并处理response.completed;环境需设置密钥与签名密钥,保留原始请求体以便验签。
require "openai"
require "webrick"

client = OpenAI::Client.new(
  webhook_secret: ENV.fetch("OPENAI_WEBHOOK_SECRET")
)

server = WEBrick::HTTPServer.new(
  BindAddress: "127.0.0.1",
  Port: Integer(ENV.fetch("OPENAI_WEBHOOK_PORT", "8000")),
  Logger: WEBrick::Log.new($stderr, WEBrick::BasicLog::WARN),
  AccessLog: []
)
response_workers = []

server.mount_proc("/webhook") do |request, response|
  if request.request_method != "POST"
    response.status = 405
    next
  end

  headers = request.header.transform_values(&:first)
  event = client.webhooks.unwrap(request.body, headers)

  if event.is_a?(OpenAI::Models::Webhooks::ResponseCompletedWebhookEvent)
    response_workers.select!(&:alive?)
    response_workers << Thread.new(event.data.id) do |response_id|
      completed_response = client.responses.retrieve(response_id)
      puts "Response output: #{completed_response.output_text}"
    end
  end

  response.status = 200
  response.body = "ok"
rescue OpenAI::Errors::InvalidWebhookSignatureError, ArgumentError => error
  warn "Invalid signature: #{error.message}"
  response.status = 400
  response.body = "Invalid signature"
ensure
  server.shutdown if ENV["OPENAI_WEBHOOK_EXIT_AFTER_REQUEST"] == "1"
end

Signal.trap("INT") { server.shutdown }
port = server.listeners.first.addr[1]
puts "Webhook server listening on http://127.0.0.1:#{port}/webhook"
$stdout.flush
server.start
response_workers.each(&:join)

可在面板订阅该事件,再创建后台响应进行测试;Webhook设置也可发送示例事件。

来源示例 4(bash):

## [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
  "model": "gpt-6-astra",
  "input": "Write a very long novel about otters in space.",
  "background": true
}'

来源示例 5(javascript):

// [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
import OpenAI from "openai";
const client = new OpenAI();

const resp = await client.responses.create({
  model: "gpt-6-astra",
  input: "Write a very long novel about otters in space.",
  background: true,
});

console.log(resp.status);

来源示例 6(python):

## [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
from openai import OpenAI

client = OpenAI()

resp = client.responses.create(
    model="gpt-6-astra",
    input="Write a very long novel about otters in space.",
    background=True,
)

print(resp.status)

来源示例 7(go):

// [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
package main

import (
    "context"
    "fmt"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/responses"
)

func main() {
    client := openai.NewClient()

    response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
        Model:      "gpt-6-astra",
        Background: openai.Bool(true),
        Input: responses.ResponseNewParamsInputUnion{
            OfString: openai.String("Write a very long novel about otters in space."),
        },
    })
    if err != nil {
        panic(err)
    }

    fmt.Println(response.Status)
}

来源示例 8(java):

// [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.ResponseCreateParams;

ResponseCreateParams params =
    ResponseCreateParams.builder()
        .model("gpt-6-astra")
        .input("Write a detailed market analysis.")
        .background(true)
        .build();

var response = client.responses().create(params);
System.out.println(response.status().orElseThrow());

来源示例 9(csharp):

// [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
using OpenAI.Responses;
#pragma warning disable OPENAI001

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
ResponsesClient client = new(key);

CreateResponseOptions options = new()
{
    Model = "gpt-6-astra",
    BackgroundModeEnabled = true,
};
options.InputItems.Add(
    ResponseItem.CreateUserMessageItem("Write a very long novel about otters in space.")
);

ResponseResult response = await client.CreateResponseAsync(options);
Console.WriteLine(response.Status);

来源示例 10(ruby):

## [中文] 生成后台响应以触发订阅事件;需先配置公开Webhook接收端,未创建真实事件。
require "openai"

client = OpenAI::Client.new
response = client.responses.create(
  model: "gpt-6-astra",
  input: "Write a detailed market analysis.",
  background: true
)

puts(response.status)

本页介绍配置接收端、处理事件和验证请求确实来自OpenAI。

创建Webhook端点

在项目Webhook设置点Create,设置方便识别的名称、你控制服务器的公开URL、一个或多个订阅事件。事件发生时发送HTTP POST。组织级事件按组织安全通知配置。

Webhook端点配置对话框

创建后会得到签名密钥,用于服务端验证。应保存,因为以后无法再次查看。接着建立服务器接收路由。

在服务器处理Webhook请求

订阅事件会发来如下HTTP POST:

来源示例 11():

HTTP请求头与JSON正文的组合示例,不是单独JSON文档。webhook-id可用于去重,签名仍需验证。

POST https://yourserver.com/webhook
user-agent: OpenAI/1.0 (+https://platform.openai.com/docs/webhooks)
content-type: application/json
webhook-id: wh_685342e6c53c8190a1be43f081506c52
webhook-timestamp: 1750287078
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
  "object": "event",
  "id": "evt_685343a1381c819085d44c354e1b330e",
  "type": "response.completed",
  "created_at": 1750287018,
  "data": { "id": "resp_abc123" }
}

应尽快返回成功2xx表示收妥,复杂处理交给后台任务,避免超时。未返回2xx或数秒内不响应会重试,OpenAI按指数退避最多尝试投递72小时。不跟随3xx跳转,应把配置改为最终URL。

极少情况下会重复投递,可用webhook-id作为幂等键去重。

本地测试Webhook

需要公网可达地址。本地开发可使用ngrok暴露接收端,或Replit、GitHub Codespaces、Cloudflare Workers、Vercel v0等云环境。本地稿并未启动服务或暴露端口。

验证Webhook签名

即使技术上可不验签就处理,也应验证来源,尤其后端会执行动作时。请求头结合签名密钥可验证请求真实性。把面板创建时给出的密钥放进服务端环境变量:

来源示例 12():

## [中文] 将签名密钥放入服务端环境变量,不能提交真实密钥。
export OPENAI_WEBHOOK_SECRET="<your secret here>"

最简单的方法是官方SDK的unwrap()辅助方法:

来源示例 13(javascript):

// [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
const client = new OpenAI();
const webhook_secret = process.env.OPENAI_WEBHOOK_SECRET;
if (!webhook_secret) throw new Error("Set OPENAI_WEBHOOK_SECRET.");

// will throw if the signature is invalid
const event = await client.webhooks.unwrap(
  req.body,
  req.headers,
  webhook_secret
);

来源示例 14(python):

## [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
import os

from flask import request
from openai import OpenAI

client = OpenAI()
webhook_secret = os.environ["OPENAI_WEBHOOK_SECRET"]

## will raise if the signature is invalid
event = client.webhooks.unwrap(
    request.data,
    request.headers,
    secret=webhook_secret,
)

来源示例 15(go):

// [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
// Verify the original bytes, before any JSON parsing.
event, err := client.Webhooks.Unwrap(body, r.Header)
if err != nil {
    http.Error(w, "Invalid webhook", http.StatusBadRequest)
    return
}
fmt.Println(event.Type)

来源示例 16(java):

// [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
import com.openai.client.OpenAIClient;
import com.openai.core.http.Headers;
import com.openai.models.webhooks.UnwrapWebhookEvent;
import com.openai.models.webhooks.WebhookVerificationParams;

// Your HTTP framework supplies the original request bytes and headers.
// Load secret from OPENAI_WEBHOOK_SECRET in your application.
// Reject InvalidWebhookSignatureException (invalid signature or timestamp)
// and IllegalArgumentException (missing required headers).
public static UnwrapWebhookEvent verifyWebhook(
    OpenAIClient client, byte[] body, Headers headers, String secret) {
  return client
      .webhooks()
      .unwrap(
          WebhookVerificationParams.builder()
              .payload(body)
              .headers(headers)
              .secret(secret)
              .build());
}

来源示例 17(ruby):

## [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
require "openai"
require "webrick"

client = OpenAI::Client.new(
  api_key: ENV.fetch("OPENAI_API_KEY"),
  webhook_secret: ENV.fetch("OPENAI_WEBHOOK_SECRET")
)
server = WEBrick::HTTPServer.new(
  BindAddress: "127.0.0.1",
  Port: Integer(ENV.fetch("OPENAI_WEBHOOK_PORT", "8000")),
  Logger: WEBrick::Log.new($stderr, WEBrick::BasicLog::WARN),
  AccessLog: []
)

server.mount_proc("/webhook") do |request, response|
  if request.request_method != "POST"
    response.status = 405
    next
  end

  headers = request.header.transform_values(&:first)
  event = client.webhooks.unwrap(request.body, headers)
  puts "Verified webhook event: #{event.type}"

  response.status = 200
  response.body = "ok"
rescue OpenAI::Errors::InvalidWebhookSignatureError, ArgumentError
  response.status = 400
  response.body = "Invalid signature"
ensure
  server.shutdown if ENV["OPENAI_WEBHOOK_EXIT_AFTER_REQUEST"] == "1"
end

Signal.trap("INT") { server.shutdown }
port = server.listeners.first.addr[1]
puts "Webhook server listening on http://127.0.0.1:#{port}/webhook"
$stdout.flush
server.start

也可使用Standard Webhooks库:

来源示例 18(rust):

// [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
use standardwebhooks::Webhook;

let webhook_secret = std::env::var("OPENAI_WEBHOOK_SECRET").expect("OPENAI_WEBHOOK_SECRET not set");
let wh = Webhook::new(webhook_secret);
wh.verify(webhook_payload, webhook_headers).expect("Webhook verification failed");

来源示例 19(php):

// [中文] 使用官方SDK unwrap或Standard Webhooks库验证原始请求体与签名头;验签成功后再处理,签名密钥来自环境变量。
$webhook_secret = getenv("OPENAI_WEBHOOK_SECRET");
$wh = new \StandardWebhooks\Webhook($webhook_secret);
$wh->verify($webhook_payload, $webhook_headers);

需要自建时按验签规范实现。密钥丢失或泄露时,在设置页轮换生成新密钥。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号