De fábrica, o Django rejeita cada POST de webhook com um 403. O culpado é o middleware CSRF fazendo o seu trabalho — protegendo contra cross-site request forgery — só que o provedor não é um navegador e não carrega cookie de sessão nem token CSRF. A correção é um decorator e uma linha de consciência sobre o acesso ao corpo da requisição.
Pular o CSRF nas views de webhook
O jeito do 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 crus, intactos
signature = request.headers.get('Stripe-Signature', '')
# Verifica a assinatura (use o SDK do stripe no 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)
O decorator @csrf_exempt remove a checagem do middleware só para esta view — os seus outros formulários continuam protegidos. request.body devolve os bytes crus como chegaram, que é exatamente o que a verificação de assinatura precisa.
Se você usa Django REST Framework
O DRF embrulha as requisições no seu próprio objeto Request e roda parsers conforme o content-type. Quando o código da sua view lê request.data, o parser JSON já consumiu o corpo. Duas saídas:
Opção A: mantenha como view de Django pura
Handlers de webhook não precisam dos parsers do DRF, da API navegável nem da maquinaria de auth. Uma view @csrf_exempt pura ao lado dos seus endpoints DRF é o padrão mais limpo.
Opção B: use DRF com acesso cru
Se você realmente quer DRF, leia request.body (a requisição Django subjacente) em 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([]) # sem auth para webhooks
@permission_classes([AllowAny]) # o provedor já autentica via assinatura
def stripe_webhook(request):
payload = request._request.body # requisição Django subjacente, bytes crus
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
# ...verifica e trata
A indireção request._request é desajeitada, mas é a saída oficial quando você precisa contornar o parsing do DRF.
Configure o túnel
- Rode o seu servidor de desenvolvimento do Django:
python manage.py runserver 8000. - Adicione o hostname do túnel a
ALLOWED_HOSTSemsettings.py. O mais fácil é['*']durante o dev local, ou um domínio de túnel específico se preferir. npx portpreview 8000em outro terminal.- Cole a URL do túnel mais o caminho do seu webhook no dashboard do provedor.
- Dispare um evento de teste e veja-o chegar à sua view.
Esquecer o ALLOWED_HOSTS produz um 400 genérico do Django sem causa óbvia. Se a sua primeira entrega de webhook devolve 400 e você não vê os logs da sua view, é por isso.
Armadilhas comuns
Ler o corpo duas vezes
Como todo framework, o Django te dá o corpo como um stream que só pode ser lido uma vez. Se um middleware ou um wrapper de logging já tocou em request.body, o código da sua view vê um objeto bytes vazio. Imprima len(request.body) como sanity check.
Fusos horários e tolerância de timestamp
Se você compara o timestamp assinado com datetime.now(), garanta usar timezone.now() ou comparar em UTC. Comparações de datetime naive falham de forma intermitente quando há mudanças de horário de verão.
Views async
O Django 4+ suporta views async def. Handlers de webhook podem ser async, mas saiba que os SDKs de verificação de assinatura (Stripe Python, etc.) são síncronos. Rode-os com asgiref.sync_to_async ou mantenha a view inteira síncrona.
Notas de deploy em produção
Passado o teste local, a mesma view funciona em produção. Gunicorn, uWSGI, Daphne — nenhum deles toca em request.body antes da sua view ver. O seu reverse proxy pode (nginx com proxy_buffering ligado, por exemplo), mas isso é sobre buffer, não modificação do corpo.
Para a teoria de assinatura por trás, veja o guia de verificação de assinatura. Para padrões específicos do Stripe, o lado do dashboard é coberto pelo guia de testes do Stripe em local. Entre na lista de espera do PortPreview para túneis que se dão bem com servidores de desenvolvimento Python.