Repository Public
1# 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.45OpenAI [webhooks](http://chatgpt.com/?q=eli5+what+is+a+webhook?) allow you to receive real-time notifications about events in the API, such as when a batch completes, a background response is generated, or a fine-tuning job finishes. Webhooks are delivered to an HTTP endpoint you control, following the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md). The full list of webhook events can be found in the [API reference](https://developers.openai.com/api/reference/resources/webhooks).67To receive misalignment monitoring notifications for an API project, see [Receive project safety alerts](https://developers.openai.com/api/docs/guides/safety-checks/misalignment-monitoring#receive-project-safety-alerts).89For Agents API sessions, see [Session webhooks](https://developers.openai.com/api/docs/guides/agents-api/sessions/webhooks) for session events and recovery patterns. Use the endpoint setup, signature verification, and delivery guidance on this page for the webhook receiver.1011[API reference for webhook events12131415 View the full list of webhook events.](https://developers.openai.com/api/reference/resources/webhooks)1617Below are examples of servers capable of ingesting webhooks from OpenAI, specifically for the [`response.completed`](https://developers.openai.com/api/reference/resources/webhooks) event.1819For the Ruby examples, install the required dependencies with20`gem install openai webrick`, then set `OPENAI_API_KEY` and21`OPENAI_WEBHOOK_SECRET`.2223Webhooks server2425```javascript26import OpenAI from "openai";27import express from "express";2829const app = express();30const client = new OpenAI({ webhookSecret: process.env.OPENAI_WEBHOOK_SECRET });3132// Don't use express.json() because signature verification needs the raw text body33app.use(express.text({ type: "application/json" }));3435app.post("/webhook", async (req, res) => {36 try {37 const event = await client.webhooks.unwrap(req.body, req.headers);3839 if (event.type === "response.completed") {40 const response_id = event.data.id;41 const response = await client.responses.retrieve(response_id);42 const output_text = response.output43 .filter((item) => item.type === "message")44 .flatMap((item) => item.content)45 .filter((contentItem) => contentItem.type === "output_text")46 .map((contentItem) => contentItem.text)47 .join("");4849 console.log("Response output:", output_text);50 }51 res.status(200).send();52 } catch (error) {53 if (error instanceof OpenAI.InvalidWebhookSignatureError) {54 console.error("Invalid signature", error);55 res.status(400).send("Invalid signature");56 } else {57 throw error;58 }59 }60});6162app.listen(8000, () => {63 console.log("Webhook server is running on port 8000");64});65```6667```python68import os69from openai import OpenAI, InvalidWebhookSignatureError70from flask import Flask, request, Response7172app = Flask(__name__)73client = OpenAI(webhook_secret=os.environ["OPENAI_WEBHOOK_SECRET"])747576@app.route("/webhook", methods=["POST"])77def webhook():78 try:79 # with webhook_secret set above, unwrap will raise an error if the signature is invalid80 event = client.webhooks.unwrap(request.data, request.headers)8182 if event.type == "response.completed":83 response_id = event.data.id84 response = client.responses.retrieve(response_id)85 print("Response output:", response.output_text)8687 return Response(status=200)88 except InvalidWebhookSignatureError as e:89 print("Invalid signature", e)90 return Response("Invalid signature", status=400)919293if __name__ == "__main__":94 app.run(port=8000)95```9697```ruby98require "openai"99require "webrick"100101client = OpenAI::Client.new(102 webhook_secret: ENV.fetch("OPENAI_WEBHOOK_SECRET")103)104105server = WEBrick::HTTPServer.new(106 BindAddress: "127.0.0.1",107 Port: Integer(ENV.fetch("OPENAI_WEBHOOK_PORT", "8000")),108 Logger: WEBrick::Log.new($stderr, WEBrick::BasicLog::WARN),109 AccessLog: []110)111response_workers = []112113server.mount_proc("/webhook") do |request, response|114 if request.request_method != "POST"115 response.status = 405116 next117 end118119 headers = request.header.transform_values(&:first)120 event = client.webhooks.unwrap(request.body, headers)121122 if event.is_a?(OpenAI::Models::Webhooks::ResponseCompletedWebhookEvent)123 response_workers.select!(&:alive?)124 response_workers << Thread.new(event.data.id) do |response_id|125 completed_response = client.responses.retrieve(response_id)126 puts "Response output: #{completed_response.output_text}"127 end128 end129130 response.status = 200131 response.body = "ok"132rescue OpenAI::Errors::InvalidWebhookSignatureError, ArgumentError => error133 warn "Invalid signature: #{error.message}"134 response.status = 400135 response.body = "Invalid signature"136ensure137 server.shutdown if ENV["OPENAI_WEBHOOK_EXIT_AFTER_REQUEST"] == "1"138end139140Signal.trap("INT") { server.shutdown }141port = server.listeners.first.addr[1]142puts "Webhook server listening on http://127.0.0.1:#{port}/webhook"143$stdout.flush144server.start145response_workers.each(&:join)146```147148149To see a webhook like this one in action, you can set up a webhook endpoint in the OpenAI dashboard subscribed to `response.completed`, and then make an API request to [generate a response in background mode](https://developers.openai.com/api/docs/guides/background).150151You can also trigger test events with sample data from the [webhook settings page](https://platform.openai.com/settings/project/webhooks).152153Generate a background response154155```bash156curl https://api.openai.com/v1/responses \157-H "Content-Type: application/json" \158-H "Authorization: Bearer $OPENAI_API_KEY" \159-d '{160 "model": "gpt-6-astra",161 "input": "Write a very long novel about otters in space.",162 "background": true163}'164```165166```javascript167import OpenAI from "openai";168const client = new OpenAI();169170const resp = await client.responses.create({171 model: "gpt-6-astra",172 input: "Write a very long novel about otters in space.",173 background: true,174});175176console.log(resp.status);177```178179```python180from openai import OpenAI181182client = OpenAI()183184resp = client.responses.create(185 model="gpt-6-astra",186 input="Write a very long novel about otters in space.",187 background=True,188)189190print(resp.status)191```192193```go194package main195196import (197 "context"198 "fmt"199200 "github.com/openai/openai-go/v3"201 "github.com/openai/openai-go/v3/responses"202)203204func main() {205 client := openai.NewClient()206207 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{208 Model: "gpt-6-astra",209 Background: openai.Bool(true),210 Input: responses.ResponseNewParamsInputUnion{211 OfString: openai.String("Write a very long novel about otters in space."),212 },213 })214 if err != nil {215 panic(err)216 }217218 fmt.Println(response.Status)219}220```221222```java223import com.openai.client.OpenAIClient;224import com.openai.client.okhttp.OpenAIOkHttpClient;225import com.openai.models.responses.ResponseCreateParams;226227ResponseCreateParams params =228 ResponseCreateParams.builder()229 .model("gpt-6-astra")230 .input("Write a detailed market analysis.")231 .background(true)232 .build();233234var response = client.responses().create(params);235System.out.println(response.status().orElseThrow());236```237238```csharp239using OpenAI.Responses;240#pragma warning disable OPENAI001241242string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;243ResponsesClient client = new(key);244245CreateResponseOptions options = new()246{247 Model = "gpt-6-astra",248 BackgroundModeEnabled = true,249};250options.InputItems.Add(251 ResponseItem.CreateUserMessageItem("Write a very long novel about otters in space.")252);253254ResponseResult response = await client.CreateResponseAsync(options);255Console.WriteLine(response.Status);256```257258```ruby259require "openai"260261client = OpenAI::Client.new262response = client.responses.create(263 model: "gpt-6-astra",264 input: "Write a detailed market analysis.",265 background: true266)267268puts(response.status)269```270271272In this guide, you will learn how to create webhook endpoints in the dashboard, set up server-side code to handle them, and verify that inbound requests originated from OpenAI.273274## Creating webhook endpoints275276To start receiving webhook requests on your server, log in to the dashboard and [open the webhook settings page](https://platform.openai.com/settings/project/webhooks). Webhooks are configured per-project.277278Click the "Create" button to create a new webhook endpoint. You will configure three things:279280- A name for the endpoint (just for your reference).281- A public URL to a server you control.282- One or more event types to subscribe to. When they occur, OpenAI will send an HTTP POST request to the URL specified.283284<img src="https://cdn.openai.com/API/images/webhook_config.png"285 alt="webhook endpoint edit dialog"286 width="450"287 style={{ margin: "16px 0" }}288/>289290After creating a new webhook, you'll receive a signing secret to use for server-side verification of incoming webhook requests. Save this value for later, since you won't be able to view it again.291292With your webhook endpoint created, you'll next set up a server-side endpoint to handle those incoming event payloads.293294## Handling webhook requests on a server295296When an event happens that you're subscribed to, your webhook URL will receive an HTTP POST request like this:297298```299POST https://yourserver.com/webhook300user-agent: OpenAI/1.0 (+https://platform.openai.com/docs/webhooks)301content-type: application/json302webhook-id: wh_685342e6c53c8190a1be43f081506c52303webhook-timestamp: 1750287078304webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=305{306 "object": "event",307 "id": "evt_685343a1381c819085d44c354e1b330e",308 "type": "response.completed",309 "created_at": 1750287018,310 "data": { "id": "resp_abc123" }311}312```313314Your endpoint should respond quickly to these incoming HTTP requests with a successful (`2xx`) status code, indicating successful receipt. To avoid timeouts, we recommend offloading any non-trivial processing to a background worker so that the endpoint can respond immediately.315If the endpoint doesn't return a successful (`2xx`) status code, or doesn't respond within a few seconds, the webhook request will be retried. OpenAI will continue to attempt delivery for up to 72 hours with exponential backoff. Note that `3xx` redirects will not be followed; they are treated as failures and your endpoint should be updated to use the final destination URL.316317In rare cases, due to internal system issues, OpenAI may deliver duplicate copies of the same webhook event. You can use the `webhook-id` header as an idempotency key to deduplicate.318319### Testing webhooks locally320321Testing webhooks requires a URL that is available on the public Internet. This can make development tricky, since your local development environment likely isn't open to the public. A few options that may help:322323- [ngrok](https://ngrok.com/) which can expose your localhost server on a public URL324- Cloud development environments like [Replit](https://replit.com/), [GitHub Codespaces](https://github.com/features/codespaces), [Cloudflare Workers](https://workers.cloudflare.com/), or [v0 from Vercel](https://v0.dev/).325326## Verifying webhook signatures327328While you can receive webhook events from OpenAI and process the results without any verification, you should verify that incoming requests are coming from OpenAI, especially if your webhook will take any kind of action on the backend. The headers sent along with webhook requests contain information that can be used in combination with a webhook secret key to verify that the webhook originated from OpenAI.329330When you create a webhook endpoint in the OpenAI dashboard, you'll be given a signing secret that you should make available on your server as an environment variable:331332```333export OPENAI_WEBHOOK_SECRET="<your secret here>"334```335336The simplest way to verify webhook signatures is by using the `unwrap()` method of the official OpenAI SDK helpers:337338Signature verification with the OpenAI SDK339340```javascript341const client = new OpenAI();342const webhook_secret = process.env.OPENAI_WEBHOOK_SECRET;343if (!webhook_secret) throw new Error("Set OPENAI_WEBHOOK_SECRET.");344345// will throw if the signature is invalid346const event = await client.webhooks.unwrap(347 req.body,348 req.headers,349 webhook_secret350);351```352353```python354import os355356from flask import request357from openai import OpenAI358359client = OpenAI()360webhook_secret = os.environ["OPENAI_WEBHOOK_SECRET"]361362# will raise if the signature is invalid363event = client.webhooks.unwrap(364 request.data,365 request.headers,366 secret=webhook_secret,367)368```369370```ruby371require "openai"372require "webrick"373374client = OpenAI::Client.new(375 api_key: ENV.fetch("OPENAI_API_KEY"),376 webhook_secret: ENV.fetch("OPENAI_WEBHOOK_SECRET")377)378server = WEBrick::HTTPServer.new(379 BindAddress: "127.0.0.1",380 Port: Integer(ENV.fetch("OPENAI_WEBHOOK_PORT", "8000")),381 Logger: WEBrick::Log.new($stderr, WEBrick::BasicLog::WARN),382 AccessLog: []383)384385server.mount_proc("/webhook") do |request, response|386 if request.request_method != "POST"387 response.status = 405388 next389 end390391 headers = request.header.transform_values(&:first)392 event = client.webhooks.unwrap(request.body, headers)393 puts "Verified webhook event: #{event.type}"394395 response.status = 200396 response.body = "ok"397rescue OpenAI::Errors::InvalidWebhookSignatureError, ArgumentError398 response.status = 400399 response.body = "Invalid signature"400ensure401 server.shutdown if ENV["OPENAI_WEBHOOK_EXIT_AFTER_REQUEST"] == "1"402end403404Signal.trap("INT") { server.shutdown }405port = server.listeners.first.addr[1]406puts "Webhook server listening on http://127.0.0.1:#{port}/webhook"407$stdout.flush408server.start409```410411412Signatures can also be verified with the [Standard Webhooks libraries](https://github.com/standard-webhooks/standard-webhooks/tree/main?tab=readme-ov-file#reference-implementations):413414Signature verification with Standard Webhooks libraries415416```rust417use standardwebhooks::Webhook;418419let webhook_secret = std::env::var("OPENAI_WEBHOOK_SECRET").expect("OPENAI_WEBHOOK_SECRET not set");420let wh = Webhook::new(webhook_secret);421wh.verify(webhook_payload, webhook_headers).expect("Webhook verification failed");422```423424```php425$webhook_secret = getenv("OPENAI_WEBHOOK_SECRET");426$wh = new \StandardWebhooks\Webhook($webhook_secret);427$wh->verify($webhook_payload, $webhook_headers);428```429430431Alternatively, if needed, you can implement your own signature verification [as described in the Standard Webhooks spec](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md#verifying-webhook-authenticity)432433If you misplace or accidentally expose your signing secret, you can generate a new one by [rotating the signing secret](https://platform.openai.com/settings/project/webhooks).