De fábrica, Django rechaza cada POST de webhook con un 403. El culpable es el middleware CSRF haciendo su trabajo — protegiendo contra cross-site request forgery — salvo que el proveedor no es un navegador y no lleva cookie de sesión ni token CSRF. La solución son un decorador y una línea de conciencia sobre el acceso al cuerpo de la petición.
Saltar CSRF en las vistas de webhook
La forma de Django:
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, HttpResponseBadRequest
import hmac, hashlib
@csrf_exempt
def stripe_webhook(request):
if request.method != 'POST':
return HttpResponseBadRequest('POST only')
payload = request.body # bytes crudos, intactos
signature = request.headers.get('Stripe-Signature', '')
# Verifica la firma (usa el SDK de stripe en código real)
if not verify(payload, signature, settings.STRIPE_WEBHOOK_SECRET):
return HttpResponse(status=401)
event = json.loads(payload)
handle_stripe_event(event)
return HttpResponse(status=200)
El decorador @csrf_exempt elimina la comprobación del middleware solo para esta vista — tus otros formularios siguen protegidos. request.body devuelve los bytes crudos tal como llegaron, que es justo lo que necesita la verificación de firma.
Si usas Django REST Framework
DRF envuelve las peticiones en su propio objeto Request y ejecuta parsers según el content-type. Para cuando tu vista lee request.data, el parser JSON ya consumió el cuerpo. Dos salidas:
Opción A: déjalo como vista de Django plana
Los handlers de webhook no necesitan los parsers de DRF, la API navegable ni la maquinaria de auth. Una vista @csrf_exempt plana junto a tus endpoints DRF es el patrón más limpio.
Opción B: usa DRF con acceso crudo
Si de verdad quieres DRF, lee request.body (la petición Django subyacente) en vez de request.data:
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny
@api_view(['POST'])
@authentication_classes([]) # sin auth para webhooks
@permission_classes([AllowAny]) # el proveedor ya autentica vía firma
def stripe_webhook(request):
payload = request._request.body # petición Django subyacente, bytes crudos
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
# ...verifica y maneja
La indirección request._request es incómoda, pero es la salida oficial cuando necesitas saltarte el parsing de DRF.
Configura el túnel
- Ejecuta tu servidor de desarrollo de Django:
python manage.py runserver 8000. - Añade el hostname del túnel a
ALLOWED_HOSTSensettings.py. Lo más fácil es['*']durante el dev local, o un dominio de túnel específico si lo prefieres. npx portpreview 8000en otra terminal.- Pega la URL del túnel más tu ruta de webhook en el dashboard del proveedor.
- Dispara un evento de prueba y míralo llegar a tu vista.
Olvidar ALLOWED_HOSTS produce un 400 genérico de Django sin causa obvia. Si tu primera entrega de webhook devuelve 400 y no ves los logs de tu vista, ese es el motivo.
Trampas comunes
Leer el cuerpo dos veces
Como todo framework, Django te da el cuerpo como un stream que solo se puede leer una vez. Si un middleware o un wrapper de logging ya tocó request.body, tu vista ve un objeto bytes vacío. Imprime len(request.body) como comprobación.
Zonas horarias y tolerancia de timestamp
Si comparas el timestamp firmado con datetime.now(), asegúrate de usar timezone.now() o comparar en UTC. Las comparaciones de datetime naive fallan de forma intermitente cuando hay cambios de horario de verano.
Vistas async
Django 4+ soporta vistas async def. Los handlers de webhook pueden ser async, pero ten en cuenta que los SDK de verificación de firma (Stripe Python, etc.) son síncronos. Córrelos con asgiref.sync_to_async o mantén toda la vista síncrona.
Notas de despliegue en producción
Una vez pasado el testing local, la misma vista funciona en producción. Gunicorn, uWSGI, Daphne — ninguno toca request.body antes de que tu vista lo vea. Tu reverse proxy podría (nginx con proxy_buffering activado, por ejemplo), pero eso es sobre búfer, no modificación del cuerpo.
Para la teoría de firmas subyacente, mira la guía de verificación de firmas. Para patrones específicos de Stripe, la guía de pruebas de Stripe en local cubre el lado del dashboard. Únete a la lista de espera de PortPreview para túneles que se llevan bien con servidores de desarrollo de Python.