SPB Git

Repository Public

webhooks.md 14.7 KB 433 lines markdown

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).
Files main
Menu

Theme