Skip to main content
Contiguity signs webhook payloads so you can verify that each request came from us and was not altered. Use the raw request body (before JSON parsing), the Contiguity-Signature header, and your webhook secret to verify each delivery.

Signing scheme

  • Signed payload: t + "." + raw_body (timestamp t + . + raw body string/bytes)
  • Algorithm: HMAC-SHA256
  • Signature encoding: hex
The header format is: t=<unix_timestamp>,v1=<hex_signature> (e.g. t=1707321600,v1=a1b2c3...). You verify by computing HMAC-SHA256 of the signed payload with your secret and comparing the result to v1 using a constant-time comparison.

Set up signing

  1. Open the Console.
  2. Go to TokensWebhook Signing.
  3. Generate a webhook secret. It will look like whsec_9974ab26f9cfa06e.... Store it securely (e.g. in env vars)
  4. Use this secret in your verification logic on the server.
You can rotate the secret from the same page. After rotating, use the new secret for verification; old secrets may still be valid for a short period depending on your configuration.

Verification samples

Use the exact raw body bytes/string as received — never re-serialize parsed JSON (e.g. do not use JSON.stringify(req.body)). Each framework below shows how to read the raw body and verify.
import crypto from "node:crypto"
import express from "express"

const app = express()

function verify_webhook_signature(raw_body, signature_header, secret) {
  if (!secret || !signature_header || raw_body == null) return false
  const match = signature_header.match(/t=(\d+),v1=([a-f0-9]+)/)
  if (!match) return false
  const [, t, v1] = match
  const signed_payload = `${t}.${raw_body}`
  const expected = crypto.createHmac("sha256", secret).update(signed_payload).digest("hex")
  return crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"))
}

// Use raw body for this route only — do not use express.json() here
app.post("/contiguity/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw_body = req.body.toString("utf-8")
  const secret = process.env.WEBHOOK_SECRET
  if (!verify_webhook_signature(raw_body, req.headers["contiguity-signature"], secret)) {
    return res.status(401).send("Invalid signature")
  }
  const event = JSON.parse(raw_body)
  // handle event...
  res.status(200).send("OK")
})

Replay protection (optional)

Reject requests that are too old by checking the timestamp t in the header. Require abs(now - t) to be within your tolerance (e.g. 300 seconds).
function verify_with_tolerance(raw_body, signature_header, secret, tolerance_seconds = 300) {
  if (!verify_webhook_signature(raw_body, signature_header, secret)) return false
  const m = signature_header.match(/t=(\d+)/)
  if (!m) return false
  const ts = parseInt(m[1], 10)
  const now = Math.floor(Date.now() / 1000)
  return Math.abs(now - ts) <= tolerance_seconds
}