Browse documentation

WEBHOOKS

Signature verification

Every delivery is signed so you can prove it came from HL Hunt and was not tampered with.

t = unix secondssend timestampraw request bodyexact bytes on the wireHMAC-SHA256(secret, t.body)whsec_… signing secretHLHunt-Signaturet=…,v1=…[,v1=…]you recompute with your secret + timing-safe compare · reject if older than 5 mintwo v1 values appear for 24h after a secret rotation — accept if any matches

Headers

Content-Type: application/json
HLHunt-Event-Id: evt_<id>
HLHunt-Signature: t=<unix-seconds>,v1=<hex-hmac>[,v1=<hex-hmac>]

The signature is HMAC-SHA256(secret, "<t>.<raw body>"), hex-encoded. Verify against the raw body exactly as received — never re-serialize the JSON first. Reject timestamps outside a 5-minute replay window.

During a secret rotation the header carries two v1values — the current secret's first, the previous secret's second, for 24 hours. Accept the delivery if any v1 verifies with a secret you hold.

Verification example (Node.js)

const crypto = require('node:crypto')

function verify(rawBody, header, secret, maxSkewSeconds = 300) {
  const parts = header.split(',')
  const t = Number(parts.find((p) => p.startsWith('t=')).slice(2))
  if (Math.abs(Date.now() / 1000 - t) > maxSkewSeconds) return false
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')
  return parts
    .filter((p) => p.startsWith('v1='))
    .some((p) => {
      const candidate = Buffer.from(p.slice(3), 'hex')
      const want = Buffer.from(expected, 'hex')
      return candidate.length === want.length && crypto.timingSafeEqual(candidate, want)
    })
}