웹훅을 사용하면 이벤트 스트림을 계속 열어 두지 않아도 세션 상태 변화에 대응할 수 있습니다. 웹훅 핸들러에서 샌드박스 컴퓨팅 리소스를 시작하거나 다시 연결할 수 있고, 애플리케이션을 업데이트하거나 워크플로우를 실행할 수도 있습니다.
| 이벤트 |
발생 시점 |
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 또는 컴퓨터 사용 승인 세부 정보를 확인하세요. 웹훅에는 이 정보가 포함되지 않습니다. 컴퓨터 사용 요청의 경우 중첩된 request.type 필드로 브라우저 출처(origin) 접근 요청인지 인증 요청인지 구분할 수 있습니다. 현재 요청을 사용자에게 보여 준 뒤 세션 이벤트 엔드포인트로 사용자의 응답을 보내세요. 요청과 응답 형식은 컴퓨터 사용을 참고하세요.
공통 웹훅 설정 가이드에 따라 엔드포인트를 만들고 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을 사용해 실행기를 시작하세요. 이 웹훅에는 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는 세션 자체가 실패했을 때 발생합니다. 개별 턴이 실패할 때마다 발생하는 이벤트는 아닙니다. 세션 삭제에 해당하는 웹훅은 없으며, 세션을 삭제해도 제공업체의 컴퓨팅 리소스는 중지되지 않습니다.