Verifying webhooks
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Verifying webhooks
Every delivery is signed so you can prove it came from Atlas and has not been tampered with or replayed. Verify before you trust the body.
Headers
Header | Meaning |
|---|---|
| The event id ( |
| Delivery time, epoch milliseconds |
|
|
The signed payload
The signature is HMAC-SHA256 over the string:
<atlas-id>.<atlas-timestamp>.<raw request body>keyed with your endpoint's signing secret (whsec_…), base64-encoded, and prefixed v1,.
The timestamp is inside the signed material, not merely a header — otherwise an attacker could replay a captured body with a fresh timestamp and the signature would still verify. Reject any delivery whose timestamp is more than 5 minutes from now (in either direction).
Verify it yourself
Use the raw request body — parse JSON only after the signature checks out. Compare with a constant-time equality.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_MS = 5 * 60_000;
function verifyAtlasWebhook(req, secret: string): boolean {
const id = req.headers['atlas-id'];
const ts = Number(req.headers['atlas-timestamp']);
const sig = String(req.headers['atlas-signature']); // "v1,<base64>"
const body = req.rawBody; // the exact bytes Atlas sent
const [version, provided] = sig.split(',');
if (version !== 'v1' || !provided) return false;
if (Math.abs(Date.now() - ts) > TOLERANCE_MS) return false; // replay window
const expected = createHmac('sha256', secret)
.update(`${id}.${ts}.${body}`)
.digest('base64');
const a = Buffer.from(provided);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}import hmac, hashlib, base64, time
def verify_atlas_webhook(headers, raw_body: bytes, secret: str) -> bool:
version, _, provided = headers["atlas-signature"].partition(",")
if version != "v1" or not provided:
return False
ts = int(headers["atlas-timestamp"])
if abs(time.time() * 1000 - ts) > 5 * 60_000:
return False
signed = f"{headers['atlas-id']}.{ts}.".encode() + raw_body
expected = base64.b64encode(hmac.new(secret.encode(), signed, hashlib.sha256).digest()).decode()
return hmac.compare_digest(provided, expected)Or let the SDK do it
The backend SDKs ship a verifier so the contract has exactly one definition:
import { verifyWebhook } from '@atlas/backend';
const result = verifyWebhook({
eventId: req.headers['atlas-id'],
timestampMs: Number(req.headers['atlas-timestamp']),
body: req.rawBody,
signature: req.headers['atlas-signature'],
secret: process.env.ATLAS_WEBHOOK_SECRET!,
});
if (!result.valid) return res.status(400).end(); // result.reason: 'malformed' | 'stale' | 'mismatch'Idempotency on your side
Deliveries can repeat (retries, manual redelivery). Dedupe on atlas-id so processing the same event twice is a no-op.