Overview
Every webhook delivery from Configly is signed with HMAC-SHA256 using the subscription secret. Verifying the signature ensures the payload genuinely originated from Configly and has not been tampered with in transit.
To manage webhook subscriptions, see Webhooks API. For the list of event types, see Webhook event types.
The signature header
Each delivery includes an X-Configly-Signature header in this format:
X-Configly-Signature: sha256=a1b2c3d4e5f6...
The value is the literal string sha256= followed by the hex-encoded HMAC-SHA256 digest.
How signatures are computed
- The raw request body (the exact JSON bytes sent by Configly) is used as the message.
- The subscription secret (the
whsec_value returned when the subscription was created) is used as the key. - The HMAC is computed using the SHA-256 algorithm.
- The result is hex-encoded and prefixed with
sha256=.
Verification steps
- Read the raw request body as bytes (before any JSON parsing).
- Compute the HMAC-SHA256 of the raw body using your stored subscription secret.
- Prepend
sha256=to the hex digest. - Compare the computed value to the
X-Configly-Signatureheader using a constant-time comparison function to prevent timing attacks. - If they match, the payload is authentic. If they do not match, reject the request with a 401 or 403 status.
Code examples
Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-Configly-Signature');
const secret = process.env.CONFIGLY_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
console.log('Received:', event.type, event.id);
res.status(200).send('OK');
});
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = 'whsec_...'
@app.post('/webhook')
def webhook():
signature = request.headers.get('X-Configly-Signature', '')
expected = 'sha256=' + hmac.new(
SECRET.encode(),
request.get_data(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
print(f"Received: {event['type']} {event['id']}")
return 'OK', 200
Ruby (Sinatra)
require 'sinatra'
require 'openssl'
require 'json'
SECRET = 'whsec_...'
post '/webhook' do
request.body.rewind
body = request.body.read
signature = request.env['HTTP_X_CONFIGLY_SIGNATURE'] || ''
expected = 'sha256=' + OpenSSL::HMAC.hexdigest('SHA256', SECRET, body)
halt 401 unless Rack::Utils.secure_compare(signature, expected)
event = JSON.parse(body)
puts "Received: #{event['type']} #{event['id']}"
status 200
'OK'
end
Additional delivery headers
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Configly-Signature |
HMAC-SHA256 signature as described above. |
X-Configly-Event |
The event type (e.g. config.applied). |
X-Configly-Delivery-Id |
A unique identifier for this delivery attempt. |
Retry behaviour
If your endpoint does not return a 2xx status, Configly retries the delivery up to three times:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
Each attempt has a 10-second timeout. If your endpoint does not respond within 10 seconds, the attempt is treated as a failure.
Auto-disable
After 10 consecutive delivery failures across all events for a subscription, Configly automatically disables the subscription and sends an email notification. The subscription stops receiving deliveries until you re-enable it.
To re-enable a disabled subscription:
curl -X PUT https://api.configly.app/v1/webhooks/{id} \
-H "Authorization: Bearer cly_your_key" \
-H "Content-Type: application/json" \
-d '{ "active": true }'
Re-enabling resets the failure counter to zero.
Delivery guarantees
Configly provides at-least-once delivery. Stalled deliveries are retried automatically after infrastructure restarts. Because the same event may be delivered more than once, your consumer should handle duplicates idempotently.
id field in the event envelope to deduplicate. Each event has a unique identifier (prefixed with evt_) that remains the same across retry attempts.
Comments
0 comments
Please sign in to leave a comment.