Guide

Verify OneLifeVibe webhooks in PHP

Verify OneLifeVibe webhooks in PHP by rebuilding the v1 HMAC-SHA256 signature from the timestamp and raw body, then comparing it in constant time.

Updated November 6, 2026

Stack
PHP (hash_hmac, hash_equals)
API
Platform API v1 · Connectors

The code

<?php
function verify_onelifevibe(string $rawBody, array $headers, string $secret, int $tolerance = 300): array {
    $timestamp = (int) ($headers['webhook-timestamp'] ?? 0);
    if (abs(time() - $timestamp) > $tolerance) {
        throw new RuntimeException('stale or missing webhook-timestamp');
    }
    $expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    if (!hash_equals($expected, (string) ($headers['webhook-signature'] ?? ''))) {
        throw new RuntimeException('bad webhook-signature');
    }
    return json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
}

// In a plain PHP endpoint:
// $raw = file_get_contents('php://input');
// $headers = array_change_key_case(getallheaders(), CASE_LOWER);
// $event = verify_onelifevibe($raw, $headers, getenv('OLV_WEBHOOK_SECRET'));
// http_response_code(204);

The facts it relies on

Related guides

What you are building

Each OneLifeVibe delivery is a POST carrying webhook-id, webhook-timestamp and webhook-signature headers, plus x-olv-event and x-olv-world. The body is an envelope with id, type, world, subject, origin, created_at, data and a delivery object holding endpoint_id and attempts. Your PHP endpoint takes that POST, verifies the signature against the raw body, and only then parses anything.

The function verify_onelifevibe does the checking: it rejects stale timestamps, rebuilds the expected signature, and returns the decoded event as an array when the signature matches. Around it sits three lines of plumbing that read the raw request body, lowercase the header keys, and answer with 204.

Delivery is at-least-once, so the same webhook-id can arrive more than once. Store the ids you have processed and skip repeats before running any side effects.

Before you start

You need the endpoint secret for the subscription. It is returned once, when the subscription is created, so capture it then and load it at runtime, for example from an environment variable such as OLV_WEBHOOK_SECRET rather than from a file that ships with your code. The secret is used as UTF-8 text, so no extra decoding step is needed.

Webhooks need a paid plan: Starter is $19/month or $199/year. On the free Sandbox, poll GET /api/platform/v1/events instead and keep the same processing code behind it.

Nothing else to install. hash_hmac, hash_equals and json_decode ship with standard PHP.

How the code works

The first check is time. The function casts webhook-timestamp to an integer and throws if it is missing or more than the tolerance away from time(), 300 seconds by default. A missing header casts to 0 and fails at once. Keeping the tolerance as a parameter lets you loosen it briefly while debugging a server clock, but 300 seconds is the value to ship.

The signature check rebuilds exactly what the platform signed: the timestamp as decimal digits, a dot, then the raw body. hash_hmac with sha256 and your secret produces lowercase hex, which the code prefixes with v1= so it matches the header format. hash_equals then compares the two strings in constant time, so a wrong signature does not leak through timing how much of it was correct.

Only after both checks pass does json_decode run, with JSON_THROW_ON_ERROR so a malformed body raises an exception instead of returning null. In the endpoint itself, file_get_contents on php://input gives the untouched body, array_change_key_case lowercases the header keys so lookups are predictable, and http_response_code(204) closes the request.

Testing it

Send a request with curl to your local endpoint first. Build the signature by hand: pick a fixed timestamp, concatenate it with a dot and your test body, run hash_hmac with sha256 and the secret, prefix v1=, and pass the result as webhook-signature. If the handler answers 204, the happy path works.

Then break it on purpose. Change one character of the body and confirm the endpoint rejects the delivery. Send a timestamp more than 300 seconds behind your clock and confirm the staleness check fires before the signature comparison. Use a different secret and confirm the comparison fails.

Test the duplicate path too. Post the same webhook-id twice and confirm your dedupe store stops the second one from repeating the work.

Going to production

Verify before you parse. Read the body once into a string and pass that same string to the verification and to json_decode; re-encoding a decoded array changes whitespace and key order, and the signature will not match.

Answer fast. Anything other than 2xx counts as a failure, and the request times out after 10 s, so a 204 with the heavy work pushed to a queue is safer than a long synchronous handler. Redirects are never followed, so point the subscription at the final URL.

Failures are retried after 30 s, 2 min, 10 min, 1 h and 6 h, six attempts in total, after which the delivery is dead. That is why dedupe on webhook-id matters: a retry can be an event you already handled. Log delivery.attempts, which starts at 1, to see how often your receiver is failing.

Questions

What should my PHP endpoint return to OneLifeVibe?

Any 2xx status; the code on the page sends 204. Anything else counts as a failure and the delivery is retried after 30 s, 2 min, 10 min, 1 h and 6 h, six attempts in total.

Why use hash_equals instead of comparing the signature with ==?

hash_equals compares in constant time, so the response time does not reveal how many leading characters of a guessed signature were correct. The expected value is v1= followed by lowercase hex, so an ordinary comparison is the wrong tool.

Can I test webhooks on the free Sandbox?

Webhooks require a paid plan: Starter is $19/month or $199/year. On the free Sandbox, poll GET /api/platform/v1/events and keep the same handler behind it.

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.