Русский | English
Неофициальная библиотека. Проект не связан с ЮKassa и ЮMoney, ими не поддерживается и их продуктом не является. Официальный SDK лежит здесь. ЮKassa и ЮMoney — товарные знаки своих владельцев.
Неофициальный клиент ЮKassa для Python в двух режимах: синхронном и асинхронном. Типизированные модели, разбор уведомлений, идемпотентность и повторы из коробки.
pip install yookassaxТребуется Python 3.10 или новее. Единственная зависимость: httpx.
Синхронно:
from yookassax import YooKassa
with YooKassa(shop_id="123456", secret_key="live_...") as kassa:
payment = kassa.payments.create({
"amount": {"value": "100.00", "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://example.com/done"},
"capture": True,
"description": "Заказ 42",
})
print(payment.confirmation_url)Асинхронно, то же самое:
from yookassax import AsyncYooKassa
async with AsyncYooKassa(shop_id="123456", secret_key="live_...") as kassa:
payment = await kassa.payments.create({
"amount": {"value": "100.00", "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://example.com/done"},
"capture": True,
})Наборы методов у режимов одинаковые, это проверяется тестом. Переход с одного
на другой сводится к добавлению await.
Все сценарии из официальной документации, оба режима, два языка: русский, English.
| Настройка клиента | аутентификация, магазин, подписки |
| Платежи | создание, подтверждение, отмена, списки |
| Возвраты | полные и частичные |
| Чеки | 54-ФЗ, маркированные товары |
| Сделки | безопасная сделка целиком |
| Выплаты | карта, СБП, кошелёк, самозанятые |
| Самозанятые | регистрация и подтверждение |
| Персональные данные | получатели выплат |
| Банки СБП | справочник |
| Счета | ссылка на оплату |
| Способы оплаты | подписки и автоплатежи |
| Кассовые ссылки | статические QR-коды |
| Уведомления | FastAPI, Django, Flask |
| Ошибки и повторы | идемпотентность, обрывы связи |
| Модели ответов | все 73 модели и сверка со спецификацией |
| Партнёрская программа | OAuth, работа от имени чужого магазина |
| Коды ответа HTTP | что означает каждый и что делать |
| Тестовый магазин | отладка, подмена HTTP, живой прогон |
| Логи | logging, loguru, секреты в лог не уходят |
Ключи хранятся в экземпляре клиента. Официальный SDK держит их в
Configuration на уровне класса. Если приложение работает с несколькими
магазинами из одного процесса, два платежа могут переписать токен друг другу
между настройкой и вызовом, и платёж уйдёт через чужой магазин. Здесь каждый
клиент носит свои ключи, и такой гонки не существует.
Асинхронный режим настоящий. Официальный SDK синхронный, внутри requests.
Вызов из асинхронного обработчика останавливает весь воркер: пока идёт
обращение к API, процесс не обслуживает никого.
Ключ идемпотентности проставляется сам на всех изменяющих запросах. Повтор после обрыва связи идёт с тем же ключом, поэтому второго платежа не возникает.
Повторы на кодах 202, 429 и 500 с экспоненциальной паузой и дрожанием. Ошибки данных (400, 404) не повторяются: второй такой же запрос даст тот же ответ.
Способ оплаты разбирается в модель своего типа. Все 19 из спецификации:
PaymentMethodBankCard, PaymentMethodSberLoan,
PaymentMethodElectronicCertificate и так далее. Различать через isinstance,
подробности.
Модели типизированы и терпимы к новым полям. ЮKassa добавляет поля в
ответы; строгая модель превратила бы это в отказ обслуживать платежи. Всё
неизвестное складывается в raw и доступно через extra. Но не молча: на
каждое такое поле один раз выдаётся UnknownFieldWarning, иначе о новом поле
никто и не узнает.
Билдеров нет. Тело запроса это обычный словарь: он принимает новые поля API сразу, а не после обновления библиотеки.
payment = kassa.payments.create({...})
payment.is_pending # ждём оплату
payment.is_waiting_for_capture # деньги захолдированы, нужен capture или cancel
payment.is_succeeded # деньги у магазина
payment.is_canceled # деньги у плательщика
payment.amount.value # Decimal("100.00"), не float
payment.created_at # datetime с часовым поясом
payment.confirmation_url # куда вести плательщика, либо None
kassa.payments.capture(payment.id)
kassa.payments.cancel(payment.id)page = kassa.payments.list(status="succeeded", limit=50)
for payment in page:
print(payment.id)
if page.has_more:
next_page = kassa.payments.list(status="succeeded", cursor=page.next_cursor)Или без ручного перелистывания:
for payment in kassa.payments.iterate(status="succeeded"):
print(payment.id)В асинхронном режиме то же самое через async for.
Тело уведомления ЮKassa не подписывает, поэтому единственная встроенная проверка это адрес отправителя. Её недостаточно: решение о деньгах принимайте по ответу API, а не по телу уведомления.
from fastapi import Request, Response
from yookassax import webhooks
@app.post("/webhook")
async def handle(request: Request):
if not webhooks.is_trusted_ip(request.headers.get("X-Real-IP", "")):
return Response(status_code=403)
notification = webhooks.parse(await request.json())
if notification.is_payment_succeeded:
payment = await kassa.payments.get(notification.object.id)
if payment.is_succeeded:
...
return {"ok": True}Берите достоверный адрес отправителя. За обратным прокси это тот, который
прокси проставляет сам, обычно X-Real-IP из nginx. Левый элемент
X-Forwarded-For подставляет клиент, и проверка теряет смысл.
Отвечайте 200 быстро: иначе ЮKassa повторит доставку, и обработчик получит то же событие ещё раз.
from yookassax import BadRequest, Forbidden, TransportError, YooKassaError
try:
payment = kassa.payments.create({...})
except Forbidden:
# магазину не разрешена операция, частый случай: не подключён рекуррент
...
except BadRequest as error:
print(error.code, error.description, error.parameter)
except TransportError:
# ответа не было вообще, состояние платежа неизвестно
...
except YooKassaError:
...TransportError стоит отдельно от остальных намеренно: если создание платежа
упало с ним, неизвестно, создан платёж или нет.
Поле, которого нет в модели, разбор не роняет, но и не скрывает:
payment = kassa.payments.get(payment_id)
# UnknownFieldWarning: Payment: в ответе API есть поля, которых нет в модели:
# loyalty_bonus. Значения доступны через extra(), но, возможно, стоит обновить
# yookassax.
payment.extra("loyalty_bonus")Предупреждение указывает на строку вашего кода и выдаётся один раз на пару "модель плюс поле" за жизнь процесса: страница из ста платежей даст одну строку, а не сто. Отключается штатным фильтром:
import warnings
from yookassax import UnknownFieldWarning
warnings.filterwarnings("ignore", category=UnknownFieldWarning)Каждое обращение к API пишется стандартным logging в логгер yookassax.
Приложениям на loguru отдельная поддержка не нужна: записи подхватывает
InterceptHandler.
import logging
logging.getLogger("yookassax").setLevel(logging.INFO)ЮKassa ответ: POST /payments -> 200 за 0.412 c, id: 3225ad37-000f-5001-8000-108ff2fd923d
ЮKassa повтор: POST /payments, попытка 2 через 0.503 c, причина: ServerError
Заголовок Authorization в лог не попадает никогда, тела запроса и ответа —
только на DEBUG: там персональные данные плательщика.
payments, refunds, receipts, payouts, webhooks, settings,
payment_methods, deals, invoices, personal_data, self_employed,
pos_links, sbp_banks.
Покрыты все маршруты официальной спецификации OpenAPI. Полнота проверяется тестом.
Для работы с чужими магазинами:
kassa = YooKassa(oauth_token="токен, выданный магазином")Одновременно с shop_id и secret_key не задаётся: клиент откажется
собираться, чтобы не выбирать за вас.
Как получить такой токен, подписаться на уведомления по API и что делать, когда магазин отозвал права, - в примерах по партнёрской программе.
from yookassax import Operation
operation = Operation(
method="POST",
path="/new_endpoint",
body={"key": "value"},
idempotent=True,
)
result = kassa.send(operation)В каталоге docs лежит llms.txt: полный справочник по
библиотеке одним файлом, чтобы вставить в контекст модели. Английская
версия: llms.en.txt.
pip install -e ".[dev]"
pytest
ruff check .
mypy srcТесты не ходят в сеть. Отдельно есть прогон по живому API, он требует ключи тестового магазина и без них пропускается:
export YOOKASSA_SHOP_ID=... YOOKASSA_SECRET_KEY=test_...
pytest tests/integrationMIT.
