---
updatedAt: 2025-12-31T20:17:44.000Z
agentTools:
  projectIndex: https://developer.breezy.hr/llms.txt
---

# Webhook Security

<br />

<br />

Breezy webhooks include a cryptographic signature that allows you to verify that the webhook was sent by Breezy and has not been tampered with.

## Signature Verification

Every webhook request includes an `X-Hook-Signature` header containing an HMAC-SHA256 signature of the request body.

### How It Works

1. When you create a webhook endpoint, you receive a `secret` (format: `whsec_...`)
2. For each webhook delivery, Breezy computes an HMAC-SHA256 signature of the JSON body using your secret
3. The signature is sent in the `X-Hook-Signature` header
4. You verify the signature by computing it yourself and comparing

### Headers

| Header                     | Description                               |
| -------------------------- | ----------------------------------------- |
| `X-Hook-Signature`         | HMAC-SHA256 signature of the request body |
| `X-Breezy-Webhook-Version` | Webhook format version                    |
| `Content-Type`             | Always `application/json`                 |

***

## Verification Examples

### Node.js

```javascript
const crypto = require('crypto')

function verifyWebhookSignature(body, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret)
  hmac.update(JSON.stringify(body))
  const calculated = hmac.digest('hex')
  
  return crypto.timingSafeEqual(
    Buffer.from(calculated),
    Buffer.from(signature)
  )
}

// Express middleware example
app.post('/webhook', express.json(), (req, res) => {
  const signature = req.headers['x-hook-signature']
  const secret = process.env.WEBHOOK_SECRET
  
  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }
  
  // Process the webhook
  console.log('Received webhook:', req.body.type)
  res.status(200).json({ success: true })
})
```

### Python

```python
import hmac
import hashlib
import json

def verify_webhook_signature(body, signature, secret):
    computed = hmac.new(
        secret.encode('utf-8'),
        json.dumps(body, separators=(',', ':')).encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(computed, signature)

# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Hook-Signature')
    secret = os.environ.get('WEBHOOK_SECRET')
    
    if not verify_webhook_signature(request.json, signature, secret):
        return jsonify({'error': 'Invalid signature'}), 401
    
    # Process the webhook
    print(f"Received webhook: {request.json['type']}")
    return jsonify({'success': True})
```

### Ruby

```ruby
require 'openssl'
require 'json'

def verify_webhook_signature(body, signature, secret)
  computed = OpenSSL::HMAC.hexdigest(
    'sha256',
    secret,
    body.to_json
  )
  
  Rack::Utils.secure_compare(computed, signature)
end

# Sinatra example
post '/webhook' do
  request.body.rewind
  body = JSON.parse(request.body.read)
  signature = request.env['HTTP_X_HOOK_SIGNATURE']
  secret = ENV['WEBHOOK_SECRET']
  
  unless verify_webhook_signature(body, signature, secret)
    halt 401, { error: 'Invalid signature' }.to_json
  end
  
  # Process the webhook
  puts "Received webhook: #{body['type']}"
  { success: true }.to_json
end
```

### PHP

```php
<?php

function verifyWebhookSignature($body, $signature, $secret) {
    $computed = hash_hmac('sha256', json_encode($body), $secret);
    return hash_equals($computed, $signature);
}

// Example usage
$body = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_HOOK_SIGNATURE'] ?? '';
$secret = getenv('WEBHOOK_SECRET');

if (!verifyWebhookSignature($body, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Process the webhook
error_log("Received webhook: " . $body['type']);
echo json_encode(['success' => true]);
```

***

## URL Requirements

Webhook endpoints must meet these requirements:

| Requirement | Description                          |
| ----------- | ------------------------------------ |
| Protocol    | HTTPS only (HTTP not allowed)        |
| Valid SSL   | Must have a valid SSL certificate    |
| Public IP   | No localhost or private IP addresses |
| Reachable   | Must be publicly accessible          |

### Blocked URLs

The following URL patterns are blocked in production:

* `localhost`
* `127.0.0.1`
* `::1`
* Private IP ranges: `10.x.x.x`, `192.168.x.x`, `172.16-31.x.x`
* Link-local addresses: `169.254.x.x`

***

## Delivery Behavior

### Timeouts

Each webhook delivery has a 30 second timeout. If your endpoint does not respond within 30 seconds, the delivery is marked as failed and will be retried.

### Retries

Failed deliveries are retried with exponential backoff:

| Attempt | Delay            |
| ------- | ---------------- |
| 1       | 1 second         |
| 2       | 2 seconds        |
| 3       | 4 seconds        |
| 4       | 8 seconds        |
| 5       | 16 seconds       |
| 6       | 32 seconds       |
| 7-10    | 60 seconds (max) |

After 10 failed attempts, the delivery is abandoned.

### Success Response

Your endpoint should return a 2xx status code to indicate successful receipt. Any other status code is treated as a failure.

***

## Best Practices

### 1. Always Verify Signatures

Never process a webhook without verifying the signature. This protects against spoofed requests.

### 2. Use Timing-Safe Comparison

Use constant-time comparison functions (like `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) to prevent timing attacks.

### 3. Respond Quickly

Process webhooks asynchronously if needed. Return a 200 response immediately, then process the data in a background job.

### 4. Handle Duplicates

In rare cases, the same webhook may be delivered more than once. Use the `_id` field to deduplicate events.

### 5. Store the Secret Securely

* Never commit the secret to source control
* Use environment variables or a secrets manager
* Rotate secrets if they may have been compromised

### 6. Log Failed Verifications

Log signature verification failures for debugging, but don't expose details to the requester.

***

## Troubleshooting

### Signature Verification Failing

1. **Check the secret** - Ensure you're using the exact secret returned when creating the endpoint
2. **Check JSON serialization** - The signature is computed on the raw JSON body, not a re-serialized version
3. **Check for middleware** - Some frameworks modify the request body before your handler sees it
4. **Check encoding** - Both secret and body should be treated as UTF-8

### Not Receiving Webhooks

1. **Check endpoint is active** - Use the GET endpoint to verify status is "active" and enabled is true
2. **Check events** - Verify the endpoint is subscribed to the event type you're expecting
3. **Check URL** - Ensure the URL is reachable from the public internet
4. **Check SSL** - Ensure your SSL certificate is valid and not expired
5. **Check stats** - Look at `failed_deliveries` to see if deliveries are failing