Todos os artigos
Uma requisição webhook passando por uma barreira CSRF até um handler de view do Django num servidor local.
DjangoPythonwebhook debugginglocal testing

Webhooks no Django: passar do CSRF e chegar ao handler

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

  1. Rode o seu servidor de desenvolvimento do Django: python manage.py runserver 8000.
  2. Adicione o hostname do túnel a ALLOWED_HOSTS em settings.py. O mais fácil é ['*'] durante o dev local, ou um domínio de túnel específico se preferir.
  3. npx portpreview 8000 em outro terminal.
  4. Cole a URL do túnel mais o caminho do seu webhook no dashboard do provedor.
  5. 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. Comece PortPreview grátis para túneis que se dão bem com servidores de desenvolvimento Python.

Perguntas frequentes

Por que o Django retorna 403 em cada POST de webhook?
O middleware CSRF do Django rejeita POSTs sem um token CSRF válido. Os provedores de webhook não enviam um. Adicione @csrf_exempt à sua view de webhook (só essa view) para contornar a checagem e verifique a assinatura no lugar.
As views de webhook devem ser Django puro ou DRF?
Django puro é mais simples para webhooks. Os parsers do DRF consomem o corpo antes da sua view ver, o que quebra a verificação de assinatura. Se mantiver o DRF, leia request._request.body para obter os bytes crus.
Qual ajuste de ALLOWED_HOSTS é necessário para túneis no Django?
Adicione o seu domínio de túnel a ALLOWED_HOSTS em settings.py. Durante o desenvolvimento local, ['*'] funciona; para configurações mais rígidas liste o hostname específico do túnel. Sem isso, o Django retorna um 400 sem uma mensagem de log clara.