Skip to main content
The OpenAI Ruby SDK provides utilities for verifying and parsing webhook events from OpenAI. Webhook verification is optional but strongly encouraged for security. For more information about webhooks, see the OpenAI API documentation.

Setup

Initialize the client with your webhook secret:
require "openai"

client = OpenAI::Client.new(
  webhook_secret: ENV['OPENAI_WEBHOOK_SECRET']
)
The webhook secret is available in your OpenAI dashboard. Store it securely in an environment variable.
The unwrap method verifies the webhook signature and parses the payload in one step:
require 'sinatra'
require 'openai'

# Set up the client with webhook secret from environment variable
client = OpenAI::Client.new(webhook_secret: ENV['OPENAI_WEBHOOK_SECRET'])

post '/webhook' do
  request_body = request.body.read

  begin
    event = client.webhooks.unwrap(request_body, request.env)

    case event.type
    when 'response.completed'
      puts "Response completed: #{event.data}"
    when 'response.failed'
      puts "Response failed: #{event.data}"
    else
      puts "Unhandled event type: #{event.type}"
    end

    status 200
    'ok'
  rescue StandardError => e
    puts "Invalid signature: #{e}"
    status 400
    'Invalid signature'
  end
end
The body parameter must be the raw JSON string sent from the server. Do not parse it before passing to unwrap - the method will parse it for you after verifying the signature.

Verify Signature Method

If you prefer to verify and parse separately, use verify_signature:
require 'sinatra'
require 'json'
require 'openai'

# Set up the client with webhook secret from environment variable
client = OpenAI::Client.new(webhook_secret: ENV['OPENAI_WEBHOOK_SECRET'])

post '/webhook' do
  request_body = request.body.read

  begin
    # Verify the signature
    client.webhooks.verify_signature(request_body, request.env)

    # Parse the body after verification
    event = JSON.parse(request_body)
    puts "Verified event: #{event}"

    status 200
    'ok'
  rescue StandardError => e
    puts "Invalid signature: #{e}"
    status 400
    'Invalid signature'
  end
end
Like unwrap, the body parameter must be the raw JSON string. Parse it yourself only after verification succeeds.

Comparison: Unwrap vs Verify

Framework Examples

require 'sinatra'
require 'openai'

client = OpenAI::Client.new(
  webhook_secret: ENV['OPENAI_WEBHOOK_SECRET']
)

post '/webhook' do
  request_body = request.body.read

  begin
    event = client.webhooks.unwrap(request_body, request.env)

    # Handle event
    puts "Received: #{event.type}"

    status 200
    'ok'
  rescue StandardError => e
    status 400
    'Invalid signature'
  end
end

Event Types

Common webhook event types:
event = client.webhooks.unwrap(request_body, request.env)

case event.type
when 'response.completed'
  # A response has completed successfully
  response_id = event.data.id
  output = event.data.output

when 'response.failed'
  # A response has failed
  error = event.data.error
  puts "Error: #{error.message}"

when 'response.streaming.delta'
  # Streaming delta received
  delta = event.data.delta

when 'batch.completed'
  # Batch processing completed
  batch_id = event.data.id

when 'batch.failed'
  # Batch processing failed
  error = event.data.error

else
  puts "Unhandled event type: #{event.type}"
end

Error Handling

post '/webhook' do
  request_body = request.body.read

  begin
    event = client.webhooks.unwrap(request_body, request.env)

    # Process event
    process_webhook_event(event)

    status 200
    'ok'
  rescue OpenAI::Errors::WebhookSignatureError => e
    # Invalid signature
    logger.error "Invalid webhook signature: #{e.message}"
    status 401
    'Unauthorized'
  rescue JSON::ParserError => e
    # Invalid JSON
    logger.error "Invalid webhook payload: #{e.message}"
    status 400
    'Bad Request'
  rescue StandardError => e
    # Other errors
    logger.error "Webhook processing error: #{e.message}"
    status 500
    'Internal Server Error'
  end
end

Testing Webhooks

Testing with Mock Data

require 'rspec'
require 'rack/test'

RSpec.describe 'Webhook Handler' do
  include Rack::Test::Methods

  let(:client) do
    OpenAI::Client.new(
      webhook_secret: 'test_secret'
    )
  end

  it 'processes valid webhook' do
    # You would need to generate a valid signature here
    # This is a simplified example
    payload = {
      type: 'response.completed',
      data: { id: 'resp_123' }
    }.to_json

    post '/webhook', payload, {
      'CONTENT_TYPE' => 'application/json',
      'HTTP_X_OPENAI_SIGNATURE' => generate_signature(payload)
    }

    expect(last_response.status).to eq(200)
  end

  it 'rejects invalid signature' do
    payload = { type: 'response.completed' }.to_json

    post '/webhook', payload, {
      'CONTENT_TYPE' => 'application/json',
      'HTTP_X_OPENAI_SIGNATURE' => 'invalid_signature'
    }

    expect(last_response.status).to eq(400)
  end
end

Best Practices

1

Always verify signatures in production

Webhook verification prevents malicious actors from sending fake events to your endpoint.
2

Use the unwrap method

The unwrap method is simpler and less error-prone than manual verification and parsing.
3

Store webhook secrets securely

Never hardcode webhook secrets. Use environment variables or a secure configuration system.
4

Return 200 quickly

Process events asynchronously if possible. OpenAI expects a quick 200 response to confirm receipt.
5

Handle unknown event types gracefully

New event types may be added. Log unknown events instead of failing.
6

Implement idempotency

Webhooks may be delivered more than once. Use event IDs to prevent duplicate processing.

Asynchronous Processing

For long-running event processing, queue events for background processing:
require 'sidekiq'

class WebhookProcessor
  include Sidekiq::Worker

  def perform(event_json)
    event = JSON.parse(event_json)

    case event['type']
    when 'response.completed'
      # Process completed response
    when 'response.failed'
      # Handle failed response
    end
  end
end

post '/webhook' do
  request_body = request.body.read

  begin
    # Verify and parse
    event = client.webhooks.unwrap(request_body, request.env)

    # Queue for background processing
    WebhookProcessor.perform_async(event.to_json)

    status 200
    'ok'
  rescue StandardError => e
    status 400
    'Invalid signature'
  end
end

Build docs developers (and LLMs) love