ПРАКТИЧЕСКИЙ РАЗБОР · 7 минут

Как принимать оплату на своём сайте

Приём платежей через ЮKassa в приложении на Django и Next.js, от кнопки оплаты до подтверждения платежа

Допустим, вы собрали веб-приложение. Пользователи могут зарегистрироваться, открыть личный кабинет и попробовать основные функции. Теперь нужно добавить платный доступ. На первый взгляд задача простая. Поставить кнопку «Оплатить», отправить человека на страницу платёжного сервиса и после оплаты открыть ему нужные возможности.

Но кнопка решает только начало задачи. Приложение должно самостоятельно узнать, что платёж действительно прошёл, и связать его с конкретным пользователем. Разберём, как это устроить на примере Django, Next.js и ЮKassa.

Что происходит после нажатия кнопки «Оплатить»

В приложении появляется кнопка покупки. Например, пользователь хочет приобрести доступ к дополнительным функциям AI-чата.

Дальше работают три участника.

  1. Frontend принимает нажатие и обращается к вашему серверу.

  2. Backend создаёт платёж через API ЮKassa и получает ссылку для оплаты.

  3. ЮKassa показывает платёжную страницу и обрабатывает действия покупателя.

После этого приложение должно проверить результат и выдать доступ.

В Next.js остаются интерфейс и переход на страницу оплаты. Django выполняет серверную часть, хранит информацию о заказе и общается с платёжным API. Саму оплату пользователь проходит на стороне ЮKassa. Вашему приложению не нужно самостоятельно принимать данные банковской карты.

Почему нельзя открыть доступ сразу после возвращения

При создании платежа сервер передаёт параметр return_url. По этому адресу пользователь возвращается после взаимодействия с платёжной страницей. Но возврат ещё не доказывает, что деньги получены. Покупатель мог отменить операцию. Платёж мог остаться в обработке или завершиться ошибкой.

Поэтому страница возвращения может показать сообщение «Проверяем оплату», но решение о выдаче доступа принимает сервер. Для этого у ЮKassa есть статусы платежа. При обычной одностадийной оплате успешное завершение обозначается succeeded.

Именно подтверждённый результат оплаты должен менять состояние заказа в вашем приложении.

Создаём первый платёж через API

Для работы понадобятся идентификатор магазина и секретный ключ ЮKassa. Их сохраняют на сервере в переменных окружения, а не в коде страницы Next.js.

Установим Python-библиотеку для HTTP-запросов.

pip install requests

Предположим, сервер уже создал заказ и назначил ему идентификатор. Для примера стоимость доступа фиксирована на сервере.

import os
import uuid
import requests

PRICE_RUB = "490.00"

def create_checkout(order_id: str, request_key: str):
    response = requests.post(
        "https://api.yookassa.ru/v3/payments",
        auth=(
            os.environ["YOOKASSA_SHOP_ID"],
            os.environ["YOOKASSA_SECRET_KEY"],
        ),
        headers={
            "Idempotence-Key": request_key,
        },
        json={
            "amount": {
                "value": PRICE_RUB,
                "currency": "RUB",
            },
            "capture": True,
            "confirmation": {
                "type": "redirect",
                "return_url": "https://example.com/payment/result",
            },
            "description": f"Оплата заказа {order_id}",
            "metadata": {
                "order_id": order_id,
            },
        },
        timeout=20,
    )

    response.raise_for_status()
    payment = response.json()

    confirmation = payment.get("confirmation") or {}
    payment_url = confirmation.get("confirmation_url")

    if not payment_url:
        raise ValueError("ЮKassa не вернула ссылку на оплату")

    return {
        "payment_id": payment["id"],
        "payment_url": payment_url,
    }

Это серверная функция для создания платежа. Её вызывает обработчик Django после проверки пользователя и заказа. Сумма 490.00 здесь учебная. В настоящем приложении сервер берёт стоимость из собственного тарифа или заказа. Цена, которую браузер прислал в запросе, не должна становиться основанием для списания.

Параметр capture=True означает одностадийный сценарий оплаты. Отдельное подтверждение списания со стороны магазина для него не требуется.

Idempotence-Key помогает избежать повторного выполнения одного и того же запроса. Ключ можно создать через uuid.uuid4() при подготовке платежа и сохранить вместе с заказом. При повторной попытке отправить тот же запрос используйте сохранённый ключ, а не новый.

Функция возвращает payment_id и payment_url. Идентификатор платежа сервер сохраняет в заказе, а адрес передаёт frontend для перехода на страницу ЮKassa.

В рабочем приложении также обрабатывают ошибки API, а сам заказ создают до обращения к платёжному сервису.

Как приложение узнаёт об успешной оплате

После создания платежа приложение ждёт результат. ЮKassa может отправить серверу уведомление payment.succeeded. Такой механизм называется webhook. Проще говоря, платёжный сервис самостоятельно обращается к определённому адресу вашего backend и сообщает об изменении статуса.

Получив уведомление, Django находит связанный заказ. Затем проверяет платёж по API ЮKassa, сопоставляет идентификатор, сумму и статус. Если всё совпало и заказ ещё не отмечен как оплаченный, сервер открывает доступ пользователю.

Здесь есть особенность, уведомление может прийти повторно. Поэтому обработчик должен спокойно реагировать на ситуацию, когда доступ уже выдан. Например, у заказа может быть состояние paid. После первого успешного подтверждения оно сохраняется в базе данных. Повторная обработка того же платежа ничего не начисляет и не создаёт второй доступ.

Для небольшого сервиса этого правила достаточно, чтобы понять основу. В более развитой системе такие изменения выполняют атомарно, чтобы два одновременных события не обработали один заказ дважды.

Три ошибки, которые лучше предупредить

Первая ошибка. Доверять странице возврата.

Пользователь открыл return_url, а приложение сразу включило платные функции. Проверка должна выполняться на backend по подтверждённому статусу платежа.

Вторая ошибка. Брать цену из frontend.

Покупатель управляет запросами своего браузера. Поэтому backend самостоятельно определяет сумму и проверяет состав заказа.

Третья ошибка. Не учитывать повторные действия.

Пользователь дважды нажал кнопку. Сервер повторил запрос после сбоя. ЮKassa повторно прислала уведомление.

Все три ситуации нужно обработать так, чтобы один оплаченный заказ приводил к одной выдаче доступа.

Подробный разбор первого платежа, защиты от двойного нажатия и связи оплаты с подпиской есть в моей статье на Habr о ЮKassa в AI-продукте.

Как платёж становится частью приложения

Подключить страницу оплаты обычно проще, чем правильно встроить платежи в собственную систему. После успешной покупки нужно обновить права пользователя, показать новый статус в личном кабинете и сохранить информацию о заказе. Для продукта с подписками добавляются сроки доступа, продление и правила повторных покупок.

Посмотрите AI-Chat. В этом учебном проекте платёжная часть связана с приложением на Django и Next.js.

В курсе «LLM-Chat + Mermaid + ЮKassa» последовательно собираем AI-сервис с пользовательским интерфейсом, backend, оплатой и управлением доступом.

Первый платёж начинается с одной кнопки и одного серверного запроса. Настоящий результат появляется тогда, когда приложение надёжно понимает, кто заплатил и какие возможности ему теперь доступны.