Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.notealy.com/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook delivery includes an X-Notealy-Signature header so your receiver can confirm Notealy sent it and that the payload was not tampered with in transit.

Header format

X-Notealy-Signature: t=1714845660,v1=4f8a...c93b
  • t — unix timestamp (seconds) at signing time
  • v1 — hex-encoded HMAC-SHA256 of "{t}.{rawBody}", keyed by your subscription’s signing secret
The secret is shown once when you create or rotate the subscription. Store it as a server-side secret.

Verifying (Node.js)

import crypto from 'node:crypto'

function verifyNotealySignature(rawBody: string, header: string, secret: string) {
  // Parse `t=...,v1=...`
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = parts.t
  const v1 = parts.v1
  if (!t || !v1) return false

  // Reject signatures older than 5 minutes to prevent replay
  const ageSec = Math.floor(Date.now() / 1000) - Number(t)
  if (ageSec > 300) return false

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

  // Constant-time compare
  return crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'))
}
A few practical notes:
  • Use the raw body, not a re-serialized one. Express middlewares like express.json() consume the buffer; capture it with a verify callback or use body-parser raw mode for the webhook route.
  • Compare in constant time with timingSafeEqual (or your language’s equivalent) to avoid timing attacks.
  • Reject stale signatures — we recommend a 5-minute tolerance window. Replay attempts older than that should be rejected even if the HMAC matches.
  • Rotate periodically. Use Rotate signing secret in the dashboard; the new secret is shown once and old signatures stop validating immediately.