webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
| En-tête |
Ce que c'est |
| webhook-id |
La livraison, la même valeur que delivery_id dans le corps. Gardez en mémoire celles que vous avez traitées, et répondez 200 à un doublon sans refaire le travail. |
| webhook-timestamp |
Le moment où nous l'avons signée, en secondes entières depuis 1970. Refusez tout ce qui s'écarte de plus de cinq minutes de votre propre horloge, pour qu'une ancienne livraison ne puisse pas vous être renvoyée. |
| webhook-signature |
v1, puis le HMAC-SHA256 en base64 de l'id, de l'horodatage et du corps brut reliés par des points, créé avec votre clé de signature. Il peut y en avoir plusieurs, séparés par des espaces. Acceptez la livraison si l'un d'eux correspond. |
La clé de signature
Elle commence par whsec_. Retirez cela et décodez le reste en base64 : ces octets sont la clé HMAC, pas le texte que vous avez copié. Elle n'est affichée qu'une fois, quand vous connectez le webhook ou appuyez sur "New signing key" sur la page de publication, et nous ne l'envoyons jamais.
Vérifiez le corps brut
Calculez la signature à partir des octets exactement tels qu'ils sont arrivés, avant tout traitement. Un JSON qui a été décodé puis réencodé n'a pas les mêmes octets, et sa signature ne correspondra pas.
PHP
<?php
$raw = getenv('SEONIS_SIGNING_KEY'); // whsec_...
// No key, no deliveries. A missing or empty variable would otherwise become
// an empty HMAC key, and anybody can sign with that.
$secret = is_string($raw) && str_starts_with($raw, 'whsec_')
? base64_decode(substr($raw, strlen('whsec_')), true)
: false;
if ($secret === false || strlen($secret) < 24) {
http_response_code(500);
exit('Signing key not configured.');
}
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$header = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('Too old.');
}
$expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $secret, true));
$valid = false;
foreach (explode(' ', $header) as $signature) {
[$version, $value] = array_pad(explode(',', $signature, 2), 2, '');
$valid = $valid || ($version === 'v1' && hash_equals($expected, $value));
}
if (! $valid) {
http_response_code(401);
exit('Bad signature.');
}
$delivery = json_decode($body, true);
Node.js
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// No key, no server. An empty variable would otherwise become an empty HMAC
// key, and anybody can sign with that.
const raw = process.env.SEONIS_SIGNING_KEY ?? '';
if (!raw.startsWith('whsec_')) throw new Error('SEONIS_SIGNING_KEY is not set.');
const key = Buffer.from(raw.slice('whsec_'.length), 'base64');
if (key.length < 24) throw new Error('SEONIS_SIGNING_KEY is not a valid key.');
// express.raw keeps the body as the bytes we signed.
app.post('/hooks/seonis', express.raw({ type: 'application/json' }), (req, res) => {
const id = req.get('webhook-id') ?? '';
const timestamp = req.get('webhook-timestamp') ?? '';
const header = req.get('webhook-signature') ?? '';
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send('Too old.');
}
const expected = crypto.createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(req.body)
.digest();
const valid = header.split(' ').some((signature) => {
const [version, value = ''] = signature.split(',');
const given = Buffer.from(value, 'base64');
return version === 'v1' && given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
if (!valid) return res.status(401).send('Bad signature.');
const delivery = JSON.parse(req.body);
res.json({ ok: true });
});
Ou utilisez une bibliothèque
Les bibliothèques officielles Standard Webhooks, pour PHP, JavaScript, Python, Go, Ruby et d'autres, font tout cela en un seul appel. Donnez-leur la clé de signature telle qu'elle vous a été affichée, whsec_ inclus.
github.com/standard-webhooks
Les anciens en-têtes
Les livraisons contiennent aussi le secret de signature comme jeton bearer, ainsi que X-AutoSEO-LT-Signature, un HMAC du seul corps créé avec ce secret. Ils sont toujours envoyés pour que les points de terminaison qui s'appuient sur eux continuent de fonctionner. Un nouveau point de terminaison doit plutôt vérifier webhook-signature : l'ancienne signature ne couvre ni l'heure ni l'id, et sa clé est envoyée dans chaque requête.
Les formats AutoSEO, Outrank et BabyLoveGrowth envoient exactement ce que ces services ont envoyé, ils ne contiennent donc pas ces en-têtes.