세션 웹훅
세션 웹훅 (Session webhooks)
웹훅을 사용하면 이벤트 스트림을 열어 두지 않아도 세션 상태 변경에 응답할 수 있어요. 웹훅 핸들러는 샌드박스 컴퓨팅을 시작하거나 다시 연결하고, 애플리케이션을 갱신하거나, 워크플로를 트리거할 수 있어요.
출처: 문서
본문
지원되는 이벤트 (Supported events)
| 이벤트 | 발생 시점 |
|---|---|
agent.session.created |
세션이 생성될 때. |
agent.session.action_required |
세션이 함수 결과, 초기 환경 연결, 또는 재연결을 필요로 할 때. |
agent.session.in_progress |
세션이 턴 처리를 시작할 때. |
agent.session.idle |
세션이 유휴 상태이고 더 많은 입력을 받을 준비가 됐을 때. |
agent.session.failed |
세션이 실패 상태에 들어갈 때. |
agent.session.action_required 이벤트는 세션 ID와 function_call 또는 environment_connection 유형의 required_action.type을 포함해요.
{
"type": "agent.session.action_required",
"data": {
"id": "sess_abc123",
"required_action": { "type": "function_call" }
}
}
세션을 검색하고 호출 ID, 인자, 환경 ID를 위해 required_actions를 검사하세요. 웹훅은 이런 세부 정보를 포함하지 않아요.
웹훅 설정하기 (Set up a webhook)
공용 웹훅 설정 가이드를 따라 엔드포인트를 만들고 Agents API 이벤트를 선택하세요. 서명 검증을 위해 엔드포인트의 서명 시크릿을 저장하세요.
이벤트 받기 (Receive events)
구독한 이벤트가 발생할 때마다 OpenAI가 서명된 HTTP POST 요청을 보내요:
{
"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"
}
}
}
샌드박스를 프로비저닝하기 전에 세션의 현재 상태를 검색하세요. Sandbox lifecycle을 참고하세요.
실행기 시작하기 (Start the executor)
자체 호스팅 세션에서는 agent.session.created가 실행기를 시작하는 데 필요한 환경 ID와 연결 URL을 포함해요. ENVIRONMENT_ID를 data.environment_id로, REMOTE_URL을 data.connect.remote_url로 설정하세요. 이것은 세션의 environment.remote_url로 반환되는 것과 같은 URL이에요. 두 값을 저장하고 재연결 시 재사용하세요:
CODEX_API_KEY="$OPENAI_ENVIRONMENT_KEY" \
codex exec-server \
--remote "$REMOTE_URL" \
--environment-id "$ENVIRONMENT_ID"
CODEX_API_KEY로 환경 키를 사용하세요. 애플리케이션 API 키는 환경 밖에 두세요.
이벤트 검증하고 처리하기 (Verify and process events)
OPENAI_API_KEY와 OPENAI_WEBHOOK_SECRET을 설정하세요. Python에서는 fastapi, uvicorn, openai를, JavaScript에서는 express와 openai를 설치하세요.
핸들러는 서명을 검증하고 포트 8000에서 수신해요. PORT로 포트를 바꿀 수 있어요. 프로덕션에서는 느린 작업을 큐에 넣으세요.
웹훅 핸들러
import 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));
import 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")))
import (
"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)
}
import 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();
require "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
환경 연결 이벤트 (Environment connection events)
초기 또는 후속 입력이 연결되지 않은 자체 호스팅 실행기를 필요로 하면 API가 environment_connection 필수 작업을 추가해요. 연결을 기다리기 전에 agent.session.action_required를 내보내요.
세션을 검색하고 required_actions가 여전히 연결을 요청하는지 확인하세요. session.environment.id와 session.environment.remote_url로 실행기를 시작하세요. 이 웹훅은 connect.remote_url을 포함하지 않아요. 대기 시간이 만료되기 전에 실행기가 연결되면 API가 필수 작업을 지우고 클라이언트가 다시 제출하지 않아도 제출을 재개해요.
API는 연결을 최대 5분 기다려요. 후속 입력 요청은 이 대기 동안 열려 있을 수 있어요. 클라이언트와 프록시 타임아웃을 그에 맞게 구성하세요. agent.session.in_progress는 실행이 시작됐음을 확인하는 것이지, API가 연결을 기다리고 있다는 뜻이 아니에요.
대기 시간이 만료되면 제출이 실패해요. 초기 입력은 비동기로 실패할 수 있고 세션을 failed 상태로 남길 수 있어요. 연결 대기는 지속적인 입력 큐를 제공하지 않아요. 프로세스 충돌이나 클라이언트 연결 끊김은 재시도가 필요할 수 있어요.
세션과 턴 결과 (Session and turn outcomes)
agent.session.idle은 세션이 더 많은 입력을 받을 준비가 됐다는 뜻이지, 마지막 턴이 성공했다는 뜻이 아니에요. 그 턴의 상태를 검사하거나 세션 스트림에서 agent.session.turn.completed, agent.session.turn.failed, agent.session.turn.cancelled를 관찰하세요. 완료된 턴에도 실패한 도구 호출이 있을 수 있어요. 도구 결과와 에이전트의 최종 응답을 확인하세요.
agent.session.failed는 실패한 세션을 보고하는 것이지, 모든 실패한 턴을 보고하는 게 아니에요. 세션 삭제에는 해당하는 웹훅이 없고, 프로바이더 컴퓨팅도 중지되지 않아요.
더 알아보기 (Learn more)
- Sandbox lifecycle에서 웹훅 관리형 샌드박스 설정을 확인하세요.
- webhooks 가이드에서 엔드포인트와 서명 검증을 더 알아보세요.