OpenAI webhooks 可讓你即時收到 API 事件通知,例如批次處理完成、背景回應生成,或微調作業完成。Webhooks 會依照 Standard Webhooks 規格,傳送至你掌控的 HTTP 端點。完整的 webhook 事件清單請參閱 API 參考文件。
若要接收 API 專案的失準監控通知,請參閱接收專案安全警示。
如需瞭解 Agents API 工作階段的事件與復原模式,請參閱工作階段 Webhooks。設定 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 儀表板中設定 webhook 端點,訂閱 response.completed,然後發出 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 端點、設定伺服器端程式碼來處理請求,以及驗證傳入的請求是否來自 OpenAI。
若要讓伺服器開始接收 webhook 請求,請登入儀表板並開啟 webhook 設定頁面。Webhooks 以專案為單位設定。
按一下「建立」按鈕來建立新的 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" }
}
你的端點應迅速以成功狀態碼(2xx)回覆這些傳入的 HTTP 請求,表示已成功接收。為避免逾時,建議將較繁重的處理工作交由背景工作程序執行,讓端點能立即回覆。
如果端點未傳回成功狀態碼(2xx),或未在幾秒內回覆,系統就會重試該 webhook 請求。OpenAI 會採用指數退避機制持續嘗試傳送,最長達 72 小時。請注意,系統不會跟隨 3xx 重新導向,而是將其視為失敗;你應更新端點,改用最終目的地的 URL。
在極少數情況下,OpenAI 可能因內部系統問題而重複傳送同一個 webhook 事件。你可以使用 webhook-id 標頭作為冪等鍵,排除重複事件。
測試 webhooks 需要可從公用網際網路存取的 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 規格的說明,自行實作簽章驗證
如果你遺失或不慎洩露簽署密鑰,可以透過輪替簽署密鑰來產生新的密鑰。