CookbookVerify in PHP

Verify webhooks in PHP

⚠️
Preview — this feature is being built; the design below is the spec.

Check the syntic-signature header before trusting a webhook delivery. The header carries a timestamp t and one or more v1 signatures (several during secret rotation). The signature is the hex HMAC-SHA256 of t + "." + raw body, keyed with your endpoint’s signing secret. Reject the delivery if no v1 matches in constant time, or if t is more than 300 seconds away from your clock. Always hash the raw body bytes, before any JSON parsing.

<?php
function syntic_verify(string $header, string $rawBody, string $secret, ?int $now = null): bool {
    $now ??= time();
    $t = null;
    $sigs = [];
    foreach (explode(',', $header) as $part) {
        $kv = explode('=', trim($part), 2);
        if (count($kv) !== 2) continue;
        if ($kv[0] === 't') $t = $kv[1];
        if ($kv[0] === 'v1') $sigs[] = $kv[1];
    }
    if ($t === null || !ctype_digit($t) || abs($now - (int) $t) > 300) return false;
 
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $sig) {
        if (hash_equals($expected, $sig)) return true;
    }
    return false;
}
 
$raw = file_get_contents('php://input');
if (!syntic_verify($_SERVER['HTTP_SYNTIC_SIGNATURE'] ?? '', $raw, getenv('SYNTIC_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
enqueue($_SERVER['HTTP_SYNTIC_EVENT_ID'] ?? '', $raw); // process later
http_response_code(200);

hash_equals takes the known value first; it compares in constant time.

Test it

Send a test event to your endpoint, or recompute a signature by hand as shown on Webhooks.

Was this page helpful?