OpenAI Webhook 让您能够实时接收 API 事件通知,例如批处理完成、后台响应生成或微调任务完成的通知。Webhook 遵循 Standard Webhooks 规范,发送到由您控制的 HTTP 端点。完整的 Webhook 事件列表请参阅 API 参考。
如需接收 API 项目的对齐偏差监测通知,请参阅接收项目安全警报。
有关 Agents API 会话的事件和恢复模式,请参阅会话 Webhook。对于 Webhook 接收端,请遵循本页的端点设置、签名验证和投递指南。
以下服务器示例展示了如何接收 OpenAI 发送的 Webhook,具体以 response.completed 事件为例。
对于 Ruby 示例,请使用
gem install openai webrick 安装所需依赖项,然后设置 OPENAI_API_KEY 和
OPENAI_WEBHOOK_SECRET。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import 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");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48require "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 的实际运行方式,您可以在 OpenAI 控制台中设置一个订阅 response.completed 事件的 Webhook 端点,然后发出 API 请求,在后台模式下生成响应。
您也可以在 Webhook 设置页面使用示例数据触发测试事件。
1
2
3
4
5
6
7
8curl 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
}'
1
2
3
4
5
6
7
8
9
10import 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);
1
2
3
4
5
6
7
8
9
10
11from 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26package 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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13import 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());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17using 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);
1
2
3
4
5
6
7
8
9
10require "openai"
client = OpenAI::Client.new
response = client.responses.create(
model: "gpt-6-astra",
input: "Write a detailed market analysis.",
background: true
)
puts(response.status)
本指南将介绍如何在控制台中创建 Webhook 端点、设置服务端代码来处理 Webhook 请求,以及验证收到的请求是否来自 OpenAI。
要开始在服务器上接收 Webhook 请求,请登录控制台并打开 Webhook 设置页面。Webhook 按项目分别配置。
点击“创建”按钮,创建新的 Webhook 端点。您需要配置以下三项:
- 端点名称(仅供您参考)。
- 指向您控制的服务器的公开 URL。
- 要订阅的一种或多种事件类型。这些事件发生时,OpenAI 会向指定 URL 发送 HTTP POST 请求。
创建新的 Webhook 后,您会获得一个签名密钥,用于在服务端验证收到的 Webhook 请求。请保存此密钥以备后用,因为您将无法再次查看它。
创建 Webhook 端点后,接下来需要设置一个服务端端点,以处理收到的事件载荷。
当您订阅的事件发生时,您的 Webhook URL 会收到如下 HTTP POST 请求:
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" }
}
您的端点应迅速向收到的 HTTP 请求返回成功状态码(2xx),以确认接收成功。为避免超时,我们建议将较复杂的处理工作交给后台工作进程,以便端点能够立即响应。
如果端点未返回成功状态码(2xx),或未在几秒内响应,系统将重试该 Webhook 请求。OpenAI 会采用指数退避策略持续尝试投递,最长持续 72 小时。请注意,系统不会跟随 3xx 重定向,而是将其视为失败;您应更新端点,使用最终目标 URL。
在极少数情况下,由于内部系统问题,OpenAI 可能会重复投递同一个 Webhook 事件。您可以将 webhook-id 请求头用作幂等键来去重。
测试 Webhook 需要一个可通过公共互联网访问的 URL。这可能给开发带来困难,因为您的本地开发环境通常不对外开放。以下几种方案可能有所帮助:
虽然您可以在不进行任何验证的情况下接收 OpenAI 的 Webhook 事件并处理结果,但您仍应验证收到的请求是否来自 OpenAI,尤其是在 Webhook 会触发后端操作时。Webhook 请求附带的请求头包含验证所需的信息,可结合 Webhook 密钥来验证该 Webhook 是否来自 OpenAI。
在 OpenAI 控制台中创建 Webhook 端点时,您会获得一个签名密钥。您应将其设置为服务器上的环境变量:
export OPENAI_WEBHOOK_SECRET="<your secret here>"
验证 Webhook 签名最简单的方法是使用官方 OpenAI SDK 辅助工具提供的 unwrap() 方法:
1
2
3
4
5
6
7
8
9
10const 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
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14import 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,
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38require "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 库验证签名:
1
2
3
4
5use 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");
1
2
3$webhook_secret = getenv("OPENAI_WEBHOOK_SECRET");
$wh = new \StandardWebhooks\Webhook($webhook_secret);
$wh->verify($webhook_payload, $webhook_headers);
此外,如有需要,您可以按照 Standard Webhooks 规范中的说明自行实现签名验证
如果您丢失或意外泄露了签名密钥,可以通过轮换签名密钥生成新的密钥。