Payin H2H

Python

Python SDK для интеграции с Merchant Payment API.

Python

Python SDK для интеграции с Merchant Payment API.

Требования

  • Python >= 3.10
  • Зависимости: cryptography >= 41.0

Установка

pip install "payin-sdk @ git+https://github.com/unitewt/sdk-payin.git#subdirectory=python"

Или клонируйте репозиторий и установите локально:

git clone https://github.com/unitewt/sdk-payin.git
pip install ./sdk-payin/python

Генерация ключей ed25519

Перед началом работы необходимо сгенерировать пару ключей и передать публичный ключ в систему uniteplat:

import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()

priv_raw = private_key.private_bytes_raw()
pub_raw = public_key.public_bytes_raw()
priv_full = priv_raw + pub_raw

print("Public key (передать в uniteplat):", base64.b64encode(pub_raw).decode())
print("Private key (хранить у себя):", base64.b64encode(priv_full).decode())

Или используйте готовый скрипт:

python examples/generate_keys.py

Быстрый старт

Инициализация клиента

import base64
from payin_sdk import Config, PayinClient

config = Config(
    base_url="https://merchant.uniteplat.org",
    key_id="ваш-key-id",
    private_key=base64.b64decode("ваш-приватный-ключ-в-base64"),
    server_public_key=base64.b64decode("публичный-ключ-сервера-в-base64"),  # опционально, для верификации ответов
)

client = PayinClient(config)

Создание платежа (синхронный режим)

Реквизиты возвращаются сразу в ответе:

from payin_sdk import Currency, PaymentMethod

payment = client.new_payment(
    sum=100.50,
    currency=Currency.RUB,
    payment_method=PaymentMethod.CARD,  # или PaymentMethod.SBP
    customer_account="user_123",
    status_webhook_url="https://your-site.com/webhook/status",  # опционально
    payer_bank="sberbank",                          # опционально (требуется некоторыми мерчантами)
    redirect_url="https://your-site.com/return",    # опционально
    email="user@example.com",                       # опционально
    user_created_at="2024-01-15T10:00:00Z",         # опционально, RFC3339/ISO8601
)

print(payment.order_id)            # ID заказа для дальнейших операций
print(payment.payee_account)       # Номер карты/счёта для оплаты
print(payment.payee_account_bank)  # Название банка (может быть None)
print(payment.payee_name)          # Имя получателя (может быть None)
print(payment.sum)                 # Сумма к оплате

Поля payee_account_bank, payee_name и deeplink могут отсутствовать в ответе — в этом случае они приходят как None.

Создание платежа (асинхронный режим)

Реквизиты придут на credentials_webhook_url после назначения:

payment = client.new_payment_async(
    sum=100.50,
    currency=Currency.RUB,
    payment_method=PaymentMethod.SBP,
    customer_account="user_123",
    credentials_webhook_url="https://your-site.com/webhook/credentials",  # обязательно
    status_webhook_url="https://your-site.com/webhook/status",
)

print(payment.order_id)  # Показать пользователю страницу ожидания

credentials_webhook_url обязателен для асинхронного режима: реквизиты придут на этот URL после назначения.

Доступные суммы

Запрос подходящих под оплату сумм до создания платежа:

result = client.get_available_amounts(
    sum=100.50,
    currency=Currency.RUB,
    payment_method=PaymentMethod.CARD,
    customer_account="user_123",  # опционально
    sum_drift_up=50,              # опционально, допустимое отклонение вверх
    sum_drift_down=50,            # опционально, допустимое отклонение вниз
)

print(result.available_amounts)  # [100.0, 150.0, ...] — список доступных сумм

Подтверждение оплаты

Вызывается после того, как пользователь сообщает, что совершил перевод:

result = client.confirm("abc123")
print(result.order_id)

Отмена платежа

result = client.cancel("abc123")

Отменить платёж можно, только пока он ожидает подтверждения оплаты пользователем либо находится на стадии арбитража (включая receiptRequired). Финальные статусы (doneSuccess, doneFail, arbitrageCancelledByMerchant) отменить нельзя.

Получение статуса

from payin_sdk import PaymentStatus

status = client.get_status("abc123")

print(status.status.value)  # "pending", "doneSuccess", etc.

if status.deeplink is not None:
    print(status.deeplink)  # Ссылка-диплинк для оплаты (если настроена у мерчанта)

if status.is_successful():
    # платёж завершён успешно
    ...

if status.requires_receipt():
    # необходимо прикрепить чек
    ...

if status.is_arbitrage_cancelled_by_merchant():
    # платёж отменён мерчантом по итогам арбитража (финальный статус)
    ...

if status.is_done():
    # платёж в финальном статусе (doneSuccess, doneFail или arbitrageCancelledByMerchant)
    ...

Возможные значения PaymentStatus: free, pending, receiptRequired, doneSuccess, doneFail, arbitrageCancelledByMerchant. Статус arbitrageCancelledByMerchant — финальный (возвращается, когда мерчант отменил платёж по итогам арбитража) и учитывается в is_done().

Прикрепление чека

Требуется при статусе receiptRequired:

# Один файл
client.attach_receipt("abc123", "/path/to/receipt.pdf")

# Несколько файлов
client.attach_receipt("abc123", "/path/to/receipt1.pdf", "/path/to/receipt2.jpg")

Обработка вебхуков

Credentials webhook (credentials_webhook_url)

import base64
from payin_sdk import WebhookVerifier
from payin_sdk.exceptions import SignatureError

verifier = WebhookVerifier(base64.b64decode("публичный-ключ-сервера-в-base64"))

# В Flask/Django/FastAPI хендлере:
def handle_credentials_webhook(request):
    try:
        payload = verifier.verify_credentials_webhook(
            body=request.body,
            signature_base64=request.headers.get("X-Signature", ""),
        )

        # Реквизиты готовы — показать пользователю
        print(payload["orderId"])
        print(payload["payeeAccount"])
        print(payload["payeeAccountBank"])
        print(payload["payeeName"])
        if payload.get("deeplink"):
            print(payload["deeplink"])  # Ссылка-диплинк, если настроена у мерчанта

        return Response(status=200)
    except SignatureError:
        return Response(status=403)

Status webhook (status_webhook_url)

from payin_sdk import WebhookVerifier, PaymentStatus
from payin_sdk.exceptions import SignatureError

verifier = WebhookVerifier(base64.b64decode("публичный-ключ-сервера-в-base64"))

def handle_status_webhook(request):
    try:
        payload = verifier.verify_status_webhook(
            body=request.body,
            signature_base64=request.headers.get("X-Signature", ""),
        )

        match payload.get("status"):
            case PaymentStatus.DONE_SUCCESS.value:
                handle_success(payload["orderId"])
            case PaymentStatus.DONE_FAIL.value:
                handle_failure(payload["orderId"])
            case PaymentStatus.RECEIPT_REQUIRED.value:
                handle_receipt_required(payload["orderId"])
            case PaymentStatus.ARBITRAGE_CANCELLED_BY_MERCHANT.value:
                handle_arbitrage_cancelled(payload["orderId"])

        return Response(status=200)
    except SignatureError:
        return Response(status=403)

Обработка ошибок

Все методы могут бросать следующие исключения:

ИсключениеКогда
ApiErrorAPI вернул success=false или неожиданный HTTP-статус
RateLimitErrorПревышен лимит запросов (HTTP 429); подкласс ApiError
SignatureErrorНе прошла верификация подписи сервера или вебхука
PayinErrorБазовый класс; перехватывает все ошибки SDK

ApiError несёт код ошибки error_code (тип ErrorCode), классифицированный по HTTP-статусу, а также status_code и order_id. Доступны предикаты: is_bad_request() (400, в т.ч. не передан обязательный payer_bank), is_forbidden() (403, мерчант заблокирован/отключён), is_not_found() (404), is_rate_limited() (429), is_server_error() (5xx).

from payin_sdk import RateLimitError
from payin_sdk.exceptions import ApiError, SignatureError, PayinError

try:
    payment = client.new_payment(
        sum=100.50,
        currency=Currency.RUB,
        payment_method=PaymentMethod.CARD,
        customer_account="user_123",
    )
except RateLimitError as e:
    # Превышен лимит запросов (HTTP 429) — стоит повторить позже
    ...
except ApiError as e:
    # Прочие ошибки API: str(e), e.order_id, e.status_code, e.error_code
    if e.is_forbidden():
        ...  # мерчант заблокирован или отключён
    elif e.is_bad_request():
        ...  # ошибка валидации, в т.ч. не передан обязательный payer_bank
    ...
except SignatureError as e:
    # Проблема с подписью
    ...
except PayinError as e:
    # Любая другая ошибка SDK
    ...

Структура пакета

python/
├── pyproject.toml
├── payin_sdk/
│   ├── __init__.py       # Экспорты
│   ├── client.py         # Главный клиент
│   ├── config.py         # Параметры подключения
│   ├── signer.py         # Подпись/верификация ed25519
│   ├── http_client.py    # HTTP-клиент
│   ├── webhook.py        # Верификация вебхуков
│   ├── models.py         # Модели запросов/ответов
│   ├── enums.py          # Currency | PaymentMethod | PaymentStatus
│   └── exceptions.py     # Типы ошибок
└── examples/
    ├── generate_keys.py     # Генерация ключей
    └── integration_test.py  # Интеграционный тест
Copyright © 2026