SkyLight Chat
Webhooks Overview

Signature Verification

Verify the authenticity of webhook deliveries using HMAC-SHA256.

Why verify signatures?

Anyone who knows your webhook endpoint URL can send fake requests to it. Signature verification ensures that every webhook delivery genuinely comes from SkyLight Chat and has not been tampered with in transit.

How it works

  1. When you create a webhook, SkyLight Chat generates a unique secret (a 64-character random string)
  2. For every delivery, SkyLight Chat computes an HMAC-SHA256 digest of the raw JSON payload using your secret
  3. The digest is sent in the X-Skylight-Signature header as sha256=<hex_digest>
  4. Your server computes the same digest and compares — if they match, the request is authentic

Request headers

Every webhook delivery includes:

Content-Type: application/json
X-Skylight-Event: contact.created
X-Skylight-Delivery: a1b2c3d4-e5f6-7890-abcd-ef1234567890
X-Skylight-Signature: sha256=abc123def456...
X-Skylight-Timestamp: 1741093200
HeaderDescription
X-Skylight-EventThe event name
X-Skylight-DeliveryUUID for this delivery attempt
X-Skylight-Signaturesha256=<hmac_hex> of the raw request body
X-Skylight-TimestampUnix timestamp of the delivery

Verification examples

import crypto from 'crypto'

function verifySignature(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)  // rawBody must be the raw bytes, NOT parsed JSON
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}

// Express example
app.post('/webhooks/skylightchat', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-skylight-signature']
  const secret = process.env.SKYLIGHTCHAT_WEBHOOK_SECRET

  if (!verifySignature(req.body, signature, secret)) {
    return res.status(401).send('Unauthorized')
  }

  const payload = JSON.parse(req.body)
  // process payload...
  res.status(200).send('OK')
})

Important: use raw bytes

Always compute the HMAC over the raw request body bytes, not over a re-serialized JSON object. JSON serialization may change whitespace or key ordering, causing signature mismatches.

In Express.js, use express.raw() instead of express.json() for the webhook route. In other frameworks, read the raw body before parsing.

Replay protection

To guard against replay attacks, validate the X-Skylight-Timestamp header:

const timestamp = parseInt(req.headers['x-skylight-timestamp'], 10)
const now = Math.floor(Date.now() / 1000)
const fiveMinutes = 5 * 60

if (Math.abs(now - timestamp) > fiveMinutes) {
  return res.status(400).send('Stale event — possible replay attack')
}

Reject any delivery where the timestamp is more than 5 minutes in the past.

Rotating your secret

If your secret is ever compromised, regenerate it immediately:

  • Dashboard: Go to webhook settings → Regenerate Secret
  • API: POST /api/v1/webhooks/{id}/regenerate-secret

Update your environment variable and deploy before the old secret stops working.