Secure Webhooks
Verify webhook signatures to ensure authenticity
Secure Webhooks
Verify webhook signatures to ensure requests genuinely come from Plexy.
Why verify signatures?
Without verification, attackers could send fake webhook events to your endpoint. Signature verification ensures:
- The request came from Plexy
- The payload hasn't been tampered with
- The request is recent (not a replay attack)
Webhook signing secret
Each webhook endpoint has a unique signing secret. Find it in your Dashboard:
- Go to Developers > Webhooks
- Click your endpoint
- Copy the Signing Secret (starts with
whsec_)
Keep your signing secret secure. Never expose it in client-side code or version control.
Signature header
Plexy includes a signature in the Plexy-Signature header:
Plexy-Signature: t=1679529600,v1=abc123...| Part | Description |
|---|---|
t | Unix timestamp when signature was generated |
v1 | HMAC-SHA256 signature |
Verify signatures
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const parts = signature.split(',');
const timestamp = parts[0].split('=')[1];
const receivedSig = parts[1].split('=')[1];
// Check timestamp is recent (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Webhook timestamp too old');
}
// Compute expected signature
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures
if (!crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
)) {
throw new Error('Invalid webhook signature');
}
return true;
}
app.post('/webhooks/plexy', express.json(), (req, res) => {
const signature = req.headers['plexy-signature'];
try {
verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(err.message);
}
// Process verified event
handleEvent(req.body);
res.status(200).send('OK');
});import hmac
import hashlib
import time
def verify_webhook_signature(payload, signature, secret):
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts['t']
received_sig = parts['v1']
# Check timestamp is recent (within 5 minutes)
current_time = int(time.time())
if current_time - int(timestamp) > 300:
raise ValueError('Webhook timestamp too old')
# Compute expected signature
signed_payload = f"{timestamp}.{payload}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures
if not hmac.compare_digest(received_sig, expected_sig):
raise ValueError('Invalid webhook signature')
return True
@app.route('/webhooks/plexy', methods=['POST'])
def handle_webhook():
signature = request.headers.get('Plexy-Signature')
try:
verify_webhook_signature(
request.data.decode(),
signature,
os.environ['WEBHOOK_SECRET']
)
except ValueError as e:
return str(e), 400
# Process verified event
handle_event(request.json)
return 'OK', 200func verifyWebhookSignature(payload []byte, signature, secret string) error {
parts := strings.Split(signature, ",")
timestamp := strings.Split(parts[0], "=")[1]
receivedSig := strings.Split(parts[1], "=")[1]
// Check timestamp is recent
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 300 {
return errors.New("webhook timestamp too old")
}
// Compute expected signature
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(payload))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expectedSig := hex.EncodeToString(mac.Sum(nil))
// Compare signatures
if !hmac.Equal([]byte(receivedSig), []byte(expectedSig)) {
return errors.New("invalid webhook signature")
}
return nil
}Using SDKs
Plexy SDKs provide built-in signature verification:
const plexy = require('@plexy/plexy-web');
app.post('/webhooks/plexy', express.raw({type: 'application/json'}), (req, res) => {
const event = plexy.webhooks.constructEvent(
req.body,
req.headers['plexy-signature'],
process.env.WEBHOOK_SECRET
);
// Event is verified
handleEvent(event);
res.status(200).send('OK');
});