OpenAI Webhook - securing

Hi everyone,

I hope someone can point me in the right direction because I have hit a wall with OpenAI webhook verification in Bubble.

What I am trying to do

  • Receive a webhook from the OpenAI Assistants API.
  • Verify the request with HMAC‑SHA256 so I am sure it really came from OpenAI.
  • Continue the backend workflow only when the signature is valid.

Current setup

  • Bubble backend workflow exposed as a public endpoint.
  • Endpoint set to Manual definition so I can capture the request body as raw text (raw_body).
  • X-Hook-Signature (or webhook-signature) header provided by OpenAI.
  • Webhook secret copied from the OpenAI dashboard (whsec_xxx...).
  • Server‑side action plugin written in JavaScript:

javascript

KopiërenBewerken

function(properties, context) {
  const crypto = require('crypto');
  const body      = properties.raw_body || "";
  const secret    = properties.secret   || "";
  const signature = context.headers["webhook-signature"] || "";

  // Bubble supposedly signs only the body, no timestamp
  const computed = crypto
    .createHmac('sha256', secret)
    .update(body, 'utf8')
    .digest('base64');

  return { verified: computed === signature.split(',')[1] };
}

The action returns a simple yes/no field verified.

The problem

No matter what I try verified is always false. Things I have tested:

  1. Secret – copied and pasted again, even rotated it once.
  2. Payload – tried hashing
  • only raw_body
  • timestamp + "." + raw_body (Standard Webhooks spec)
  • the entire Bubble envelope { body: {...}, headers: {...} }
  1. Base‑64 vs hex digests.
  2. Removing the whsec_ prefix just in case.

The header signature never matches the computed value.

Sample data

Secret (from OpenAI): whsec_XXXX

Header webhook-signature: v1,XXXX

Header webhook-timestamp: 1753789340

Raw body captured by Bubble (compact JSON):

json

KopiërenBewerken

{"id":"evt_6888b39ca814819099bdb15bbcfac96b","object":"event","created_at":1753789340,"type":"response.completed","data":{"id":"resp_abc123"}}

My questions

  1. Does Bubble actually alter the body before I see it in raw_body?
  2. Does OpenAI sign something different from raw_body when it sends the request to Bubble?
  3. If anyone has a working verification flow in Bubble, could you share the exact steps or a code snippet?
  4. Is a proxy (Cloudflare Worker, Lambda) the only reliable fix?

Any hint, checklist or example would be greatly appreciated. I am at the point where I suspect Bubble never gives me the byte‑perfect body that OpenAI used to compute the signature, but I hope I am missing something obvious.

NB.

It is highly unlikely that someone will actually post something in the API and I can stop any workflow if the resp_ id is not matching something in the database nevertheless, if something can be secured it should..

Thanks in advance!

Yes, there is at least one plugin that fixes this (but in the context of Stripe).

You can also use an intermediary (hookdeck etc.)

Thanks!
I’ll be testing some existing HMAC plugins as well. As soon as i’ve figured out what works i’ll try to rember to post something here as likely this is something many will bump into (at least if they are trying to secure the setup)

What is the API call and the event that triggers the webhook?

OpenAI has a background mode. Especially convenient when operating more complicated o3 requests in the backend (no sight on when they time-out) When you use this you can choose a variety of things. like polling the server, but openAI now also allows to connect a webhook (on a Project) so they will send a post that a response is ready (so you can pick up the response via the GET API call).

Funny thing now is that i got a work around, the “text” object (containg the JSON-SCHEMA) is actually a Text string with escaped JSON… complete nightmare to parse!

Anyone have a proper solution for that? RIght now the workflow even won’t save the raw text as it is so messed up :slight_smile:

@DRRR I’d suggest to use Hookdeck. You’ll probably just use the free plan for now. Bubble simply changes the response that is sent through the endpoint, so it’s nearly impossible to get it right, let alone making sure it stays working.

Developer

Always $0 /month

Features

  • Up to 10,000 events
  • 3-day retention
  • 1 user
  • SOC2

You don’t need webhooks for this, just poll using “schedule backend workflow” and a parameter that includes the response ID. It’s not “correct” but it doesn’t matter, works fine. You can use an exponential backoff built around the estimated time of the return call.

However, if you are dead set on using webhooks, you could include metadata with a large randomly generated key and then look for it on the return.

Thanks, I have been polling in the past as well (sort of when using the openai v2 assistant API to GET run) but I like the idea of a webhook a bit better. Also overtime it may be interesting to perform some batch analysis overnight. Nevertheless I think i’m working on a theoretic risk on the odd chance that someone/thing would a. find the api b. thinks about using it c. intercepts a resp key d. submits data that is actually mathching the key values in the subsequent json parsers..

Not too fond of adding additional tech dependencies into the setup. By the time I sort of figure out the security and setup it is outdated :slight_smile: