Overview
Django's batteries-included approach makes it straightforward to handle ModelRiver event-driven AI webhooks. Use Django views for the webhook endpoint, Celery for background processing, and Django REST Framework for structured request handling.
What you'll build:
- A webhook endpoint that receives AI-generated data from ModelRiver
- HMAC signature verification decorator
- Celery tasks for async processing and callback
- Django ORM integration for data persistence
Quick start
Install dependencies
Bash
pip install django djangorestframework celery requests python-dotenvConfiguration
PYTHON
1# settings.py2import os3 4MODELRIVER_API_KEY = os.environ.get("MODELRIVER_API_KEY", "")5MODELRIVER_WEBHOOK_SECRET = os.environ.get("MODELRIVER_WEBHOOK_SECRET", "")Webhook handler
View
PYTHON
1# webhooks/views.py2import hmac3import hashlib4import json5import os6import requests7from django.http import JsonResponse8from django.views.decorators.csrf import csrf_exempt9from django.views.decorators.http import require_POST10from django.conf import settings11 12 13def verify_webhook_signature(payload_bytes, signature, secret):14 """Verify the mr-signature header using HMAC-SHA256."""15 expected = hmac.new(16 secret.encode("utf-8"),17 payload_bytes,18 hashlib.sha256,19 ).hexdigest()20 return hmac.compare_digest(signature, expected)21 22 23@csrf_exempt24@require_POST25def modelriver_webhook(request):26 signature = request.headers.get("mr-signature", "")27 raw_body = request.body28 29 # 1. Verify webhook signature30 if not verify_webhook_signature(raw_body, signature, settings.MODELRIVER_WEBHOOK_SECRET):31 return JsonResponse({"error": "Invalid signature"}, status=401)32 33 payload = json.loads(raw_body)34 event_type = payload.get("type")35 callback_url = payload.get("callback_url")36 37 # 2. Handle event-driven workflow38 if event_type == "task.ai_generated" and callback_url:39 from .tasks import process_ai_webhook40 41 # Queue background task with Celery42 process_ai_webhook.delay(43 event=payload.get("event"),44 ai_response=payload.get("ai_response"),45 callback_url=callback_url,46 customer_data=payload.get("customer_data", {}),47 )48 return JsonResponse({"received": True}, status=200)49 50 # Standard webhook51 return JsonResponse({"received": True}, status=200)URL configuration
PYTHON
1# webhooks/urls.py2from django.urls import path3from . import views4 5urlpatterns = [6 path("webhooks/modelriver/", views.modelriver_webhook, name="modelriver_webhook"),7]Celery background task
PYTHON
1# webhooks/tasks.py2import requests3from celery import shared_task4from django.conf import settings5from datetime import datetime6 7 8@shared_task(bind=True, max_retries=3, default_retry_delay=10)9def process_ai_webhook(self, event, ai_response, callback_url, customer_data):10 """Process AI response and call back to ModelRiver."""11 try:12 enriched_data = {**ai_response.get("data", {})}13 14 # 3. Your custom business logic based on the event15 if event == "content_ready":16 from myapp.models import Content17 18 content = Content.objects.create(19 title=enriched_data.get("title", ""),20 body=enriched_data.get("description", ""),21 category=customer_data.get("category", "general"),22 source="modelriver",23 )24 enriched_data["id"] = str(content.id)25 enriched_data["slug"] = content.slug26 enriched_data["saved_at"] = datetime.now().isoformat()27 28 if event == "entities_extracted":29 from myapp.models import Entity30 31 for entity in enriched_data.get("entities", []):32 Entity.objects.create(33 name=entity["name"],34 entity_type=entity["type"],35 source_id=customer_data.get("document_id"),36 )37 enriched_data["entities_saved"] = len(enriched_data.get("entities", []))38 39 # 4. Call back to ModelRiver40 response = requests.post(41 callback_url,42 json={43 "data": enriched_data,44 "task_id": f"django_{event}_{datetime.now().strftime('%Y%m%d%H%M%S')}",45 "metadata": {46 "processed_by": "django",47 "processed_at": datetime.now().isoformat(),48 },49 },50 headers={51 "Authorization": f"Bearer {settings.MODELRIVER_API_KEY}",52 "Content-Type": "application/json",53 },54 timeout=10,55 )56 response.raise_for_status()57 print(f"✅ Callback sent for event: {event}")58 59 except requests.exceptions.RequestException as exc:60 print(f"❌ Callback failed: {exc}")61 # Send error callback62 try:63 requests.post(64 callback_url,65 json={66 "error": "processing_failed",67 "message": str(exc),68 },69 headers={70 "Authorization": f"Bearer {settings.MODELRIVER_API_KEY}",71 "Content-Type": "application/json",72 },73 timeout=10,74 )75 except Exception:76 pass77 raise self.retry(exc=exc)Django REST Framework
For a more structured approach using DRF serializers:
PYTHON
1# webhooks/serializers.py2from rest_framework import serializers3 4 5class AIResponseSerializer(serializers.Serializer):6 data = serializers.DictField()7 8 9class WebhookPayloadSerializer(serializers.Serializer):10 type = serializers.CharField()11 event = serializers.CharField(required=False)12 channel_id = serializers.CharField()13 ai_response = AIResponseSerializer(required=False)14 callback_url = serializers.URLField(required=False)15 callback_required = serializers.BooleanField(required=False)16 customer_data = serializers.DictField(required=False, default={})17 timestamp = serializers.DateTimeField(required=False)18 19 20# webhooks/views.py21from rest_framework.views import APIView22from rest_framework.response import Response23from rest_framework import status24 25 26class ModelRiverWebhookView(APIView):27 authentication_classes = [] # Webhook uses signature verification instead28 permission_classes = []29 30 def post(self, request):31 signature = request.headers.get("mr-signature", "")32 raw_body = request.body33 34 if not verify_webhook_signature(raw_body, signature, settings.MODELRIVER_WEBHOOK_SECRET):35 return Response({"error": "Invalid signature"}, status=status.HTTP_401_UNAUTHORIZED)36 37 serializer = WebhookPayloadSerializer(data=request.data)38 serializer.is_valid(raise_exception=True)39 40 data = serializer.validated_data41 if data["type"] == "task.ai_generated" and data.get("callback_url"):42 process_ai_webhook.delay(43 event=data.get("event"),44 ai_response=data.get("ai_response"),45 callback_url=data["callback_url"],46 customer_data=data.get("customer_data", {}),47 )48 49 return Response({"received": True}, status=status.HTTP_200_OK)Triggering async requests
PYTHON
1# ai/client.py2import requests3from django.conf import settings4 5 6def trigger_async_ai(workflow: str, prompt: str, metadata: dict = None) -> dict:7 """Trigger an event-driven AI request."""8 response = requests.post(9 "https://api.modelriver.com/v1/ai/async",10 headers={11 "Authorization": f"Bearer {settings.MODELRIVER_API_KEY}",12 "Content-Type": "application/json",13 },14 json={15 "workflow": workflow,16 "messages": [{"role": "user", "content": prompt}],17 "metadata": metadata or {},18 },19 timeout=10,20 )21 response.raise_for_status()22 return response.json()Best practices
- Use Celery for processing: Never block the webhook response with heavy logic.
- Set task retries: Use
max_retriesanddefault_retry_delayon Celery tasks. - Verify signatures first: Always check
mr-signaturebefore processing. - Use Django ORM transactions: Wrap database writes in
transaction.atomic()for consistency. - Monitor with Request Logs: Track event-driven flows in Observability.
Next steps
- FastAPI event-driven guide: Async Python alternative
- Django integration: Standard ModelRiver + Django usage
- Webhooks reference: Retry policies and delivery monitoring