Webhooks with Django — Complete Guide
In this tutorial, you will learn about Webhooks with Django. We cover key concepts, practical examples, and best practices to help you master this topic.
Django provides a robust framework for building webhook systems with its ORM, signals, and middleware architecture. This lesson covers creating webhook consumers and providers in Django, handling CSRF protection correctly, and leveraging Django's ecosystem for webhook management.
What You'll Learn
- Set up Django webhook endpoints with proper CSRF exemption
- Implement HMAC signature verification in Django views
- Use Django signals to trigger webhook events
- Build a webhook subscription model and admin interface
Why It Matters
Django powers many content platforms, SaaS applications, and APIs that need webhook functionality. Django's built-in admin, ORM, and signal system provide excellent building blocks for webhook infrastructure. Understanding Django-specific patterns helps you avoid common pitfalls like CSRF blocking or database performance issues.
Real-World Use
- Django-based e-commerce platforms use webhooks to notify external inventory systems
- Content management systems built with Django send webhooks on publish events
- Django REST Framework APIs integrate webhooks for third-party integrations
- SaaS platforms use Django signals to trigger webhook deliveries asynchronously
Mermaid Flow
graph TD
A[External Provider] --> B[Django View]
B --> C[CSRF Exempt Decorator]
C --> D[Raw Body Access]
D --> E[Signature Verification]
E --> F{Valid Signature?}
F -->|No| G[Return 401]
F -->|Yes| H[Parse JSON]
H --> I[Process Event]
I --> J[Return 200]
K[Django Signal] --> L[Webhook Dispatcher]
L --> M[Lookup Subscriptions]
M --> N[Queue Delivery]
N --> O[HTTP POST to Consumer]
Teacher's Corner
Emphasize that CSRF protection must be disabled for webhook endpoints because webhook providers do not have Django CSRF tokens. The @csrf_exempt decorator is essential. Also stress that request.body must be accessed before any Parsing middleware that may consume it. Compare Django's signal-based approach to Express middleware chains.
Code Examples
Example 1: Webhook Consumer View with Signature Verification
import hashlib
import hmac
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
WEBHOOK_SECRET = "whsec_your_secret".encode()
@csrf_exempt
@require_POST
def webhook_consumer(request):
signature = request.headers.get("X-Signature-256", "")
payload = request.body
expected = hmac.new(WEBHOOK_SECRET, payload, hashlib.sha256).hexdigest()
expected_header = f"sha256={expected}"
if not hmac.compare_digest(signature, expected_header):
return JsonResponse({"error": "invalid signature"}, status=401)
try:
event = json.loads(payload)
except json.JSONDecodeError:
return JsonResponse({"error": "invalid JSON"}, status=400)
print(f"Processed: {event.get('type')} [{event.get('id')}]")
return JsonResponse({"status": "received"})
Expected Output: POST with valid signature returns 200 with {"status": "received"}. Invalid signature returns 401.
Example 2: Webhook Provider Using Django Signals
import hashlib
import hmac
import json
import requests
from django.dispatch import Signal
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from .models import WebhookSubscription, Order
webhook_event = Signal()
@receiver(post_save, sender=Order)
def order_webhook_handler(sender, instance, created, **kwargs):
event_type = "order.created" if created else "order.updated"
webhook_event.send(
sender=Order,
event_type=event_type,
data={
"id": str(instance.id),
"total": float(instance.total),
"status": instance.status
}
)
@receiver(webhook_event)
def dispatch_webhooks(sender, event_type, data, **kwargs):
subscriptions = WebhookSubscription.objects.filter(
events__contains=[event_type],
active=True
)
for sub in subscriptions:
payload = json.dumps({
"type": event_type,
"id": f"{event_type}-{data['id']}",
"data": data
})
signature = hmac.new(
sub.secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
try:
requests.post(
sub.url,
data=payload,
headers={
"Content-Type": "application/json",
"X-Signature-256": f"sha256={signature}"
},
timeout=10
)
except requests.RequestException as e:
print(f"Delivery failed to {sub.url}: {e}")
Expected Output: When an Order is saved, the signal fires and delivers webhook payloads to all matching subscriptions.
Example 3: Django Admin for Webhook Subscription Management
from django.contrib import admin
from .models import WebhookSubscription
class WebhookSubscriptionAdmin(admin.ModelAdmin):
list_display = ("url", "events_list", "active", "created_at")
list_filter = ("active", "created_at")
search_fields = ("url",)
fieldsets = (
(None, {
"fields": ("url", "secret", "active")
}),
("Events", {
"fields": ("events",),
"description": "Enter event types as a JSON list, e.g. [\"order.created\", \"order.updated\"]"
}),
)
def events_list(self, obj):
return ", ".join(obj.events) if obj.events else "All events"
events_list.short_description = "Subscribed Events"
admin.site.register(WebhookSubscription, WebhookSubscriptionAdmin)
Expected Output: A clean Django admin interface for managing webhook subscriptions with event type filtering and activation toggling.
Common Mistakes
- Forgetting to add
@csrf_exemptto webhook endpoints, causing 403 Forbidden responses - Accessing
request.bodyafterrequest.POSTorrequest.GEThas been accessed, losing raw content - Using
request.POSTfor JSON payloads instead ofrequest.bodywithjson.loads() - Sending webhooks synchronously in signal handlers, blocking the request-response cycle
- Not using
hmac.compare_digestfor signature comparison, leaving timing attack vulnerabilities - Storing webhook secrets in plaintext in the database without encryption
- Creating recursive signal loops when webhook processing modifies the same model
Practice Questions
- Why must webhook views be decorated with
@csrf_exempt? - How would you make webhook delivery asynchronous in Django?
- What is the risk of processing webhooks synchronously in a signal handler?
- How would you encrypt webhook secrets at rest in the database?
- Challenge: Build a Django app that supports webhook subscriptions via REST API, dispatches events asynchronously using Celery, stores delivery logs in a separate model, and provides an admin dashboard for monitoring delivery success rates.
Answer Key
1. Webhook providers do not have a Django CSRF token. Without `@csrf_exempt`, Django's CSRF middleware rejects all POST requests to the view. 2. Use Celery or Django Channels to queue webhook delivery tasks. The signal handler should create an async task instead of calling `requests.post` directly. 3. Synchronous delivery blocks the HTTP response until all webhooks are sent. A slow or failing consumer endpoint delays the entire request. Use async task queues instead. 4. Use Django's encryption utilities (`django.core.signing`) or a field-level encryption library like `django-encrypted-model-fields` to store secrets encrypted at rest. 5. Create a WebhookSubscription model with encrypted secret field, a DeliveryLog model, a ViewSet for subscription CRUD, a Celery task for delivery with retry logic, and a custom admin view showing delivery statistics with charts.FAQ
Mini Project
Build a Django webhook manager app. Create models for WebhookSubscription (url, secret, events, active flag), DeliveryLog (subscription, event_id, status_code, response_body, delivered_at). Implement: (1) a consumer view with signature verification, (2) a signal-based dispatcher using Celery, (3) an admin interface with subscription management and delivery log browsing, (4) a management command to retry failed deliveries, and (5) a REST API for subscription CRUD using DRF.
What's Next
Learn how to build webhook systems with Spring Boot, a popular Java framework for enterprise webhook infrastructure.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro