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

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

Related guides

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.

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.