Из коробки Django отклоняет каждый POST-вебхук с кодом 403. Виновник — CSRF-middleware, который делает свою работу, защищая от межсайтовой подделки запросов, вот только провайдер не браузер и не несёт ни сессионной cookie, ни CSRF-токена. Решение — один декоратор и одна строчка понимания про доступ к телу запроса.
Пропустить CSRF для вебхук-вью
По-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 # сырые байты, нетронутые
signature = request.headers.get('Stripe-Signature', '')
# Проверка подписи (в реальном коде используйте SDK Stripe)
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)
Декоратор @csrf_exempt снимает проверку middleware только для этой вью — остальные формы остаются под защитой. request.body возвращает сырые байты как пришли — именно то, что нужно для проверки подписи.
Если вы используете Django REST Framework
DRF оборачивает запросы в собственный объект Request и запускает парсеры по content-type. К моменту, когда код вью читает request.data, JSON-парсер уже поглотил тело. Два выхода:
Вариант A: оставить как обычную Django-вью
Обработчикам вебхуков не нужны парсеры DRF, browsable API или механика аутентификации. Обычная @csrf_exempt-вью рядом с вашими DRF-эндпоинтами — самый чистый паттерн.
Вариант B: DRF с сырым доступом
Если вам действительно нужен DRF, читайте request.body (низлежащий запрос Django) вместо request.data:
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from rest_framework.permissions import AllowAny
@api_view(['POST'])
@authentication_classes([]) # без auth для вебхуков
@permission_classes([AllowAny]) # провайдер уже аутентифицируется подписью
def stripe_webhook(request):
payload = request._request.body # низлежащий запрос Django, сырые байты
signature = request.META.get('HTTP_STRIPE_SIGNATURE', '')
# ...проверить и обработать
Косвенность request._request неуклюжа, но это официальный обходной путь, когда нужно миновать парсинг DRF.
Настройте туннель
- Запустите dev-сервер Django:
python manage.py runserver 8000. - Добавьте хостнейм туннеля в
ALLOWED_HOSTSвsettings.py. Проще всего['*']на время локальной разработки или конкретный домен туннеля, если так удобнее. npx portpreview 8000в отдельном терминале.- Вставьте URL туннеля плюс путь вебхука в дашборд провайдера.
- Запустите тестовое событие и смотрите, как оно приходит в вашу вью.
Забытый ALLOWED_HOSTS даёт обобщённый Django-400 без очевидной причины. Если первая доставка вебхука возвращает 400, а логов вашей вью не видно — вот почему.
Частые ловушки
Двойное чтение тела
Как и любой фреймворк, Django отдаёт тело как поток, читаемый лишь однажды. Если middleware или логирующая обёртка уже трогали request.body, код вью увидит пустой объект bytes. Для проверки выведите len(request.body).
Часовые пояса и допуск по времени
Если сравниваете подписанную метку времени с datetime.now(), используйте timezone.now() или сравнивайте в UTC. Сравнения наивных datetime периодически ломаются при переходе на летнее время.
Async-вью
Django 4+ поддерживает вью async def. Обработчики вебхуков могут быть async, но учтите, что SDK проверки подписи (Stripe Python и др.) синхронны. Запускайте их через asgiref.sync_to_async или держите всю вью синхронной.
Заметки о деплое в продакшен
Когда локальное тестирование позади, та же вью работает в продакшене. Gunicorn, uWSGI, Daphne — никто из них не трогает request.body до того, как его увидит вью. Ваш reverse proxy — может (например, nginx со включённым proxy_buffering), но это про буферизацию, а не про изменение тела.
О базовой теории подписей см. руководство по проверке подписей. По специфике Stripe сторону дашборда покрывает руководство по локальному тестированию Stripe. Запишитесь в лист ожидания PortPreview ради туннелей, которые дружат с dev-серверами Python.