Приём платежей в Django: как встроить провайдера без граблей
Три способа подключить платёжную систему к Django-проекту: готовый пакет, прямой вызов API провайдера и редирект на его форму. У каждого своя цена в безопасности, поддержке и контроле над UX.

Магазин на Django, оформление заказа готово, осталось взять деньги. Вопрос, который решает архитектуру половины бэкенда: как именно вы принимаете платёж. От выбора зависит, храните ли вы данные карт (и попадаете ли под PCI DSS), как обрабатываете возвраты, что делаете, когда провайдер прислал уведомление об оплате, а пользователь закрыл вкладку до редиректа обратно.
Разберём три подхода на примере российских провайдеров (ЮKassa, Тинькофф, CloudPayments) и покажем, где каждый ломается. Код — по делу, версии: Django 4.2 LTS и 5.x, Python 3.11+.
Задача и почему «просто вызвать API» мало
Оплата — это не один HTTP-запрос. Это распределённая транзакция между тремя сторонами: пользователь, ваш сервер, сервер провайдера. В любой момент связь рвётся. Пользователь ушёл с формы 3-D Secure, вернулся через полчаса; провайдер списал деньги, но ваш ответ на его вебхук потерялся из-за деплоя; браузер прислал редирект «успех», а деньги на самом деле не пришли.
Поэтому источник истины о статусе платежа — всегда сервер провайдера, а не редирект пользователя. Любой вариант интеграции обязан уметь принимать серверные уведомления (вебхуки/нотификации) и переспрашивать статус по API. Это не деталь, а требование ко всем трём подходам ниже.
Редирект пользователя на «страницу успеха» — это подсказка UI, а не факт оплаты. Заказ переводите в статус «оплачен» только по вебхуку или по прямому запросу статуса к API провайдера.
Вариант 1. Готовый пакет-интеграция
Для популярных провайдеров есть SDK и Django-обёртки. У ЮKassa — официальный пакет yookassa (Python SDK, не Django-специфичный). Для расчётов внутри Django встречаются пакеты вроде django-payments с набором бэкендов под разные провайдеры.
Идея: пакет берёт на себя формирование запроса на создание платежа, подпись, разбор ответа. Вы дёргаете метод и получаете ссылку на форму оплаты.
# yookassa SDK, версия пакета уточните на PyPI перед установкой
from yookassa import Configuration, Payment
import uuid
Configuration.account_id = "YOUR_SHOP_ID"
Configuration.secret_key = "YOUR_SECRET_KEY"
def create_payment(order):
idempotence_key = str(uuid.uuid4()) # защита от двойного списания при ретраях
payment = Payment.create({
"amount": {"value": f"{order.total:.2f}", "currency": "RUB"},
"confirmation": {
"type": "redirect",
"return_url": "https://example.com/orders/return/",
},
"capture": True,
"description": f"Заказ №{order.id}",
"metadata": {"order_id": order.id},
}, idempotence_key)
order.payment_id = payment.id # сохраняем, чтобы потом сверить вебхук
order.save(update_fields=["payment_id"])
return payment.confirmation.confirmation_urlТочные имена методов и структуру полликов сверяйте с актуальной документацией провайдера — они меняются между версиями SDK. Здесь важен принцип: idempotence key обязателен, иначе повторный запрос при таймауте создаст второй платёж.
Что выигрываете: меньше кода на подпись и разбор ответа, готовые модели статусов. Чем платите: зависимость от чужого релизного цикла. Провайдер обновил API — ждёте, пока мейнтейнер пакета догонит. Абстрактный пакет вроде django-payments покрывает общий знаменатель провайдеров и часто не даёт доступа к специфичным полям (чек по 54-ФЗ, сохранение метода оплаты для рекуррентов).
Вариант 2. Прямой вызов API провайдера
Вы сами шлёте HTTP-запросы на эндпоинты провайдера через requests или httpx, сами считаете подпись, сами разбираете ответ. Никаких обёрток.
import httpx
import base64
# Пример структуры запроса к ЮKassa через прямой HTTP
# Точные URL и заголовки — из официальной документации провайдера
def create_payment_direct(order, shop_id: str, secret: str):
auth = base64.b64encode(f"{shop_id}:{secret}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"Idempotence-Key": str(order.idempotence_key),
"Content-Type": "application/json",
}
body = {
"amount": {"value": f"{order.total:.2f}", "currency": "RUB"},
"capture": True,
"confirmation": {
"type": "redirect",
"return_url": "https://example.com/orders/return/",
},
"metadata": {"order_id": order.id},
}
with httpx.Client(timeout=15) as client:
resp = client.post(
"https://api.yookassa.ru/v3/payments", # сверьте адрес по докам
json=body, headers=headers,
)
resp.raise_for_status()
data = resp.json()
return data["confirmation"]["confirmation_url"]Что выигрываете: полный контроль. Появилось новое поле в API — используете сразу, не ждёте никого. Понимаете каждый байт запроса, легче отлаживать. Чем платите: сами держите в голове формат подписи, коды ошибок, ретраи, таймауты. Провайдер меняет API — правите вы. Тесты пишете сами.
Вариант 3. Редирект на готовую платёжную форму
Самый простой по объёму работы: вы формируете подписанную ссылку или POST-форму, пользователь уходит на страницу провайдера, вводит карту там, возвращается к вам. Данные карты у вас не появляются вообще.
Это ключевое отличие: если карта вводится на стороне провайдера, вы не обрабатываете и не храните её данные, и требования PCI DSS к вам сводятся к минимальной анкете самооценки (SAQ A). Как только вы принимаете номер карты на своей форме — попадаете под гораздо более тяжёлый набор требований.
Что выигрываете: минимум обязательств по безопасности, минимум кода. Чем платите: UX уводит пользователя с вашего сайта на чужой дизайн, кастомизация ограничена настройками провайдера. Мобильные конверсии на редиректах иногда ниже из-за лишнего перехода.
Сравнение
| Критерий | Готовый пакет | Прямой API | Редирект на форму |
|---|---|---|---|
| Объём своего кода | Средний | Высокий | Низкий |
| Контроль над полями запроса | Ограничен пакетом | Полный | Полный в рамках формы |
| Данные карты на вашем сервере | Обычно нет | Нет (при редиректе) | Нет |
| Уровень PCI DSS | Зависит от режима | Зависит от режима | Минимальный (SAQ A) |
| Скорость реакции на смену API провайдера | Ждёте мейнтейнера | Своими силами | Не касается |
| Кастомизация UX оплаты | Средняя | Высокая (с виджетом) | Низкая |
Вебхук — общая часть для всех трёх
Как бы вы ни создавали платёж, статус подтверждаете вебхуком. Ключевые правила: проверяйте, что запрос действительно от провайдера (подпись или проверка по списку IP), делайте обработчик идемпотентным (один вебхук может прийти дважды), и не доверяйте сумме из вебхука вслепую — сверяйте с заказом.
import json
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.db import transaction
@csrf_exempt # провайдер не знает вашего CSRF-токена; защита — по подписи/IP
def payment_webhook(request):
if request.method != "POST":
return HttpResponseBadRequest()
# 1. Проверка подлинности источника до разбора тела
if not is_request_from_provider(request):
return HttpResponseBadRequest("bad source")
event = json.loads(request.body)
payment_id = event["object"]["id"]
status = event["object"]["status"]
with transaction.atomic():
# select_for_update защищает от гонки при двойном вебхуке
order = Order.objects.select_for_update().get(payment_id=payment_id)
if order.is_paid:
return HttpResponse(status=200) # уже обработали, отвечаем ОК
if status == "succeeded":
# сверяем сумму, а не верим на слово
paid = event["object"]["amount"]["value"]
if f"{order.total:.2f}" != paid:
return HttpResponseBadRequest("amount mismatch")
order.mark_paid()
return HttpResponse(status=200)Отвечайте провайдеру 200 только после успешной записи в базу. Если обработчик упал, верните 5xx — провайдер повторит доставку. Именно поэтому обработчик обязан быть идемпотентным.
Когда что выбирать
- Редирект на форму провайдера — когда хотите минимум обязательств по PCI DSS и быстро запуститься. Подходит большинству небольших магазинов.
- Готовый пакет — когда провайдер один из популярных, а UX-редиректа хватает, и вы готовы принять зависимость от мейнтейнера. Экономит время на рутине.
- Прямой API — когда нужны специфичные поля (рекуррентные платежи, чеки по 54-ФЗ, сплиты между продавцами) или вы держите несколько провайдеров за одним интерфейсом и не хотите зоопарк пакетов.
Виджет-оплата (карта вводится в iframe провайдера прямо на вашей странице) — отдельный гибрид: UX как у формы на сайте, но данные карты не касаются вашего сервера. Здесь его не разбираем.
Что делать дальше
Первое — вынесите обращения к API провайдера в асинхронную задачу (Celery), чтобы таймаут провайдера не держал воркер Django. Второе — настройте retry на вебхуках со стороны провайдера и мониторинг «оплачено у провайдера, но не оплачено у нас»: расхождение здесь означает потерянные заказы или недоставленные вебхуки. Третье — заведите периодическую сверку статусов по API для платежей, зависших в промежуточном состоянии дольше N минут. И держите тестовый режим провайдера в CI: боевые ключи в тестах не место.
Дефектный акт по фото: Осмотр по фотографиям: классификация дефектов, ссылки на нормы, требования к устранению. Готовый акт в Word.
Спросить за 80 ₽Источники: ЮKassa API — официальная документация, Django — CSRF и декоратор csrf_exempt
Частые вопросы
А если у меня Django 3.2, код из статьи заработает?
Вебхук не приходит, что проверять первым?
Нужен ли мне PCI DSS, если я просто редиректю на форму провайдера?
Что делать, если деньги списались, а вебхук потерялся при деплое?
Можно ли обойтись без Celery и слать запрос к провайдеру прямо во вью?
Как тестировать оплату, не тратя реальные деньги?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.