Verify signatures
Check that every webhook request really came from Pulsar and wasn't altered on the way.
Your endpoint is a public URL, so anyone can send it a request. Every notification from Pulsar carries an X-Pulsar-Signature header, a hash of the request body computed with your signing secret. Only Pulsar and you know the secret, so a matching signature proves the request came from Pulsar and that the body wasn't changed.
The signature is the HMAC-SHA256 of the raw request body, keyed with your signing secret and written as lowercase hex.
Use the secret exactly as Pulsar gives it to you
The signing secret is a 32-character string. Use its text (UTF-8 bytes) as the HMAC key, as-is. Don't Base64-decode or hex-decode it. Decoding usually succeeds without an error but produces the wrong key, so every signature check fails.
How to verify a request
Read the raw body
Compute the signature over the exact bytes Pulsar sent, before any JSON parsing. Parsing and re-serializing the JSON can change spacing or key order, which changes the hash. Most frameworks give you the raw body separately; see the samples below.
Compute the expected signature
Compute HMAC-SHA256 of the raw body using your signing secret as the key, and hex-encode the result.
Compare it with the header
Compare your result with the X-Pulsar-Signature header using a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js). A plain == can leak, through response timing, how much of a guessed signature was right.
Accept or reject
If the signatures match, handle the event and return 2XX. If the header is missing or doesn't match, return 401 and ignore the request.
Sample code
Both samples read the secret from a PULSAR_WEBHOOK_SECRET environment variable.
import hashlib
import hmac
import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["PULSAR_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/webhooks/pulsar")
def pulsar_webhook():
signature = request.headers.get("X-Pulsar-Signature", "")
# The exact bytes Pulsar sent, before any JSON parsing.
body = request.get_data()
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
return "", 401
notification = request.get_json()
if notification["eventType"] == "Ping":
return "", 200
# Queue the work and respond straight away; see "Respond with a 2XX, quickly".
enqueue(notification)
return "", 202Replayed requests
The signature covers the body only, so anyone who gets hold of a copy of a request, for example from a log, could send it to you again unchanged and it would still pass the check. Protect against that the same way you handle duplicates: record each eventId you've processed and skip one you've already seen.
Don't reject requests just because eventTimestamp is old. Retries and replays deliberately resend the original event, with its original timestamp, possibly much later.