Todos os artigos
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. Entre na lista de espera do PortPreview 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.