Authorization and Security
Quick Start
Every webhook sent by Prembly includes two critical security headers:
x-prembly-signature: HMAC-SHA256 signature (base64-encoded)token: Unique webhook identifier
[!IMPORTANT]
You MUST verify the signature before processing any webhook.
Why Verify Webhooks?
Without verification, anyone could send fake webhooks to your endpoint. Signature verification ensures:
- ✅ The webhook came from Prembly
- ✅ The payload hasn't been tampered with
- ✅ Protection against replay attacks (when combined with token tracking)
How It Works
Prembly generates signature:
HMAC-SHA256(payload, your_public_key)- Base64 encodes it
- Sends as
x-prembly-signatureheader
You verify:
- Compute the same signature using your public key
- Compare with the received signature
- If they match → webhook is authentic ✓
Implementation Examples
Python/Django
import hmac
import hashlib
import base64
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
def verify_prembly_signature(payload, signature_b64, public_key):
"""Verify webhook signature."""
expected_signature = hmac.new(
public_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')
return hmac.compare_digest(signature_b64, expected_signature_b64)
@csrf_exempt
def webhook_endpoint(request):
# Extract headers
signature = request.headers.get('x-prembly-signature')
token = request.headers.get('token')
if not signature or not token:
return JsonResponse({'error': 'Missing security headers'}, status=401)
# Get raw body
payload = request.body.decode('utf-8')
# Verify signature
public_key = "YOUR_PREMBLY_PUBLIC_KEY" # From your dashboard
if not verify_prembly_signature(payload, signature, public_key):
return JsonResponse({'error': 'Invalid signature'}, status=401)
# Parse and process webhook
data = json.loads(payload)
# Your business logic here
session_id = data.get('data', {}).get('widget_info', {}).get('session_id')
status = data.get('status')
verification_result = data.get('data', {}).get('verification_response', {})
# Return 200 to acknowledge receipt
return JsonResponse({'status': 'received'}, status=200)
Python/Flask
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
import json
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
# Extract headers
signature = request.headers.get('x-prembly-signature')
token = request.headers.get('token')
if not signature or not token:
return jsonify({'error': 'Missing security headers'}), 401
# Get raw body
payload = request.get_data(as_text=True)
# Verify signature
public_key = "YOUR_PREMBLY_PUBLIC_KEY"
expected_signature = hmac.new(
public_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature_b64 = base64.b64encode(expected_signature).decode('utf-8')
if not hmac.compare_digest(signature, expected_signature_b64):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
data = json.loads(payload)
# Your business logic
return jsonify({'status': 'received'}), 200
Node.js/Express
const express = require('express');
const crypto = require('crypto');
const app = express();
// Important: Get raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
function verifySignature(payload, signature, publicKey) {
const hmac = crypto.createHmac('sha256', publicKey);
hmac.update(payload);
const expectedSignature = hmac.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-prembly-signature'];
const token = req.headers['token'];
if (!signature || !token) {
return res.status(401).json({ error: 'Missing security headers' });
}
const publicKey = process.env.PREMBLY_PUBLIC_KEY;
if (!verifySignature(req.rawBody, signature, publicKey)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
const { data, status } = req.body;
// Your business logic
res.status(200).json({ status: 'received' });
});
PHP
<?php
function verifyPremblySignature($payload, $signature, $publicKey) {
$expectedSignature = base64_encode(
hash_hmac('sha256', $payload, $publicKey, true)
);
return hash_equals($signature, $expectedSignature);
}
// Get raw POST data
$payload = file_get_contents('php://input');
// Get headers
$signature = $_SERVER['HTTP_X_PREMBLY_SIGNATURE'] ?? '';
$token = $_SERVER['HTTP_TOKEN'] ?? '';
if (empty($signature) || empty($token)) {
http_response_code(401);
echo json_encode(['error' => 'Missing security headers']);
exit;
}
// Verify signature
$publicKey = 'YOUR_PREMBLY_PUBLIC_KEY';
if (!verifyPremblySignature($payload, $signature, $publicKey)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process webhook
$data = json_decode($payload, true);
// Your business logic
http_response_code(200);
echo json_encode(['status' => 'received']);
Best Practices
1. Always Use HTTPS
Never expose webhook endpoints over HTTP. Use HTTPS to encrypt data in transit.
2. Implement Idempotency
Store the token value to prevent duplicate processing:
# Check if token already processed
if WebhookLog.objects.filter(token=token).exists():
return JsonResponse({'status': 'already_processed'}, status=200)
# Process webhook
# ...
# Save token
WebhookLog.objects.create(token=token, processed_at=timezone.now())
3. Validate Webhook Structure
After verifying the signature, validate the payload structure:
required_fields = ['status', 'data']
if not all(field in data for field in required_fields):
return JsonResponse({'error': 'Invalid payload structure'}, status=400)
4. Handle Errors Gracefully
Return appropriate HTTP status codes:
200: Successfully processed401: Invalid signature400: Invalid payload500: Server error (Prembly may retry)
5. Log Everything
Log all webhook events for debugging:
logger.info(f"Webhook received - Token: {token}, Session: {session_id}")
6. Respond Quickly
Process webhooks asynchronously to avoid timeouts:
# Queue for background processing
webhook_task.delay(data)
# Respond immediately
return JsonResponse({'status': 'queued'}, status=200)
Troubleshooting
"Invalid signature" error
Cause: Signature mismatch
Solutions:
- Verify you're using the correct public key from your Prembly dashboard
- Ensure you're using the raw request body (not parsed JSON)
- Check character encoding (UTF-8)
- Verify no modifications to the request body before verification
Testing signature verification
Use this Python script to test:
import hmac
import hashlib
import base64
payload = '{"status":"completed","data":{"session_id":"123"}}'
public_key = "your_public_key"
signature = hmac.new(
public_key.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
signature_b64 = base64.b64encode(signature).decode('utf-8')
print(f"Expected signature: {signature_b64}")
Updated 15 days ago
Did this page help you?
