API Reference
OIDC
Platform

Verifying webhooks

AdminUpdated Sep 11, 2026

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

atlas-id

The event id (evt_…) — also your dedupe key

atlas-timestamp

Delivery time, epoch milliseconds

atlas-signature

v1,<base64> — the HMAC over the signed payload

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.

Was this page helpful?
Verifying webhooks