webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
| Header |
Hvad det er |
| webhook-id |
Leveringen, den samme værdi som delivery_id i brødteksten. Husk dem, du har behandlet, og svar på en gentagelse med 200 uden at udføre arbejdet igen. |
| webhook-timestamp |
Da vi signerede den, i hele sekunder siden 1970. Afvis alt, der er mere end fem minutter fra dit eget ur, så en gammel levering ikke kan sendes til dig igen. |
| webhook-signature |
v1, derefter base64 HMAC-SHA256 af id'et, tidsstemplet og den rå brødtekst sat sammen med punktummer, lavet med din signeringsnøgle. Der kan være mere end én, adskilt af mellemrum. Accepter leveringen, hvis en af dem passer. |
Signeringsnøglen
Den begynder med whsec_. Fjern det, og base64-dekod resten: de bytes er HMAC-nøglen, ikke den tekst, du kopierede. Den vises én gang, når du forbinder webhooken eller trykker på "New signing key" på siden Publishing, og vi sender den aldrig.
Kontrollér den rå brødtekst
Beregn signaturen ud fra bytesene præcis, som de kom frem, før noget parser dem. JSON, der er blevet dekodet og kodet igen, er ikke de samme bytes, og signaturen vil ikke passe.
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 brug et bibliotek
De officielle Standard Webhooks-biblioteker til PHP, JavaScript, Python, Go, Ruby med flere klarer alt dette i ét kald. Giv dem signeringsnøglen, præcis som den blev vist til dig, inklusive whsec_.
github.com/standard-webhooks
De ældre headers
Leveringer indeholder også signeringshemmeligheden som bearer-token og X-AutoSEO-LT-Signature, en HMAC af kun brødteksten lavet med den hemmelighed. De sendes stadig, så endpoints, der bygger på dem, fortsat virker. Et nyt endpoint bør i stedet kontrollere webhook-signature: den gamle signatur dækker hverken tid eller id, og nøglen sendes med i hver anmodning.
Formaterne AutoSEO, Outrank og BabyLoveGrowth sender præcis det, de tjenester sendte, så de har ikke disse headers.