webhook-id: 7c1e2f0a-3b5d-4e8f-9a61-2d4c8b0e5f13
webhook-timestamp: 1790490600
webhook-signature: v1,s0RzLJ1d7Cxd9nwe8pqER9DWy/stEIB+uT9MlGVLlM0=
| Header |
Ce este |
| webhook-id |
Livrarea, aceeași valoare ca delivery_id din corp. Țineți minte livrările pe care le-ați procesat și răspundeți la o repetare cu 200 fără să faceți din nou aceeași muncă. |
| webhook-timestamp |
Momentul când am semnat-o, în secunde întregi de la 1970. Refuzați orice diferă cu mai mult de cinci minute față de ceasul dumneavoastră, pentru ca o livrare veche să nu vă poată fi trimisă din nou. |
| webhook-signature |
v1, apoi HMAC-SHA256 în base64 al id-ului, al marcajului temporal și al corpului brut, unite prin puncte, făcut cu cheia dumneavoastră de semnare. Pot fi mai multe, separate prin spații. Acceptați livrarea dacă oricare dintre ele se potrivește. |
Cheia de semnare
Începe cu whsec_. Scoateți asta și decodificați restul din base64: acei bytes sunt cheia HMAC, nu textul pe care l-ați copiat. Este afișată o singură dată, când conectați webhookul sau apăsați pe "New signing key" în pagina Publishing, iar noi nu o trimitem niciodată.
Verificați corpul brut
Calculați semnătura din bytes exact așa cum au sosit, înainte ca ceva să îi parseze. Un JSON care a fost decodat și codificat din nou nu mai are aceiași bytes, iar semnătura lui nu se va potrivi.
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 });
});
Sau folosiți o bibliotecă
Bibliotecile oficiale Standard Webhooks, pentru PHP, JavaScript, Python, Go, Ruby și altele, fac toate acestea într-un singur apel. Dați-le cheia de semnare exact cum v-a fost afișată, inclusiv whsec_.
github.com/standard-webhooks
Headerele mai vechi
Livrările includ și secretul de semnare ca token bearer și X-AutoSEO-LT-Signature, un HMAC doar al corpului, făcut cu acel secret. Acestea sunt trimise în continuare pentru ca endpointurile construite pe ele să funcționeze în continuare. Un endpoint nou ar trebui să verifice în schimb webhook-signature: semnătura veche nu acoperă nici timpul, nici id-ul, iar cheia ei este trimisă în fiecare cerere.
Formatele AutoSEO, Outrank și BabyLoveGrowth trimit exact ce au trimis acele servicii, așa că nu includ aceste headere.