Guide
Verify OneLifeVibe webhooks in Ruby
Verify OneLifeVibe webhook signatures in Ruby with OpenSSL::HMAC and a constant-time compare, and parse the raw body only after it passes.
Updated November 6, 2026
- Stack
- Ruby (OpenSSL::HMAC, Rack)
The code
require "openssl"
require "json"
def verify_onelifevibe(raw_body, headers, secret, tolerance = 300)
timestamp = headers["webhook-timestamp"].to_i
raise "stale or missing webhook-timestamp" if (Time.now.to_i - timestamp).abs > tolerance
expected = "v1=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{raw_body}")
given = headers["webhook-signature"].to_s
raise "bad webhook-signature" unless expected.bytesize == given.bytesize && OpenSSL.fixed_length_secure_compare(expected, given)
JSON.parse(raw_body)
end
# Rack / Sinatra: request.body.read is the raw body; header names arrive as HTTP_WEBHOOK_SIGNATURE etc.
# event = verify_onelifevibe(request.body.read,
# { "webhook-timestamp" => request.env["HTTP_WEBHOOK_TIMESTAMP"], "webhook-signature" => request.env["HTTP_WEBHOOK_SIGNATURE"] },
# ENV.fetch("OLV_WEBHOOK_SECRET"))
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 Cloudflare Workers, Deno or Bun
What you are building
The recipe is one function, verify_onelifevibe, that takes the raw request body, the two signing headers, your endpoint secret and an optional tolerance. It checks the timestamp, recomputes the expected signature, compares the two in constant time, and returns the parsed event only when both checks pass. Anything else raises, so your Rack handler can answer with a non-2xx status instead of acting on an unverified payload.
It fits a Sinatra route, a Rails controller or plain Rack middleware, because it assumes nothing beyond the request body and the env hash. The comments at the bottom of the page show the wiring: request.body.read gives you the raw body, and header names arrive uppercased with an HTTP_ prefix.
Before you start
You need the endpoint secret from the moment the subscription is created, because the secret is returned once and never shown again. Store it outside your repository, for example in the environment variable OLV_WEBHOOK_SECRET that the snippet reads, and use a separate secret per environment.
Webhooks require a paid plan: Starter at $19 per month or $199 per year. On the free Sandbox you do not receive deliveries; poll GET /api/platform/v1/events instead.
In Rack the headers do not arrive under their literal names. Read webhook-id, webhook-timestamp and webhook-signature from HTTP_WEBHOOK_ID, HTTP_WEBHOOK_TIMESTAMP and HTTP_WEBHOOK_SIGNATURE in request.env.
How the code works
The first step reads webhook-timestamp, converts it with to_i and rejects anything more than 300 seconds away from Time.now.to_i. That is the default tolerance, and it is worth leaving alone: a wider window gives a captured request more time to be replayed.
The signed string is the timestamp, a literal dot, then the raw body exactly as it arrived, meaning the bytes you received before JSON parsing touches them. OpenSSL::HMAC.hexdigest with the SHA256 digest and your secret as UTF-8 text produces lowercase hex, and the code prefixes that with v1= to match the header format.
The comparison is guarded by a bytesize check and then OpenSSL.fixed_length_secure_compare, so a wrong signature fails without leaking timing through an early return. Only after that does JSON.parse run, which means a forged body never reaches your parser.
Testing it
Post a request with a signature you computed yourself, then re-post it with an old webhook-timestamp to confirm the timestamp branch fires. Change one character of the body and confirm the signature check fires. Drop the v1= prefix and confirm that fails too, since a bare hex digest will never match expected.
Inspect the parsed event for the shape you expect: id, type, world, subject, origin, created_at, data and delivery, where delivery carries endpoint_id and attempts. Assert on webhook-id, which is the event id, because you need it for deduplication later.
Your endpoint must return a 2xx quickly. Anything else counts as a failure and triggers retries after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, six attempts in total, after which the delivery is dead. Requests time out after 10 seconds and redirects are never followed, so return the status directly.
Going to production
Delivery is at-least-once, so persist webhook-id and skip any event you have already processed. Verify and dedupe inline, then hand the parsed event to a background queue and answer 2xx; slow work does not belong in the request.
Log webhook-id, the x-olv-event and x-olv-world headers and the failure reason for every rejected delivery, so you can tell a bad secret from a stale timestamp. Recreating a subscription gives you a new secret, and the old one stops matching.
Keep the raw body capture in place. Middleware that parses JSON before verification runs changes the bytes and produces a mismatch that looks like a secret problem but is not.
Questions
How do I verify a OneLifeVibe webhook signature in Ruby?
Read webhook-timestamp and compute the HMAC-SHA256 hex digest of the timestamp, a dot and the raw body, keyed with your endpoint secret as UTF-8 text. Prefix the digest with v1=, compare it to webhook-signature in constant time, and only then parse the body.
Why does verification fail with bad webhook-signature?
The usual causes are a secret from a different endpoint or environment, a body that was parsed or re-encoded before hashing, and a header missing the v1= prefix. Check the timestamp too, since the function rejects anything more than 300 seconds from your clock before it looks at the signature.
What happens if my endpoint does not return 2xx?
The delivery counts as a failure and OneLifeVibe retries after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, six attempts in all. After that the delivery is dead, so alert on your own error rate rather than letting retries absorb an outage.
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.