Платёжная форма в Django: разбор интеграции ЮKassa
Как подключить приём платежей в Django-проекте через ЮKassa: создание платежа, редирект, обработка вебхука и защита от подделок. Разбираем рабочую схему без псевдокода.

Приём онлайн-платежей в Django почти никогда не сводится к «вставить кнопку». Реальная задача — провести пользователя через редирект на страницу оплаты, дождаться подтверждения от платёжного сервиса и не поверить клиенту на слово, что он заплатил. Разберём это на примере ЮKassa (бывшая Яндекс.Касса) и её официального SDK yookassa версии 3.x. Стек: Django 5.x, Python 3.11+. Угол этой статьи — именно интеграция платёжного потока: создание платежа, редирект и приём вебхука. Витрину, корзину и учёт остатков не трогаем.
Почему нельзя «просто принять деньги на фронте»
Очевидное решение новичка: показать форму, отправить сумму на сервер платёжной системы, получить «успех» и открыть доступ к товару. Проблема в том, что фронтенду верить нельзя. Ответ платёжной системы, пришедший в браузер пользователя, легко подделать или потерять — вкладку закрыли до редиректа, сеть отвалилась, скрипт заблокировал плагин. Единственный надёжный сигнал «деньги пришли» — это серверное уведомление (вебхук) от платёжного провайдера, подписанное или проверяемое по IP.
Поэтому платёжный поток всегда двусторонний: клиент уходит на оплату по одной ссылке, а подтверждение приходит на ваш сервер по другому каналу. Эти два события не синхронизированы — вебхук может прийти раньше, чем пользователь вернётся на сайт.
Статус заказа меняет вебхук, а не редирект пользователя. Страница «спасибо за оплату» — это UI, а не источник правды. Если вы выдаёте товар на возврате пользователя, вас обманут за пять минут.
Что понадобится до начала
- Аккаунт в ЮKassa и тестовый магазин:
shopIdи секретный ключ из личного кабинета. - Django 5.x с уже настроенной моделью заказа (нужно поле для внешнего ID платежа и статуса).
- Публичный HTTPS-адрес для приёма вебхука. Локально — туннель вроде ngrok, потому что ЮKassa не постучится на
localhost. - Установленный SDK:
pip install yookassa(на момент написания — ветка 3.x).
Модель заказа: минимум полей
# orders/models.py — Django 5.x
import uuid
from django.db import models
class Order(models.Model):
class Status(models.TextChoices):
NEW = "new", "Создан"
PENDING = "pending", "Ожидает оплаты"
PAID = "paid", "Оплачен"
CANCELED = "canceled", "Отменён"
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=16, choices=Status.choices, default=Status.NEW)
# ID платежа на стороне ЮKassa — по нему сверяем вебхук
payment_id = models.CharField(max_length=64, blank=True, db_index=True)
created_at = models.DateTimeField(auto_now_add=True)
Подходы к созданию платежа
ЮKassa предлагает два способа принять деньги, и выбор влияет на всю архитектуру формы.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
| Redirect (confirmation type=redirect) | Всю форму карты держит ЮKassa, вы не касаетесь PAN. Проще с точки зрения PCI DSS. | Пользователь уходит с вашего сайта на страницу оплаты и возвращается по return_url. |
| Встраиваемый виджет (confirmation type=embedded) | Оплата в модальном окне без ухода со страницы, лучше конверсия. | Нужен фронтенд-скрипт виджета, обработка confirmation_token, чуть больше кода на клиенте. |
Для первой интеграции берите redirect: меньше движущихся частей и меньше поводов ошибиться. Виджет подключают, когда конверсия на редиректе уже измерена и её хочется поднять.
Инициализация SDK и создание платежа
# orders/payments.py
import uuid
from decimal import Decimal
from django.conf import settings
from yookassa import Configuration, Payment
# Ключи держим в переменных окружения, не в коде
Configuration.account_id = settings.YOOKASSA_SHOP_ID
Configuration.secret_key = settings.YOOKASSA_SECRET_KEY
def create_payment(order, return_url):
# idempotence_key защищает от двойного списания при ретрае запроса
idempotence_key = str(uuid.uuid4())
payment = Payment.create({
"amount": {
"value": str(order.amount), # строка, две цифры после точки
"currency": "RUB",
},
"confirmation": {
"type": "redirect",
"return_url": return_url,
},
"capture": True, # одностадийный платёж: списываем сразу
"description": f"Заказ {order.id}",
"metadata": {"order_id": str(order.id)},
}, idempotence_key)
return payment
Поле metadata.order_id — ключевое: именно по нему вы свяжете входящий вебхук со своим заказом, не полагаясь на клиентские данные. idempotence_key обязателен для операций создания — при повторе того же ключа ЮKassa вернёт тот же платёж, а не создаст новый.
View, который отправляет пользователя на оплату
# orders/views.py
from django.shortcuts import get_object_or_404, redirect
from .models import Order
from .payments import create_payment
def pay_order(request, order_id):
order = get_object_or_404(Order, id=order_id)
return_url = request.build_absolute_uri("/orders/thanks/")
payment = create_payment(order, return_url)
order.payment_id = payment.id
order.status = Order.Status.PENDING
order.save(update_fields=["payment_id", "status"])
# confirmation_url — адрес страницы оплаты на стороне ЮKassa
return redirect(payment.confirmation.confirmation_url)
Приём вебхука — здесь ломается чаще всего
После оплаты ЮKassa шлёт POST на настроенный вами URL с JSON вида {"event": "payment.succeeded", "object": {...}}. Три типовые ошибки на этом шаге:
- Забыть отключить CSRF. Вебхук приходит без CSRF-токена. Без
@csrf_exemptDjango вернёт 403, а ЮKassa посчитает уведомление недоставленным и будет повторять его. - Поверить телу запроса без проверки. Кто угодно может отправить вам «payment.succeeded». Проверяйте источник и перепроверяйте статус запросом к API по
payment_id. - Не отвечать 200. Любой ответ кроме 200 ЮKassa трактует как ошибку и ретраит уведомление в течение суток. Дубли обязаны обрабатываться идемпотентно.
# orders/views.py
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from yookassa import Payment
from yookassa.domain.notification import WebhookNotification
from .models import Order
@csrf_exempt
@require_POST
def yookassa_webhook(request):
try:
payload = json.loads(request.body)
notification = WebhookNotification(payload)
except Exception:
return HttpResponseBadRequest("bad payload")
payment_object = notification.object
# Перепроверяем статус прямым запросом к API, а не верим телу вебхука
payment = Payment.find_one(payment_object.id)
if payment.status == "succeeded" and payment.paid:
order_id = payment.metadata.get("order_id")
# update() без повторной выдачи товара, если уже оплачен
updated = Order.objects.filter(
id=order_id, status=Order.Status.PENDING
).update(status=Order.Status.PAID)
# updated == 0 значит вебхук-дубль, это нормально
return HttpResponse(status=200)
Дополнительно ЮKassa рекомендует ограничить приём вебхуков списком их IP-адресов — актуальный список смотрите в документации, он меняется. Проверять принадлежность IP лучше на уровне nginx или через middleware, а не хардкодить в view.
Почему прямой запрос к API, а не доверие вебхуку
Тело уведомления может быть подделано, если ваш URL кто-то узнал. Запрос Payment.find_one(id) уходит на серверы ЮKassa с вашим секретным ключом и возвращает реальный статус. Даже если злоумышленник пришлёт фейковый «succeeded», реальный статус платежа окажется pending или canceled, и товар не выдастся.
Когда что выбирать
- Redirect + capture=True — большинство магазинов с моментальной выдачей. Деньги списываются сразу.
- capture=False (двухстадийный платёж) — если между заказом и отгрузкой есть проверка наличия. Сначала холдируете сумму, потом подтверждаете
Payment.capture()или отменяете. Холд живёт ограниченное время — уточняйте срок в документации. - Embedded-виджет — когда уход на внешнюю страницу уже заметно роняет конверсию и это подтверждено цифрами.
Что делать дальше
Рабочий минимум готов: платёж создаётся, пользователь платит, статус меняет вебхук. Дальше в проде смотрите на три вещи. Первое — логируйте каждый входящий вебхук с его ID до обработки, иначе разбирать спорные платежи будет нечем. Второе — заведите ежедневную сверку: раз в сутки запрашивайте у ЮKassa список платежей и сверяйте со своими заказами, потому что вебхук изредка не доходит. Третье — вынесите тяжёлую обработку (отправку письма, генерацию документов) в Celery, чтобы view вебхука отвечал 200 быстро и не ловил ретраи по таймауту. Возвраты и чеки по 54-ФЗ — отдельная тема, здесь мы их не касались.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: ЮKassa API — официальная документация, yookassa-sdk-python на GitHub
Частые вопросы
А если я использую Django другой версии — 4.2?
Вебхук не приходит, что проверить в первую очередь?
Пользователь заплатил, но вернулся на сайт и не увидел «оплачено». Это баг?
Не спишутся ли деньги дважды при повторной отправке формы?
Обязательно ли перепроверять статус запросом к API, если вебхук пришёл?
Где хранить shopId и секретный ключ?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.