webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
| Header |
Vad det är |
| webhook-id |
Leveransen, samma värde som delivery_id i brödtexten. Kom ihåg dem du har hanterat, och svara på en upprepning med 200 utan att göra arbetet igen. |
| webhook-timestamp |
När vi signerade den, i hela sekunder sedan 1970. Avvisa allt som skiljer sig mer än fem minuter från din egen klocka, så att en gammal leverans inte kan skickas till dig igen. |
| webhook-signature |
v1, sedan base64 HMAC-SHA256 för id, tidsstämpeln och den råa brödtexten sammanfogade med punkter, skapad med din signeringsnyckel. Det kan finnas fler än en, åtskilda med mellanslag. Godkänn leveransen om någon av dem stämmer. |
Signeringsnyckeln
Den börjar med whsec_. Ta bort det och base64-avkoda resten: de byten är HMAC-nyckeln, inte texten du kopierade. Den visas en gång, när du ansluter webhooken eller trycker på "New signing key" på sidan Publishing, och vi skickar den aldrig.
Kontrollera den råa begärandetexten
Beräkna signaturen från byten exakt som de kom fram, innan något tolkar dem. JSON som har avkodats och kodats igen är inte samma byte, och dess signatur kommer inte att stämma.
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 });
});
Eller använd ett bibliotek
De officiella biblioteken för Standard Webhooks, för PHP, JavaScript, Python, Go, Ruby med flera, gör allt detta i ett anrop. Ge dem signeringsnyckeln precis som den visades för dig, inklusive whsec_.
github.com/standard-webhooks
De äldre headers
Leveranser innehåller också signeringshemligheten som en bearer-token, och X-AutoSEO-LT-Signature, en HMAC av bara brödtexten skapad med den hemligheten. De skickas fortfarande så att endpoints som bygger på dem fortsätter att fungera. En ny endpoint bör i stället kontrollera webhook-signature: den gamla signaturen omfattar varken tid eller id, och dess nyckel följer med i varje begäran.
Formaten AutoSEO, Outrank och BabyLoveGrowth skickar exakt det som de tjänsterna skickade, så de har inte de här headers.