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:

  1. HMAC-SHA256(payload, your_public_key)
  2. Base64 encodes it
  3. Sends as x-prembly-signature header

You verify:

  1. Compute the same signature using your public key
  2. Compare with the received signature
  3. 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 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

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}")

Did this page help you?