Платёжная форма в Django: подключаем ЮKassa за один вечер
Разбираем, как принять оплату на сайте Django 5.x через ЮKassa: создать платёж, отправить пользователя на форму и обработать уведомление о результате. Займёт около часа.

К концу этого руководства у вас будет рабочая цепочка: пользователь нажимает «Оплатить», уходит на форму провайдера, платит и возвращается, а ваш сервер узнаёт результат из уведомления и меняет статус заказа. Разбираем на примере ЮKassa и её официального пакета yookassa, потому что это один из самых частых кейсов в рунете, а логика у большинства провайдеров одинаковая. Django берём версии 5.x, Python 3.10+. Времени уйдёт около часа, если аккаунт продавца у вас уже есть.
Что нужно иметь до начала
Проверьте всё до того, как писать код, — иначе застрянете на середине.
- Проект на Django 5.x с работающими миграциями и хотя бы одной моделью заказа.
- Магазин в личном кабинете ЮKassa (можно тестовый) и оттуда — shopId и секретный ключ. Тестовый магазин выдаёт ключи с префиксом
test_. - Установленный официальный SDK:
pip install yookassa. На момент написания актуальна ветка 3.x. - Публичный HTTPS-адрес для приёма уведомлений. Локально это решается туннелем (ngrok, cloudflared): без внешнего URL провайдер не сможет достучаться до вашего сервера.
Уведомление о платеже (webhook) — единственный надёжный источник правды о том, что деньги пришли. Возврат пользователя на сайт после оплаты этим источником не является: браузер может закрыться на полпути.
Шаг 1. Храним ключи вне кода
Секретный ключ не должен попадать в репозиторий. Прочитайте его из переменных окружения в settings.py:
# settings.py, Django 5.x
import os
YOOKASSA_SHOP_ID = os.environ["YOOKASSA_SHOP_ID"]
YOOKASSA_SECRET_KEY = os.environ["YOOKASSA_SECRET_KEY"]
Значения кладите в .env или в переменные окружения сервера, а сам .env добавьте в .gitignore. Проверка: python manage.py shell, затем from django.conf import settings; print(settings.YOOKASSA_SHOP_ID) — должен вывести ваш shopId, а не упасть с KeyError.
Шаг 2. Модель заказа с полем платежа
Нужны два поля: идентификатор платежа на стороне ЮKassa и статус. Добавьте их в вашу модель заказа.
# orders/models.py
from django.db import models
class Order(models.Model):
PENDING = "pending"
PAID = "paid"
CANCELED = "canceled"
STATUS_CHOICES = [
(PENDING, "Ожидает оплаты"),
(PAID, "Оплачен"),
(CANCELED, "Отменён"),
]
amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=PENDING
)
payment_id = models.CharField(max_length=64, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
После правки: python manage.py makemigrations && python manage.py migrate. В консоли должна появиться строка Applying orders....... OK.
Шаг 3. Создаём платёж и отправляем на форму
Здесь мы конфигурируем SDK и создаём объект платежа. Ключевые параметры: сумма, описание, режим подтверждения redirect и return_url — адрес, куда вернётся пользователь после оплаты.
# orders/views.py
import uuid
from decimal import Decimal
from django.conf import settings
from django.shortcuts import get_object_or_404, redirect
from yookassa import Configuration, Payment
from .models import Order
Configuration.account_id = settings.YOOKASSA_SHOP_ID
Configuration.secret_key = settings.YOOKASSA_SECRET_KEY
def start_payment(request, order_id):
order = get_object_or_404(Order, id=order_id)
# idempotence_key защищает от повторного списания при ретрае
idempotence_key = str(uuid.uuid4())
payment = Payment.create({
"amount": {
"value": str(order.amount), # строка с двумя знаками, напр. "499.00"
"currency": "RUB",
},
"confirmation": {
"type": "redirect",
"return_url": request.build_absolute_uri(
f"/orders/{order.id}/done/"
),
},
"capture": True, # одностадийный платёж: списываем сразу
"description": f"Заказ №{order.id}",
}, idempotence_key)
order.payment_id = payment.id
order.save(update_fields=["payment_id"])
# уводим пользователя на платёжную форму провайдера
return redirect(payment.confirmation.confirmation_url)
Проверка: перейдите на /orders/<id>/start/. Браузер должен уехать на домен yoomoney.ru с формой ввода карты. В тестовом режиме там доступны тестовые номера карт из документации.
Про сумму. Передавайте её строкой с двумя знаками после запятой. Если отправить число с плавающей точкой, легко получить 499.0 или ошибку округления — а деньги ошибок не прощают.
Шаг 4. Принимаем уведомление о результате
ЮKassa шлёт POST с JSON на URL, который вы указали в кабинете магазина в разделе HTTP-уведомлений. Обрабатывать нужно как минимум событие успешной оплаты. Внешний POST приходит без CSRF-токена, поэтому вьюху освобождаем от проверки.
# 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
@csrf_exempt
@require_POST
def payment_webhook(request):
try:
event = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponseBadRequest("bad json")
obj = event.get("object", {})
payment_id = obj.get("id")
if not payment_id:
return HttpResponseBadRequest("no payment id")
# не доверяем телу запроса, а перезапрашиваем статус у провайдера
payment = Payment.find_one(payment_id)
order = Order.objects.filter(payment_id=payment_id).first()
if order is None:
# платёж не наш — отвечаем 200, чтобы не долбили ретраями
return HttpResponse(status=200)
if payment.status == "succeeded" and payment.paid:
order.status = Order.PAID
order.save(update_fields=["status"])
elif payment.status == "canceled":
order.status = Order.CANCELED
order.save(update_fields=["status"])
# 200 говорит провайдеру: уведомление доставлено, не повторять
return HttpResponse(status=200)
Главное правило: не верьте телу webhook на слово. Кто угодно может отправить POST с "status": "succeeded". Метод Payment.find_one перезапрашивает реальный статус по защищённому каналу — только он и решает.
Проверка: оплатите тестовый заказ, дождитесь POST на webhook (в логах туннеля увидите входящий запрос), затем в админке убедитесь, что статус заказа стал «Оплачен».
Маршруты
# orders/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("orders/<int:order_id>/start/", views.start_payment, name="start_payment"),
path("payments/webhook/", views.payment_webhook, name="payment_webhook"),
]
Полный адрес webhook (https://ваш-домен/payments/webhook/) пропишите в личном кабинете ЮKassa в настройках HTTP-уведомлений.
Если не получилось
AuthenticationErrorпри создании платежа. Неверная пара shopId/секретный ключ или вы смешали тестовые и боевые. Убедитесь, что ключ с префиксомtest_соответствует тестовому магазину.- Webhook не приходит. Проверьте, что URL в кабинете указан с HTTPS и без опечаток, а туннель поднят. Провайдер не стучится на
http://localhost. 403 Forbiddenна webhook. Забыли@csrf_exempt. Django отклоняет POST без CSRF-токена, а внешний сервис его не пришлёт.- Статус заказа не меняется, хотя POST пришёл. Скорее всего
payment_idне совпал: проверьте, что вы сохранили его в шаге 3 до редиректа, а в webhook ищете заказ по тому же полю. - Двойное списание. Не передаёте
idempotence_keyили генерируете его заново при ретрае. Ключ должен быть уникален на попытку создания, но постоянен при повторе одной и той же операции.
Что делать дальше
Рабочая цепочка готова, но до боевого режима осталось несколько шагов. Переключите магазин из тестового в боевой и замените ключи в переменных окружения. Добавьте обработку возврата средств через Refund.create, если у вас есть отмены заказов. Настройте чеки для 54-ФЗ — ЮKassa умеет отправлять данные в ОФД, для этого в объект платежа добавляется блок receipt; параметры смотрите в актуальной документации, потому что состав обязательных полей меняется. И заведите логирование всех входящих webhook в отдельную таблицу — при спорах с покупателем это ваш единственный след того, что и когда пришло.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: ЮKassa для разработчиков: приём платежей (документация API), yookassa-sdk-python — репозиторий официального SDK
Частые вопросы
А если у меня Django другой версии, например 4.2?
Обязательно ли проверять подпись уведомления?
Что будет, если пользователь закроет вкладку сразу после оплаты?
Почему сумму нужно передавать строкой, а не числом?
Webhook приходит несколько раз для одного платежа — это нормально?
Можно ли обойтись без публичного HTTPS-адреса на этапе разработки?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.