Guide

Verify OneLifeVibe webhooks in Node.js

Verify OneLifeVibe webhooks in Node.js with node:crypto and Express: check the HMAC against the raw body, then parse the event.

Updated November 6, 2026

Stack
Node.js (node:crypto, Express)
API
Platform API v1 · Connectors

The code

import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody: the request body exactly as received (a string or Buffer), before JSON.parse.
export function verifyOneLifeVibe(rawBody, headers, secret, toleranceSeconds = 300) {
  const timestamp = Number(headers['webhook-timestamp']);
  if (!Number.isFinite(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) {
    throw new Error('stale or missing webhook-timestamp');
  }
  const expected = 'v1=' + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const given = String(headers['webhook-signature'] || '');
  const a = Buffer.from(expected);
  const b = Buffer.from(given);
  if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error('bad webhook-signature');
  return JSON.parse(rawBody);
}

// Express: keep the raw body for this route only.
// app.post('/onelifevibe', express.raw({ type: 'application/json' }), (req, res) => {
//   const event = verifyOneLifeVibe(req.body.toString('utf8'), req.headers, process.env.OLV_WEBHOOK_SECRET);
//   res.sendStatus(204); // answer first, then process event (dedupe on event.id)
// });

The facts it relies on

Related guides

What you are building

OneLifeVibe sends each webhook as a POST carrying five headers: webhook-id (the event id), webhook-timestamp in Unix seconds, webhook-signature, x-olv-event and x-olv-world. Your route accepts that POST, proves the signature was made with your endpoint secret, and only then parses the JSON.

The body holds id, type, world, subject, origin, created_at, data, and a delivery object with endpoint_id and attempts. Delivery is at-least-once, so the same event can arrive twice; dedupe on webhook-id before doing anything with it.

The code on the page is two pieces: verifyOneLifeVibe, a pure function taking the raw body, the headers and the secret, plus a commented Express handler that mounts it at POST /onelifevibe.

Before you start

You need the endpoint secret. It is returned once, when the subscription is created, so copy it into an environment variable such as OLV_WEBHOOK_SECRET before you close that response.

Webhooks need a paid plan: Starter at $19/month or $199/year. On the free Sandbox, poll GET /api/platform/v1/events instead.

Install Express. The signing uses only node:crypto — createHmac and timingSafeEqual — so verification adds no dependency.

How the code works

verifyOneLifeVibe starts with the timestamp. It converts webhook-timestamp with Number, rejects anything that is not finite, then compares it to Date.now() / 1000 and throws if the gap exceeds the tolerance, 300 seconds by default. That window decides how long a captured request stays replayable.

It then builds the expected signature: the literal v1= followed by the lowercase hex HMAC-SHA256 of the timestamp, a dot, and the raw body, keyed with the endpoint secret as UTF-8 text. Nothing else enters the digest — no header names, no URL. The comparison turns both strings into buffers, checks that the lengths match, and calls timingSafeEqual; the length check exists because timingSafeEqual throws on mismatched buffer sizes. Equal lengths plus a constant-time compare is what keeps a partial guess from being measurable.

The handler mounts express.raw({ type: 'application/json' }) on that one route, so req.body is a Buffer of the bytes as sent. It converts the buffer to a UTF-8 string, verifies with process.env.OLV_WEBHOOK_SECRET, replies 204, and processes the event afterward. Answering first keeps you inside the 10-second request timeout.

Testing it

Run the server locally and call verifyOneLifeVibe directly with a body and headers you build yourself. Check the three rejection paths: a webhook-timestamp more than 300 seconds from now, a signature computed over different bytes, and a missing header. Each should throw, and the route should answer non-2xx.

Then trigger a real event on your site and read the delivery object in the body. attempts should be 1 on the first try, and endpoint_id should match the subscription you created.

Know what a failure costs. OneLifeVibe retries after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours — six attempts — then the delivery is dead. Redirects are never followed, so the route URL must be final, and requests time out after 10 seconds.

Going to production

Store webhook-id, or the body's id, under a unique constraint and skip anything already handled. Duplicates are normal under at-least-once delivery, not a bug. Write the event down and hand the slow work to a queue.

Leave the 300-second tolerance where it is. A delivery that fails the timestamp check deserves a look, not a wider window. Log webhook-id, x-olv-event and x-olv-world with every request so a failed attempt can be traced back to its event and world.

Return 2xx for anything you have accepted, including duplicates. Keep the handler clear of downstream calls that can hang: 10 seconds is the whole budget, and an outage elsewhere should not burn your six attempts.

Questions

Why does my signature check fail when I use express.json()?

The signature covers the exact bytes that arrived, and express.json() replaces them with a parsed object. Re-serializing changes whitespace and key order, so the HMAC no longer matches. Use express.raw({ type: 'application/json' }) on the route and parse only after verification passes.

How many times does OneLifeVibe retry a failed webhook?

Six attempts in total, after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours. After the last one the delivery is dead.

Can I receive webhooks on the free Sandbox plan?

Webhooks need a paid plan: Starter at $19/month or $199/year. On the free Sandbox, poll GET /api/platform/v1/events for events instead of waiting for deliveries.

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.