Guide
Verify OneLifeVibe webhooks in Python
Verify OneLifeVibe webhook deliveries in Python with hmac and hashlib: recompute the signature over the timestamp and raw body before you parse.
Updated November 6, 2026
- Stack
- Python (hmac, hashlib, Flask)
The code
import hashlib
import hmac
import json
import time
def verify_onelifevibe(raw_body: bytes, headers, secret: str, tolerance: int = 300) -> dict:
timestamp = int(headers.get("webhook-timestamp", "0"))
if abs(time.time() - timestamp) > tolerance:
raise ValueError("stale or missing webhook-timestamp")
signed = f"{timestamp}.".encode() + raw_body
expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, headers.get("webhook-signature", "")):
raise ValueError("bad webhook-signature")
return json.loads(raw_body)
# Flask: request.get_data() is the raw body.
# @app.post("/onelifevibe")
# def onelifevibe():
# event = verify_onelifevibe(request.get_data(), request.headers, os.environ["OLV_WEBHOOK_SECRET"])
# return "", 204
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 PHP
- Verify OneLifeVibe webhooks in Ruby
- Verify OneLifeVibe webhooks in Cloudflare Workers, Deno or Bun
What you are building
You are adding a Flask route that accepts OneLifeVibe deliveries, checks their signature, and hands back the parsed event. Each delivery is a POST carrying webhook-id (the event id), webhook-timestamp in Unix seconds, webhook-signature, plus x-olv-event and x-olv-world describing what happened and where. The helper on the page turns those headers and the raw request body into a Python dict, or raises ValueError when the request should not be trusted.
Verification tells you the delivery came from OneLifeVibe and is fresh. It does not tell you it is new: delivery is at-least-once, so the same webhook-id can arrive twice. Dedupe on webhook-id in your own storage before you act on an event.
Before you start
Webhooks need a paid plan: Starter at $19/month or $199/year. On the free Sandbox there are no deliveries, so poll GET /api/platform/v1/events while you build there.
You need the endpoint secret, which is returned once when the subscription is created and not shown again. Store it in an environment variable such as OLV_WEBHOOK_SECRET and read it at startup. The helper relies on the standard library (hmac, hashlib, json, time) plus Flask for the route, so Flask is the only thing to install.
Keep the host clock accurate. The freshness check compares webhook-timestamp against the time your server thinks it is.
How the code works
The function checks the timestamp before it looks at the signature. It reads webhook-timestamp with a default of 0, converts it to an integer, and rejects the request when it sits more than 300 seconds away from the current time. A missing header becomes 0 and fails the same test, so absence and staleness share one branch.
It then rebuilds exactly what was signed: the timestamp as text, a literal dot, then the raw body bytes. That byte string is hashed with HMAC-SHA256 using the endpoint secret encoded as UTF-8, and the lowercase hex digest is prefixed with v1= to form the expected header value. hmac.compare_digest compares it with webhook-signature in constant time, so a mismatch raises ValueError.
Only after that does the helper call json.loads on the raw body. The order matters in the Flask route too: request.get_data() returns the untouched bytes, while request.json parses first and hands you a re-serialized object whose whitespace and key order no longer match the signature.
Testing it
Point a small script at your local route, sign a sample body the same way, and confirm you get 204 back. Then break it on purpose: change one character of the signature, send a timestamp several minutes old, drop the timestamp header entirely. Each case should raise ValueError. The route in the example does not catch it, so Flask returns 500; catch it and return 400 instead so bad requests do not look like server errors.
Watch your timing while you test. OneLifeVibe times out requests after 10 seconds and never follows redirects, so a handler that hangs is a failed delivery even when the signature was correct.
Going to production
Answer fast and do the real work afterward. Any response outside 2xx counts as a failure, and retries follow after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, six attempts in all, after which the delivery is dead. The delivery object in the body reports endpoint_id and attempts, useful for logging how deep into a retry chain an event is.
Verify before you parse, log or enqueue anything, and dedupe on webhook-id before any side effect. Treat the endpoint secret like any other credential, because the value is not retrievable after the subscription is created.
Questions
Why does my OneLifeVibe webhook signature check fail when the secret is correct?
Usually because the body was already parsed. Sign the raw bytes from request.get_data(), not a re-serialized dict, and sign with the timestamp from webhook-timestamp rather than the current time.
What happens if my endpoint returns a 500?
It counts as a failed delivery. OneLifeVibe retries after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours, six attempts total, then the delivery is dead.
Can I test webhooks on the free Sandbox?
Webhooks need a paid plan, Starter at $19/month or $199/year, so there are no deliveries on Sandbox. Poll GET /api/platform/v1/events there instead.
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.