Webhook signature verification
Ogni POST webhook è firmato con HMAC SHA-256 del body raw usando il secret che hai visto alla creazione del webhook (prefix whk_…).
Algoritmo:
sha256 = HMAC_SHA256(secret, raw_body). L'header X-Syrus-Signature-256 ha formato sha256=<hex_lowercase>.
Confronta SEMPRE con constant-time comparison (evita timing attacks).
Test vector
Usa questi valori per verificare la tua implementazione offline:
secret = "whk_test_secret_abc123"
body = b'{"event":"lead_qualified","test":true}'
expected = "sha256=a91b91ca1b39a9f7f09e8b0e4fcb5da36b6cf30c0c1c4af6ae7f5c6f4a28f65a"
Python (Flask handler)
import hashlib, hmac, time
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
SECRET = b"whk_...la-tua-chiave..."
@app.post("/syrus/webhook")
def syrus_webhook():
sig_header = request.headers.get("X-Syrus-Signature-256", "")
ts = request.headers.get("X-Syrus-Timestamp", "")
body = request.get_data() # raw bytes, NON decoded
# Replay protection
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(400, "timestamp too old")
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig_header, expected):
abort(401, "invalid signature")
event = request.headers.get("X-Syrus-Event")
payload = request.get_json()
# … elabora lead
return jsonify({"ok": True})
Node.js (Express middleware)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.SYRUS_WEBHOOK_SECRET;
app.post("/syrus/webhook",
express.raw({ type: "application/json" }), // MUST use raw body
(req, res) => {
const ts = req.header("X-Syrus-Timestamp") || "";
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send("timestamp too old");
}
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
const received = req.header("X-Syrus-Signature-256") || "";
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.status(401).send("invalid signature");
const payload = JSON.parse(req.body.toString("utf-8"));
// … elabora lead
res.json({ ok: true });
});
PHP (plain)
<?php
$secret = getenv('SYRUS_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_SYRUS_TIMESTAMP'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
http_response_code(400); exit('timestamp too old');
}
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$received = $_SERVER['HTTP_X_SYRUS_SIGNATURE_256'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401); exit('invalid signature');
}
$payload = json_decode($body, true);
// … elabora lead
echo json_encode(['ok' => true]);
Best practices
- Idempotenza: usa
X-Syrus-Delivery-Idper deduplicare (rispondi 200 anche se hai già processato l'evento). - Rispondi velocemente: target <5s di elaborazione sul tuo endpoint; enqueue lavori pesanti in background.
- Codici HTTP: 2xx = successo; 4xx (non 429) = errore permanente (noi smettiamo di riprovare); 5xx/429/timeout = retry con backoff 30s→2m→10m→1h→6h (5 tentativi totali).
- Accept POST only: gli altri metodi dovrebbero ritornare 405.