# Signing & verification

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

Source: https://docs.watsi.ai/en/webhooks/signing

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

```http
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:

```javascript Node.js
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 })
})
```

```python Python
import hashlib
import hmac
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_signature(req):
    signature = req.headers.get('X-Watsi-Signature')
    if not signature:
        return False

    raw_body = req.get_data()
    computed = hmac.new(
        os.environ['WATSI_WEBHOOK_SECRET'].encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, computed)

@app.post('/webhooks/watsi')
def handle_webhook():
    if not verify_signature(request):
        return jsonify({'error': 'Invalid signature'}), 401

    event = request.get_json(force=True)
    print('Verified event', event['type'])
    return jsonify({'received': True}), 200
```

```ruby Ruby
require 'openssl'
require 'rack/utils'

post '/webhooks/watsi' do
  raw_body = request.body.read
  signature = request.env['HTTP_X_WATSI_SIGNATURE']
  secret = ENV.fetch('WATSI_WEBHOOK_SECRET')
  computed = OpenSSL::HMAC.hexdigest('sha256', secret, raw_body)

  halt 401, 'Invalid signature' unless signature &&
    signature.bytesize == computed.bytesize &&
    Rack::Utils.secure_compare(signature, computed)

  status 200
  body 'OK'
end
```

```go Go
func verifySignature(rawBody []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(os.Getenv("WATSI_WEBHOOK_SECRET")))
    mac.Write(rawBody)
    computed := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(computed))
}
```

```php PHP
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WATSI_SIGNATURE'] ?? '';
$computed = hash_hmac('sha256', $rawBody, getenv('WATSI_WEBHOOK_SECRET'));

if (!hash_equals($computed, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}
```

## Operational tips

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