Overview
Signature verification proves that the request was created using the secret assigned to your PulseGrid endpoint and that its body was not altered in transit.
PulseGrid signs these exact bytes:
timestamp + '.' + raw request body
The signature appears in X-PulseGrid-Signature as v1=<hex digest>.
Important: verify the raw request body before parsing JSON. Re-serialising parsed JSON may change spaces or key ordering and cause a valid signature to fail.
Setup
1. Copy the endpoint signing secret once from PulseGrid.
2. Store it in a backend environment variable such as PULSEGRID_WEBHOOK_SECRET.
3. Read X-PulseGrid-Timestamp and X-PulseGrid-Signature.
4. Reject missing or invalid headers.
5. Reject timestamps older than five minutes to reduce replay risk.
6. Calculate HMAC-SHA256 over timestamp.raw_body.
7. Compare signatures with hmac.compare_digest.
8. Only then parse and process the JSON.
Code example
import hashlib
import hmac
import json
import os
import time
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def pulsegrid_webhook(request):
secret = os.environ["PULSEGRID_WEBHOOK_SECRET"]
timestamp = request.headers.get("X-PulseGrid-Timestamp", "")
supplied = request.headers.get("X-PulseGrid-Signature", "")
try:
timestamp_number = int(timestamp)
except ValueError:
return JsonResponse({"error": "Invalid timestamp"}, status=401)
if abs(int(time.time()) - timestamp_number) > 300:
return JsonResponse({"error": "Expired request"}, status=401)
signed = timestamp.encode() + b"." + request.body
expected = hmac.new(
secret.encode(), signed, hashlib.sha256
).hexdigest()
if not hmac.compare_digest("v1=" + expected, supplied):
return JsonResponse({"error": "Invalid signature"}, status=401)
event = json.loads(request.body)
# Apply duplicate protection before business processing.
handle_event(event)
return JsonResponse({"received": True}, status=200)
Notes
Do not log or return the signing secret. Rotate the secret immediately if it is exposed.
The receiver route normally needs CSRF exemption because it is called by PulseGrid, not by a browser carrying your Django CSRF cookie. Signature verification provides webhook authentication.