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.

Test your check with a ping first

Pulsar doesn't retry a 4XX response (see Retries). So if your secret is misconfigured, real events are rejected and stay undelivered until Pulsar replays them. Ask for a ping and confirm your endpoint returns 2XX before you rely on it.

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 "", 202

Replayed 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.

On this page