Repository Public
1# Session webhooks23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Use webhooks to respond to session state changes without keeping an event stream open. A webhook handler can [start or reconnect sandbox compute](https://developers.openai.com/api/docs/guides/agents-api/environments/lifecycle#set-up-webhook-managed-sandboxes), update your application, or trigger a workflow.67## Supported events89| Event | When it fires |10| ------------------------------- | ------------------------------------------------------------------------------------- |11| `agent.session.created` | A session is created. |12| `agent.session.action_required` | The session needs a function result, initial environment connection, or reconnection. |13| `agent.session.in_progress` | The session starts processing a turn. |14| `agent.session.idle` | The session is idle and ready for more input. |15| `agent.session.failed` | The session enters a failed state. |1617An `agent.session.action_required` event includes the session ID and a18`required_action.type` of `function_call` or `environment_connection`.1920```json21{22 "type": "agent.session.action_required",23 "data": {24 "id": "sess_abc123",25 "required_action": { "type": "function_call" }26 }27}28```2930Retrieve the session and inspect `required_actions` for call IDs, arguments, or31environment IDs. The webhook does not include those details.3233## Set up a webhook3435Follow the shared [webhook setup guide](https://developers.openai.com/api/docs/guides/webhooks#creating-webhook-endpoints) to create an endpoint and select Agents API events. Store the endpoint's signing secret for [signature verification](https://developers.openai.com/api/docs/guides/webhooks#verifying-webhook-signatures).3637## Receive events3839OpenAI sends a signed HTTP POST request whenever a subscribed event occurs:4041```json42{43 "id": "evt_123",44 "object": "event",45 "created_at": 1750287018,46 "type": "agent.session.created",47 "data": {48 "id": "sess_abc123",49 "environment_id": "ccarenv_abc123",50 "environment_type": "self_hosted",51 "connect": {52 "remote_url": "https://api.openai.com/v1/agents/api"53 }54 }55}56```5758596061Retrieve the session's current state before provisioning a sandbox. See [Sandbox lifecycle](https://developers.openai.com/api/docs/guides/agents-api/environments/lifecycle#handle-lifecycle-webhooks).6263### Start the executor6465For self-hosted sessions, `agent.session.created` includes the environment ID and connection URL needed to start an executor. Set `ENVIRONMENT_ID` to `data.environment_id` and `REMOTE_URL` to `data.connect.remote_url`. This is the same URL returned as `environment.remote_url` on the session. Save both values and reuse them on reconnect:6667```bash68CODEX_API_KEY="$OPENAI_ENVIRONMENT_KEY" \69codex exec-server \70 --remote "$REMOTE_URL" \71 --environment-id "$ENVIRONMENT_ID"72```7374Use an [environment key](https://developers.openai.com/api/docs/guides/agents-api/environments/self-hosted#authentication) as `CODEX_API_KEY`. Keep your application API key outside the environment.7576## Verify and process events7778Set `OPENAI_API_KEY` and `OPENAI_WEBHOOK_SECRET`. For Python, install `fastapi`, `uvicorn`, and `openai`. For JavaScript, install `express` and `openai`.7980The handlers verify signatures and listen on port 8000. Set `PORT` to change the port. In production, [queue slower work](https://developers.openai.com/api/docs/guides/agents-api/environments/lifecycle#handle-lifecycle-webhooks).8182Webhook handler8384```javascript85import express from "express";86import OpenAI from "openai";8788const app = express();89const webhooks = new OpenAI({90 webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,91});9293app.post(94 "/webhooks/openai",95 express.raw({ type: "application/json" }),96 async (request, response) => {97 const payload = request.body.toString("utf8");98 try {99 await webhooks.webhooks.verifySignature(payload, request.headers);100 } catch {101 response.status(400).send("Invalid signature");102 return;103 }104 const event = JSON.parse(payload);105 if (event.type === "agent.session.idle") {106 const session = await webhooks.beta.agents.sessions.retrieve(107 event.data.id108 );109 console.log("session idle event:", session.id);110 } else {111 console.log("session event:", event.type, event.data.id);112 }113 response.sendStatus(200);114 }115);116117app.listen(Number(process.env.PORT ?? 8000));118```119120```python121import json122import os123124import uvicorn125from fastapi import FastAPI, Request, Response126from openai import AsyncOpenAI, InvalidWebhookSignatureError127128app = FastAPI()129webhooks = AsyncOpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])130131132@app.post("/webhooks/openai")133async def handle_webhook(request: Request):134 payload = await request.body()135 try:136 webhooks.webhooks.verify_signature(payload=payload, headers=request.headers)137 except (InvalidWebhookSignatureError, ValueError):138 return Response("Invalid signature", status_code=400)139140 event = json.loads(payload)141 if event["type"] == "agent.session.idle":142 session_id = event["data"]["id"]143 session = await webhooks.beta.agents.sessions.retrieve(session_id, timeout=10)144 print("session idle event:", session.id)145 else:146 print("session event:", event["type"], event["data"]["id"])147 return Response(status_code=200)148149150if __name__ == "__main__":151 uvicorn.run(app, port=int(os.environ.get("PORT", "8000")))152```153154```go155import (156 "encoding/json"157 "fmt"158 "io"159 "net/http"160 "os"161162 "github.com/openai/openai-go/v3"163)164165client := openai.NewClient()166http.HandleFunc("/webhooks/openai", func(w http.ResponseWriter, r *http.Request) {167 if r.Method != http.MethodPost {168 w.WriteHeader(http.StatusMethodNotAllowed)169 return170 }171 body, err := io.ReadAll(r.Body)172 if err != nil {173 http.Error(w, "Invalid body", http.StatusBadRequest)174 return175 }176 if err := client.Webhooks.VerifySignature(body, r.Header); err != nil {177 http.Error(w, "Invalid signature", http.StatusBadRequest)178 return179 }180 var event struct {181 Type string `json:"type"`182 Data struct {183 ID string `json:"id"`184 } `json:"data"`185 }186 if err := json.Unmarshal(body, &event); err != nil {187 http.Error(w, "Invalid JSON", http.StatusBadRequest)188 return189 }190 if event.Type == "agent.session.idle" {191 session, err := client.Beta.Agents.Sessions.Get(r.Context(), event.Data.ID)192 if err != nil {193 http.Error(w, "Could not retrieve session", http.StatusInternalServerError)194 return195 }196 fmt.Println("session idle event:", session.ID)197 } else {198 fmt.Println("session event:", event.Type, event.Data.ID)199 }200 w.WriteHeader(http.StatusOK)201})202port := os.Getenv("PORT")203if port == "" {204 port = "8000"205}206if err := http.ListenAndServe(":"+port, nil); err != nil {207 panic(err)208}209```210211```java212import com.fasterxml.jackson.databind.json.JsonMapper;213import com.openai.client.OpenAIClient;214import com.openai.client.okhttp.OpenAIOkHttpClient;215import com.openai.core.http.Headers;216import com.openai.errors.InvalidWebhookSignatureException;217import com.openai.models.webhooks.WebhookVerificationParams;218import com.sun.net.httpserver.HttpServer;219import java.net.InetSocketAddress;220import java.nio.charset.StandardCharsets;221222OpenAIClient client = OpenAIOkHttpClient.fromEnv();223var json = new JsonMapper();224int port = Integer.parseInt(System.getenv().getOrDefault("PORT", "8000"));225var server = HttpServer.create(new InetSocketAddress(port), 0);226server.createContext(227 "/webhooks/openai",228 exchange -> {229 try (exchange) {230 if (!exchange.getRequestMethod().equals("POST")) {231 exchange.sendResponseHeaders(405, -1);232 return;233 }234 String payload =235 new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);236 try {237 client238 .webhooks()239 .verifySignature(240 WebhookVerificationParams.builder()241 .payload(payload)242 .headers(Headers.builder().putAll(exchange.getRequestHeaders()).build())243 .build());244 } catch (InvalidWebhookSignatureException e) {245 exchange.sendResponseHeaders(400, -1);246 return;247 }248 var event = json.readTree(payload);249 if (event.path("type").asText().equals("agent.session.idle")) {250 var session =251 client252 .beta()253 .agents()254 .sessions()255 .retrieve(event.path("data").path("id").asText());256 System.out.println("session idle event: " + session.id());257 } else {258 System.out.println(259 "session event: "260 + event.path("type").asText()261 + " "262 + event.path("data").path("id").asText());263 }264 exchange.sendResponseHeaders(200, -1);265 }266 });267server.start();268```269270```ruby271require "openai"272require "webrick"273require "json"274275client = OpenAI::Client.new276server = WEBrick::HTTPServer.new(Port: Integer(ENV.fetch("PORT", "8000")))277server.mount_proc "/webhooks/openai" do |request, response|278 if request.request_method != "POST"279 response.status = 405280 next281 end282 payload = request.body283 begin284 client.webhooks.verify_signature(payload, request.header.transform_values(&:first))285 rescue OpenAI::Errors::InvalidWebhookSignatureError286 response.status = 400287 response.body = "Invalid signature"288 next289 end290 event = JSON.parse(payload)291 if event["type"] == "agent.session.idle"292 session = client.beta.agents.sessions.retrieve(event.fetch("data").fetch("id"))293 puts "session idle event: #{session.id}"294 else295 puts "session event: #{event["type"]} #{event.dig("data", "id")}"296 end297 response.status = 200298end299trap("INT") { server.shutdown }300server.start301```302303304## Environment connection events305306When initial or follow-up input needs a disconnected self-hosted executor, the API adds an `environment_connection` required action. It emits `agent.session.action_required` **before waiting** for the connection.307308Retrieve the session and confirm that `required_actions` still requests a connection. Start the executor with `session.environment.id` and `session.environment.remote_url`. This webhook does not include `connect.remote_url`. If the executor connects before the wait expires, the API clears the required action and resumes the submission without client resubmission.309310The API waits up to five minutes for the connection. A follow-up input request can remain open during this wait. Configure client and proxy timeouts accordingly. `agent.session.in_progress` confirms execution has started, not that the API is waiting for a connection.311312If the wait expires, the submission fails. Initial input can fail asynchronously and leave the session in `failed`. The connection wait does not provide a durable input queue. A process crash or client disconnect may require retries.313314## Session and turn outcomes315316`agent.session.idle` means the session is ready for more input, not that its last turn succeeded. Inspect that turn's status or observe `agent.session.turn.completed`, `agent.session.turn.failed`, or `agent.session.turn.cancelled` on the session stream. A completed turn can still contain failed tool calls. Check tool results and the agent's final response.317318`agent.session.failed` reports a failed session, not every failed turn. Session deletion has no corresponding webhook and does not stop provider compute.