Webhook を使うと、イベントストリームを開いたままにせずに、セッションの状態変化に応答できます。Webhook ハンドラーでは、サンドボックスのコンピューティング環境を起動したり、そこに再接続したりできます。アプリケーションの更新やワークフローの起動も可能です。
| イベント |
発生するタイミング |
agent.session.created |
セッションが作成されたとき |
agent.session.action_required |
セッションで関数の実行結果、環境への接続、またはコンピューターの使用に対する承認が必要になったとき |
agent.session.in_progress |
セッションがターンの処理を開始したとき |
agent.session.idle |
セッションがアイドル状態になり、次の入力を受け付けられるようになったとき |
agent.session.failed |
セッションが失敗状態に移行したとき |
agent.session.action_required イベントにはセッション ID と required_action.type が含まれます。後者の値は function_call、environment_connection、computer_use_approval_request のいずれかです。
1234567{
"type": "agent.session.action_required",
"data": {
"id": "sess_abc123",
"required_action": { "type": "function_call" }
}
}
セッションを取得し、required_actions で呼び出し ID、引数、環境 ID、またはコンピューターの使用に関する承認の詳細を確認します。これらの情報は Webhook に含まれません。コンピューターの使用では、ネストされた request.type で、ブラウザのオリジンへのアクセスを求めるリクエストか、認証を求めるリクエストかを判別できます。現在のリクエストをユーザーに表示し、ユーザーの回答をセッションイベントのエンドポイント経由で返します。リクエストとレスポンスの形式については、コンピューターの使用を参照してください。
共通の Webhook 設定ガイドに沿ってエンドポイントを作成し、Agents API のイベントを選択します。署名の検証に使うため、エンドポイントの署名シークレットを保存してください。
購読しているイベントが発生するたびに、OpenAI は署名付きの HTTP POST リクエストを送信します。
1234567891011121314{
"id": "evt_123",
"object": "event",
"created_at": 1750287018,
"type": "agent.session.created",
"data": {
"id": "sess_abc123",
"environment_id": "ccarenv_abc123",
"environment_type": "self_hosted",
"connect": {
"remote_url": "https://api.openai.com/v1/agents/api"
}
}
}
サンドボックスをプロビジョニングする前に、セッションの現在の状態を取得してください。詳しくは、サンドボックスのライフサイクルを参照してください。
セルフホスト型のセッションでは、agent.session.created にエグゼキューターの起動に必要な環境 ID と接続 URL が含まれます。ENVIRONMENT_ID には data.environment_id の値を、REMOTE_URL には data.connect.remote_url の値を設定します。この URL は、セッションの environment.remote_url として返される URL と同じです。両方の値を保存し、再接続時にも使用してください。
1234CODEX_API_KEY="$OPENAI_ENVIRONMENT_KEY" \
codex exec-server \
--remote "$REMOTE_URL" \
--environment-id "$ENVIRONMENT_ID"
環境キーを CODEX_API_KEY として使用します。アプリケーションの API キーは環境の外部で管理してください。
OPENAI_API_KEY と OPENAI_WEBHOOK_SECRET を設定します。Python の場合は fastapi、uvicorn、openai を、JavaScript の場合は express と openai をインストールします。
ハンドラーはポート 8000 でリクエストを待ち受け、署名を検証します。ポートを変更するには PORT を設定します。本番環境では、時間のかかる処理をキューに入れてください。
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
33import express from "express";
import OpenAI from "openai";
const app = express();
const webhooks = new OpenAI({
webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,
});
app.post(
"/webhooks/openai",
express.raw({ type: "application/json" }),
async (request, response) => {
const payload = request.body.toString("utf8");
try {
await webhooks.webhooks.verifySignature(payload, request.headers);
} catch {
response.status(400).send("Invalid signature");
return;
}
const event = JSON.parse(payload);
if (event.type === "agent.session.idle") {
const session = await webhooks.beta.agents.sessions.retrieve(
event.data.id
);
console.log("session idle event:", session.id);
} else {
console.log("session event:", event.type, event.data.id);
}
response.sendStatus(200);
}
);
app.listen(Number(process.env.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
31import json
import os
import uvicorn
from fastapi import FastAPI, Request, Response
from openai import AsyncOpenAI, InvalidWebhookSignatureError
app = FastAPI()
webhooks = AsyncOpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])
@app.post("/webhooks/openai")
async def handle_webhook(request: Request):
payload = await request.body()
try:
webhooks.webhooks.verify_signature(payload=payload, headers=request.headers)
except (InvalidWebhookSignatureError, ValueError):
return Response("Invalid signature", status_code=400)
event = json.loads(payload)
if event["type"] == "agent.session.idle":
session_id = event["data"]["id"]
session = await webhooks.beta.agents.sessions.retrieve(session_id, timeout=10)
print("session idle event:", session.id)
else:
print("session event:", event["type"], event["data"]["id"])
return Response(status_code=200)
if __name__ == "__main__":
uvicorn.run(app, port=int(os.environ.get("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
48
49
50
51
52
53
54import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/openai/openai-go/v3"
)
client := openai.NewClient()
http.HandleFunc("/webhooks/openai", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Invalid body", http.StatusBadRequest)
return
}
if err := client.Webhooks.VerifySignature(body, r.Header); err != nil {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
var event struct {
Type string `json:"type"`
Data struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if event.Type == "agent.session.idle" {
session, err := client.Beta.Agents.Sessions.Get(r.Context(), event.Data.ID)
if err != nil {
http.Error(w, "Could not retrieve session", http.StatusInternalServerError)
return
}
fmt.Println("session idle event:", session.ID)
} else {
fmt.Println("session event:", event.Type, event.Data.ID)
}
w.WriteHeader(http.StatusOK)
})
port := os.Getenv("PORT")
if port == "" {
port = "8000"
}
if err := http.ListenAndServe(":"+port, nil); err != nil {
panic(err)
}
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
48
49
50
51
52
53
54
55
56import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.Headers;
import com.openai.errors.InvalidWebhookSignatureException;
import com.openai.models.webhooks.WebhookVerificationParams;
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
OpenAIClient client = OpenAIOkHttpClient.fromEnv();
var json = new JsonMapper();
int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));
var server = HttpServer.create(new InetSocketAddress(port), 0);
server.createContext(
"/webhooks/openai",
exchange -> {
try (exchange) {
if (!exchange.getRequestMethod().equals("POST")) {
exchange.sendResponseHeaders(405, -1);
return;
}
String payload =
new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
try {
client
.webhooks()
.verifySignature(
WebhookVerificationParams.builder()
.payload(payload)
.headers(Headers.builder().putAll(exchange.getRequestHeaders()).build())
.build());
} catch (InvalidWebhookSignatureException e) {
exchange.sendResponseHeaders(400, -1);
return;
}
var event = json.readTree(payload);
if (event.path("type").asText().equals("agent.session.idle")) {
var session =
client
.beta()
.agents()
.sessions()
.retrieve(event.path("data").path("id").asText());
System.out.println("session idle event: " + session.id());
} else {
System.out.println(
"session event: "
+ event.path("type").asText()
+ " "
+ event.path("data").path("id").asText());
}
exchange.sendResponseHeaders(200, -1);
}
});
server.start();
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
30require "openai"
require "webrick"
require "json"
client = OpenAI::Client.new
server = WEBrick::HTTPServer.new(Port: Integer(ENV.fetch("PORT", "8000")))
server.mount_proc "/webhooks/openai" do |request, response|
if request.request_method != "POST"
response.status = 405
next
end
payload = request.body
begin
client.webhooks.verify_signature(payload, request.header.transform_values(&:first))
rescue OpenAI::Errors::InvalidWebhookSignatureError
response.status = 400
response.body = "Invalid signature"
next
end
event = JSON.parse(payload)
if event["type"] == "agent.session.idle"
session = client.beta.agents.sessions.retrieve(event.fetch("data").fetch("id"))
puts "session idle event: #{session.id}"
else
puts "session event: #{event["type"]} #{event.dig("data", "id")}"
end
response.status = 200
end
trap("INT") { server.shutdown }
server.start
初回または追加の入力の処理に、未接続のセルフホスト型エグゼキューターが必要な場合、API は environment_connection を必須アクションに追加します。API が agent.session.action_required を発行するのは、接続待ちに入る前です。
セッションを取得し、required_actions で接続が引き続き求められていることを確認します。session.environment.id と session.environment.remote_url を使ってエグゼキューターを起動します。この Webhook には connect.remote_url は含まれません。待機時間内にエグゼキューターが接続すると、API は必須アクションを解除し、送信済みの入力の処理を再開します。クライアントによる再送信は不要です。
API は接続を最大 5 分間待ちます。その間、追加入力のリクエストは応答待ちのままになることがあります。この待機時間を考慮して、クライアントとプロキシのタイムアウトを設定してください。agent.session.in_progress は実行が始まったことを示します。API が接続を待っていることを示すものではありません。
待機時間内に接続できなければ、送信済みの入力の処理は失敗します。初回入力の処理は非同期で失敗することがあり、その場合、セッションは failed 状態になります。接続を待つ仕組みには、入力を永続化するキューはありません。プロセスがクラッシュしたり、クライアントの接続が切れたりした場合は、再試行が必要になることがあります。
agent.session.idle は、セッションが次の入力を受け付けられることを示します。直前のターンが成功したとは限りません。そのターンのステータスを調べるか、セッションストリームで agent.session.turn.completed、agent.session.turn.failed、agent.session.turn.cancelled のいずれが発生したかを確認してください。完了したターンにも、失敗したツール呼び出しが含まれることがあります。ツールの実行結果とエージェントの最終応答を確認してください。
agent.session.failed はセッションの失敗を通知します。ターンが失敗するたびに通知されるわけではありません。セッションの削除に対応する Webhook はありません。また、セッションを削除しても、プロバイダー側のコンピューティングリソースは停止しません。