A webhook endpoint is an open door. Anyone who learns its Uniform Resource Locator (URL) can send it a request, and the URL is not a secret: it travels through logs, dashboards, and third-party configuration screens. Hash-based Message Authentication Code (HMAC) signature verification is how you tell a real delivery from a forged one.
First principles
The endpoint sees only bytes and headers. It cannot see who sent them. So any proof of origin has to travel inside the request, and it has to be something only the real provider can produce.
A shared secret gives you that. The provider holds it, you hold it, and nobody else does. A hash of the body keyed with that secret can only be produced by someone who holds the secret, and it changes if a single byte of the body changes. One check proves two things: who sent the request, and that it arrived unaltered.
Three rules follow, and the rest of this post is about them:
- Hash the exact bytes that were sent, or a genuine request fails the check.
- Compare in a way that does not leak how much of a guess was right.
- Treat a valid signature as proof of the sender, not of a first delivery. The same body can arrive more than once, so the handler has to be idempotent.
Why a signature, not an IP allowlist
Providers change the Internet Protocol (IP) address ranges they send from, and IP addresses can be spoofed in ways that are hard to reason about. A shared secret and a signature prove the payload came from someone who holds the secret, and that the bytes were not changed along the way.
How HMAC verification works
The provider hashes the raw request body together with a secret only the two of you know, and sends the result in a header. You recompute the same hash and compare. If they match, the request is genuine.
Verify against the raw body
This is the detail that trips people up. You must hash the exact bytes that were sent, so read request.body, never the parsed request.data. Re-serializing parsed JavaScript Object Notation (JSON) reorders keys and changes whitespace, and the signature will never match again.
import base64
import hashlib
import hmac
from django.conf import settings
def verify_shopify_webhook(request):
received = request.headers.get("X-Shopify-Hmac-Sha256", "")
digest = hmac.new(
settings.SHOPIFY_WEBHOOK_SECRET.encode(),
request.body,
hashlib.sha256,
).digest()
expected = base64.b64encode(digest).decode()
return hmac.compare_digest(received, expected)
Always use a constant-time comparison
Compare with hmac.compare_digest, not ==. A plain equality check can return early on the first mismatched character. That timing difference leaks information an attacker can use to guess the signature byte by byte.
Wire it into the view
The endpoint carries no session and no token, so it turns off the framework’s authentication and permission classes and relies entirely on the signature.
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
class ShopifyOrderWebhook(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
if not verify_shopify_webhook(request):
return Response(status=status.HTTP_401_UNAUTHORIZED)
process_order_webhook.delay(request.data)
return Response(status=status.HTTP_200_OK)
Acknowledge fast, process later
The view verifies the signature, pushes the payload to a Celery task, and returns 200 immediately. Most providers retry a webhook if the response is slow, and a retry while the first request is still running means duplicate work. Fast acknowledgement avoids that.
Make the handler idempotent
Webhooks are delivered at least once, which means sometimes more than once. Key the work on the provider’s event id and skip anything you have already seen. A signature proves who sent the payload. Idempotency keeps a repeat delivery from charging a customer twice.