Skip to content
watsi

Webhooks

Signing & verification

Verify the X-Watsi-Signature header using the raw request body and your webhook secret.

Every webhook delivery includes an X-Watsi-Signature header. watsi computes the value by hashing the raw request body with HMAC-SHA256 and your subscription secret.

Header format

X-Watsi-Signature: 8a9f0d1c...

Treat the header value as a hex-encoded digest. To verify it, recompute the same HMAC on your side and compare the two digests using a constant-time comparison.

Verification algorithm

  1. Read the raw request body bytes exactly as received.
  2. Read X-Watsi-Signature from the request headers.
  3. Compute HMAC-SHA256(secret, rawBody).
  4. Hex-encode the computed digest.
  5. Compare the computed digest and header value in constant time.
  6. Reject the request with 401 if the values do not match.

Code examples

Examples in Node.js, Python, Ruby, Go, and PHP:

import crypto from 'crypto'
import express from 'express'

const app = express()
app.use(
  express.json({
    verify: (req, _res, buf) => {
      req.rawBody = buf
    },
  })
)

function verifySignature(req) {
  const signature = req.headers['x-watsi-signature']
  if (!signature || !Buffer.isBuffer(req.rawBody)) return false

  const computed = crypto
    .createHmac('sha256', process.env.WATSI_WEBHOOK_SECRET ?? '')
    .update(req.rawBody)
    .digest('hex')

  const expectedBuf = Buffer.from(String(signature), 'utf8')
  const computedBuf = Buffer.from(computed, 'utf8')
  if (expectedBuf.length !== computedBuf.length) return false
  return crypto.timingSafeEqual(expectedBuf, computedBuf)
}

app.post('/webhooks/watsi', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  console.log('Verified event', req.body.type)
  res.status(200).json({ received: true })
})

Operational tips

PracticeWhy it helps
Use the raw request bodyHash the exact bytes that arrived over HTTP before any JSON parsing or re-serialization.
Compare in constant timeUse timing-safe equality helpers to avoid leaking information through string comparison timing.
Store secrets outside codeKeep your signing secret in an environment variable or secret manager, never in the repository.
Log the delivery idPersist X-Watsi-Delivery-Id or event.id so retries can be correlated cleanly.