How to Secure and Verify OpenAI Webhooks with Hookdeck
Webhook security and verification are critical components of any integration, and OpenAI webhooks are no exception. An unverified webhook handler is an unauthenticated public endpoint that does work on receipt of an HTTP POST — exactly the kind of surface area attackers look for to trigger replay attacks, forge events, or fan out denial-of-service traffic.
OpenAI follows the Standard Webhooks specification, signing every delivery with HMAC-SHA256 and including a timestamp to prevent replays. You can verify deliveries manually, lean on the OpenAI SDK helpers, or offload the work entirely to Hookdeck. This article walks through each. For a comprehensive overview of OpenAI webhook capabilities, see our guide to OpenAI webhooks.
How to manually secure OpenAI webhooks
When you create a webhook endpoint in the OpenAI Dashboard, OpenAI generates a signing secret prefixed whsec_ and shows it to you once. From that point on, every event delivered to your endpoint includes three Standard Webhooks headers: webhook-id (a unique delivery identifier that doubles as the idempotency key), webhook-timestamp (Unix epoch seconds), and webhook-signature (an HMAC of the canonical signed payload).
The seven things you need to do on every request:
- Store the signing secret as an environment variable — OpenAI's SDKs use
OPENAI_WEBHOOK_SECRETby convention. Never check the secret into source control or hardcode it. - Read the raw, unparsed request body. If you parse JSON before you verify, the byte-for-byte input to the HMAC won't match what OpenAI signed and verification will always fail.
- Read the three Standard Webhooks headers:
webhook-id,webhook-timestamp, andwebhook-signature. - Build the canonical signed payload by concatenating:
webhook-id + "." + webhook-timestamp + "." + raw_body. - Compute an HMAC-SHA256 of that string using the base64-decoded portion of the signing secret (everything after
whsec_). Base64-encode the result. - Parse the
webhook-signatureheader. Its format isv1,<base64-signature>and may contain multiple space-separated signatures (Standard Webhooks supports overlapping secrets for rotation). Compare your computed signature against each one using a constant-time comparison — never a plain string comparison, which leaks information through timing. - Read the
webhook-timestampand reject anything more than five minutes old (the Standard Webhooks default tolerance). The freshness check is what protects against replay attacks: an attacker who captures a valid signed payload can't replay it later.
A minimal manual verification handler in Node.js looks like this:
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.OPENAI_WEBHOOK_SECRET;
// strip prefix and base64-decode the signing key
const KEY = Buffer.from(SECRET.replace(/^whsec_/, ''), 'base64');
app.post('/webhooks/openai',
express.text({ type: 'application/json' }),
(req, res) => {
const id = req.headers['webhook-id'];
const timestamp = req.headers['webhook-timestamp'];
const signatures = req.headers['webhook-signature'];
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageSeconds > 300) return res.sendStatus(400);
const signed = `${id}.${timestamp}.${req.body}`;
const expected = crypto
.createHmac('sha256', KEY)
.update(signed)
.digest('base64');
const match = signatures.split(' ').some(sig => {
const [, value] = sig.split(',');
return value && crypto.timingSafeEqual(
Buffer.from(value),
Buffer.from(expected)
);
});
if (!match) return res.sendStatus(401);
handleEvent(JSON.parse(req.body));
res.sendStatus(200);
}
);
The OpenAI SDKs collapse this down to a single helper call — client.webhooks.unwrap() — which is available in both the Python and Node SDKs:
import OpenAI from 'openai';
const client = new OpenAI({
webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,
});
app.post('/webhooks/openai',
express.text({ type: 'application/json' }),
async (req, res) => {
try {
const event = await client.webhooks.unwrap(req.body, req.headers);
handleEvent(event);
res.sendStatus(200);
} catch (err) {
res.sendStatus(400);
}
}
);
The helper verifies the signature, enforces the freshness window, and returns a parsed event — throwing InvalidWebhookSignatureError if anything fails.
But even with the helper, manual verification still leaves you with a list of operational responsibilities: rotating the signing secret without breaking deliveries, ensuring every fleet of handlers shares the same secret, deduplicating on webhook-id because OpenAI's at-least-once delivery means the same event arrives more than once, and surviving the 72-hour retry window when something goes wrong downstream. The work compounds the more endpoints, environments, and event types you handle.
How to secure and verify OpenAI webhooks with Hookdeck
Hookdeck Event Gateway centralizes signature verification at the edge, so your handler only ever sees pre-verified events. Because OpenAI uses the Standard Webhooks spec, Hookdeck's verification works out of the box. The setup is mostly point-and-click:
- Create a free Hookdeck Event Gateway account.
- In the Hookdeck dashboard, create a new Source and pick "OpenAI" — Hookdeck has the Standard Webhooks verification logic for the
webhook-id,webhook-timestamp, andwebhook-signatureheaders built in. - Paste your OpenAI signing secret into the Source's authentication settings. Hookdeck stores it encrypted.
- Create a Connection from the OpenAI Source to a Destination (your application's webhook URL, or the Hookdeck CLI for local development).
- Configure the URL Hookdeck gave you as the webhook endpoint in the OpenAI Dashboard under Settings > Project > Webhooks.
- Subscribe to the event types you care about.
- Send a test event from the OpenAI Dashboard. Hookdeck verifies the signature, rejects anything outside the freshness window, and only forwards verified payloads to your application.
- Optionally enable Hookdeck's own outbound signature verification so your application can verify that requests really came from Hookdeck and not a third party who guessed your endpoint.
Once that's wired up, the operational responsibilities collapse. Secret rotation is a dashboard change. Every handler in every environment receives the same pre-verified event — no shared-secret distribution, no per-service verification code, no risk of one team forgetting the constant-time comparison or the 5-minute timestamp tolerance.
Conclusion
Securing OpenAI webhooks means more than checking a signature once. It means rotating secrets, handling replay protection, surviving retries without re-running side effects, and keeping verification consistent across every service that consumes events. Manual verification is possible (and the OpenAI SDK helpers make it easier than it used to be) but it's still infrastructure work that doesn't differentiate your product.
Get started with Hookdeck Event Gateway to verify, queue, retry, and replay your OpenAI webhooks without writing the boilerplate yourself. For more, see our guide to OpenAI webhook features and best practices and how to test and replay OpenAI webhooks locally.