Dashboard · Webhooks

Signing & verifying

Every delivery is HMAC-SHA256 signed with your endpoint secret. Reject unsigned, stale, or mismatched bodies.

Always verify
Treat unsigned or invalid signatures as forged. Use the raw request body bytes — not a re-serialized JSON object — when computing the MAC.

Secret format

Macro generates secrets as whsec_ plus 48 hex characters (24 random bytes). You may supply your own secret (16–256 characters) at create time. Reveal or rotate from the dashboard edit flow or Management API credential routes.

Signature algorithm

  1. Read X-Macro-Timestamp (Unix seconds) and the raw body string.
  2. Compute HMAC-SHA256 over {timestamp}.{rawBody} using the endpoint secret.
  3. Compare to X-Macro-Signature, which looks like sha256=<hex digest>, with a timing-safe equality check.
  4. Reject if the absolute skew between now and the timestamp exceeds 300 seconds (5-minute replay window).
verify-webhook.js
import { createHmac, timingSafeEqual } from 'node:crypto'

const REPLAY_WINDOW_SEC = 300

export function verifyMacroWebhook({ secret, timestamp, rawBody, signature }) {
  const now = Math.floor(Date.now() / 1000)
  const ts = Number.parseInt(timestamp, 10)
  if (!Number.isFinite(ts) || Math.abs(now - ts) > REPLAY_WINDOW_SEC) return false

  const expected =
    'sha256=' +
    createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')

  const a = Buffer.from(expected)
  const b = Buffer.from(signature)
  if (a.length !== b.length) return false
  return timingSafeEqual(a, b)
}
verify.sh
# timestamp and raw body must match the request exactly
TIMESTAMP="1710000000"
BODY='{"id":"evt_…","object":"event"}'
SECRET="whsec_…"
printf '%s' "${TIMESTAMP}.${BODY}" \
  | openssl dgst -sha256 -hmac "$SECRET" \
  | awk '{print "sha256="$2}'

Idempotency

Prefer X-Macro-Event-Id (stable per logical event) for de-duplication. X-Macro-Delivery-Id uniquely identifies each attempt, including retries and manual resends.

Rotate the secret

  1. Rotate in Macro

    Dashboard edit → rotate, or POST /management/webhooks/:webhookId/rotate-secret (settings.webhook:write). Copy the new whsec_… value.

  2. Update your receiver

    Deploy the new secret. During cutover you can briefly accept either old or new until traffic uses the rotated credential.

GET/management/webhooks/:webhookId/credentialmt_

Reveal the current signing secret (settings.webhook:read).

POST/management/webhooks/:webhookId/rotate-secretmt_

Issue a new secret and return it once (settings.webhook:write).

Do

  • Verify signature + timestamp before parsing business logic.
  • Store secrets in a vault / env var — never in client-side code.
  • Log delivery ids when debugging failed verifications.

Don’t

  • Don’t trust X-Macro-Trigger alone without a valid signature.
  • Don’t re-stringify JSON before verifying — whitespace must match the raw body.
  • Don’t skip the replay window check in production.