Guide
Verify OneLifeVibe webhooks in Cloudflare Workers, Deno or Bun
Verify OneLifeVibe webhook deliveries in Cloudflare Workers, Deno or Bun with WebCrypto HMAC-SHA256 over the raw body.
Updated November 6, 2026
- Stack
- WebCrypto (Cloudflare Workers, Deno, Bun)
The code
// Cloudflare Workers, Deno, Bun or any runtime with WebCrypto.
const enc = new TextEncoder();
export async function verifyOneLifeVibe(request, secret, toleranceSeconds = 300) {
const raw = await request.text();
const timestamp = Number(request.headers.get('webhook-timestamp'));
if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
throw new Error('stale or missing webhook-timestamp');
}
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc.encode(`${timestamp}.${raw}`)));
const expected = enc.encode('v1=' + [...mac].map((b) => b.toString(16).padStart(2, '0')).join(''));
const given = enc.encode(request.headers.get('webhook-signature') || '');
let diff = expected.length ^ given.length;
for (let i = 0; i < expected.length; i++) diff |= expected[i] ^ (given[i] ?? 0);
if (diff !== 0) throw new Error('bad webhook-signature');
return JSON.parse(raw);
}
The facts it relies on
- Each delivery is a POST with headers webhook-id (the event id; dedupe on it, delivery is at-least-once), webhook-timestamp (Unix seconds) and webhook-signature, plus x-olv-event and x-olv-world.
- webhook-signature is v1= followed by the lowercase hex HMAC-SHA256 of "{webhook-timestamp}.{raw body}", keyed with the endpoint secret as UTF-8 text. The secret is returned once, when the subscription is created.
- Verify against the raw body before parsing, compare in constant time, and reject a timestamp more than 300 seconds from your clock.
- Respond 2xx quickly; anything else is a failure. Retries follow after 30 s, 2 min, 10 min, 1 h and 6 h (6 attempts), then the delivery is dead. Redirects are never followed and requests time out after 10 s.
- Body: {"id":"evt_…","type":"world.event","world":"soclasica","subject":…,"origin":"ingest","created_at":"…","data":{…},"delivery":{"endpoint_id":"whe_…","attempts":1}}.
- Webhooks need a paid plan (Starter $19/month or $199/year); on the free Sandbox, poll GET /api/platform/v1/events.
Related guides
- Verify OneLifeVibe webhooks in Node.js
- Verify OneLifeVibe webhooks in Python
- Verify OneLifeVibe webhooks in PHP
- Verify OneLifeVibe webhooks in Ruby
What you are building
OneLifeVibe sends each webhook delivery as a POST carrying webhook-id, webhook-timestamp and webhook-signature, plus x-olv-event and x-olv-world so you can route without parsing the body. The function on this page does one job: it takes the incoming Request and your endpoint secret, proves the delivery came from OneLifeVibe, and hands back the parsed event.
It uses only crypto.subtle, TextEncoder and the standard Request interface, so the same code runs unchanged on Cloudflare Workers, Deno and Bun. The signature is v1= followed by the lowercase hex HMAC-SHA256 of the timestamp, a dot and the exact request body, keyed with the endpoint secret as UTF-8 text.
Before you start
You need an active webhook subscription and the endpoint secret it returned at creation. That secret is shown once, so capture it before you leave the screen and store it with your runtime's secret mechanism rather than in the repository. Webhooks require a paid plan: Starter is $19/month or $199/year. On the free Sandbox, poll GET /api/platform/v1/events while you build.
Plan for duplicates. Delivery is at-least-once, which is why every request carries webhook-id, the event id. Keep a store of ids you have processed and skip a repeat before doing any work.
How the code works
Order matters. The function calls request.text() first and holds the raw string, because anything you do after JSON.parse can change whitespace and break the digest. It then reads webhook-timestamp, converts it to a number, and rejects the request if the value is not finite or sits more than 300 seconds from your clock, which limits replay.
Next it imports the secret with crypto.subtle.importKey as raw HMAC key material for SHA-256, signs the string formed from the timestamp, a dot and the raw body, and hex-encodes the resulting bytes with a v1= prefix. The comparison is deliberately not a plain equality check: the loop starts with the XOR of the two lengths, then ORs in the XOR of each byte pair, treating missing bytes as zero. A length mismatch and a content mismatch both leave a nonzero value and fail with the same error. On success the function returns the parsed body.
The parsed event carries id, type, world, subject, origin, created_at and data, plus a delivery object with endpoint_id and attempts. x-olv-event and x-olv-world mirror the type and world fields for quick filtering.
Testing it
Write a small harness that builds a Request with a JSON body, computes the same signature with your secret, and passes it to the function. Then break it on purpose: flip one byte of the body after signing, change one hex character of webhook-signature, remove the timestamp header, and set the timestamp to an hour ago. Each case should throw, with bad webhook-signature for the first two and stale or missing webhook-timestamp for the last two.
A green run proves your happy path and your rejection path. It does not prove what the platform sends, so before going live, point a real subscription at a test endpoint and read the headers off an actual delivery.
Going to production
Return 2xx as soon as verification passes, before you do slow work; anything else counts as a failure. Failed deliveries retry after 30 s, 2 min, 10 min, 1 h and 6 h, six attempts in total, and then the delivery is dead. Redirects are never followed and requests time out after 10 s, so keep the handler inside that budget and move long jobs to a queue.
Retries can arrive while an earlier attempt is still being processed. Dedupe on webhook-id at the start of the handler, not after the side effect, and log the webhook-id with the outcome so a dead delivery can be traced later.
Questions
How do I verify a OneLifeVibe webhook signature with WebCrypto?
Import the endpoint secret with crypto.subtle.importKey using raw key material and HMAC with SHA-256, then sign the string {webhook-timestamp}.{raw body}. Hex-encode the result with a v1= prefix, compare it to webhook-signature in constant time, and reject timestamps more than 300 seconds from your clock.
What happens when my endpoint returns a non-2xx response?
OneLifeVibe treats it as a failure and retries after 30 s, 2 min, 10 min, 1 h and 6 h. After six attempts the delivery is dead, so keep the webhook-id from the headers and check your handler logs against it.
Why verify the raw body instead of the parsed JSON?
The signature covers the exact bytes OneLifeVibe sent. Parsing and re-serializing can reorder keys or change whitespace and produce a different digest, so read request.text() first, verify, and only then call JSON.parse.
Try it against the real API. A free Sandbox key reads the catalogue and the event log at once, no card.
Written from the OneLifeVibe tool registry and platform configuration, and checked automatically before publishing. Something wrong? Tell us.