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)
API
Platform API v1 · Connectors

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

Related guides

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.

Get a free key Read the docs

Written from the OneLifeVibe tool registry and platform configuration, and checked automatically before publishing. Something wrong? Tell us.