When a signing secret is configured for your outbound webhook (Alerts →
Outbound webhook, or via PUT /v1/integrations/webhook/{tenant_id}),
every delivery SkyQon sends is signed. Verifying the signature proves the
request came from SkyQon and that the payload was not tampered with or
replayed.
| Header | Meaning |
|---|---|
X-SkyQon-Signature | The signature, prefixed with its scheme version: v1=<hex hmac-sha256> |
X-SkyQon-Signature-Version | Currently always v1 |
X-SkyQon-Timestamp | Unix seconds when the delivery was signed |
X-SkyQon-Delivery | Opaque delivery id (use it for idempotency / dedup on retries) |
The signature is HMAC-SHA256 over the string
"{timestamp}.{raw_body}", keyed with your webhook secret and
hex-encoded. The timestamp is inside the MAC (Stripe-style), so a
captured body cannot be replayed later with a fresh timestamp.
|now − X-SkyQon-Timestamp| exceeds your tolerance window (we recommend 300 seconds).hex(hmac_sha256(secret, timestamp + "." + raw_body)) and prefix v1=.X-SkyQon-Signature using a constant-time comparison.import hashlib, hmac, time
TOLERANCE = 300 # seconds
def verify_skyqon_signature(secret: str, raw_body: bytes,
signature: str, timestamp: str) -> bool:
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(int(time.time()) - ts) > TOLERANCE:
return False
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(f"v1={mac}", signature)
# Flask example:
# ok = verify_skyqon_signature(
# SECRET, request.get_data(),
# request.headers.get("X-SkyQon-Signature", ""),
# request.headers.get("X-SkyQon-Timestamp", ""))
const crypto = require('crypto');
const TOLERANCE = 300; // seconds
function verifySkyqonSignature(secret, rawBody, signature, timestamp) {
const ts = parseInt(timestamp, 10);
if (!Number.isFinite(ts)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > TOLERANCE) return false;
const mac = crypto.createHmac('sha256', secret)
.update(`${ts}.`).update(rawBody).digest('hex');
const expected = Buffer.from(`v1=${mac}`);
const got = Buffer.from(String(signature || ''));
return expected.length === got.length &&
crypto.timingSafeEqual(expected, got);
}
// Express: use express.raw({ type: '*/*' }) on the webhook route so
// req.body is the raw Buffer, then:
// const ok = verifySkyqonSignature(SECRET, req.body,
// req.get('X-SkyQon-Signature'), req.get('X-SkyQon-Timestamp'));
PUT /v1/integrations/webhook/{tenant_id} with {"rotate_secret": true}.If no secret is configured, deliveries are sent unsigned (no
X-SkyQon-Signature header) — legacy behaviour. We recommend
configuring a secret for any receiver that acts on alert data.