Authorization and Security

Every webhook sent by Prembly includes two critical security headers:

  • x-prembly-signature: HMAC-SHA256 (base64 encoded)
  • token: This is a unique webhook identifier

You Must verify the signature before processing any webhook.

Why Verify Webhooks?

without verification, anyone could send the 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

  1. Prembly generates signature:
  • HMAC-SHA256(payload, your_public_key)
  • Base64 encodes it
  • Sends as an x-prembly-signature header
  1. You Verify:
  • Complete 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

'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']); # 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 processed 401: Invalid signature 400: Invalid payload 500: 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 **"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: # 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}")