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
(orwebhook-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:
- Secret – copied and pasted again, even rotated it once.
- Payload – tried hashing
- only
raw_body
timestamp + "." + raw_body
(Standard Webhooks spec)- the entire Bubble envelope
{ body: {...}, headers: {...} }
- Base‑64 vs hex digests.
- 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
- Does Bubble actually alter the body before I see it in
raw_body
? - Does OpenAI sign something different from
raw_body
when it sends the request to Bubble? - If anyone has a working verification flow in Bubble, could you share the exact steps or a code snippet?
- 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!