@fw42/yoomoney
v2.4.6
Published
Runtime-agnostic TypeScript SDK for YooMoney Wallet API — payments, history, notifications, payment links
Maintainers
Readme
yoomoney-sdk
Runtime-agnostic TypeScript SDK для YooMoney Wallet API.
Работает с Node.js 18+, Bun, Deno — использует только Web API (fetch, crypto.subtle, URLSearchParams), без привязки к конкретному рантайму.
Возможности
- Информация об аккаунте — баланс, статус, привязанные карты
- История операций — фильтрация по типу, лейблу, дате; автопагинация
- Детали операции — подробная информация о транзакции
- Проверка платежа по лейблу — верификация входящих платежей
- Ожидание платежа — поллинг до появления платежа с заданным лейблом
- Генерация ссылок на оплату — URL и HTML-форма для quickpay
- Верификация вебхуков — HMAC-SHA256 проверка подписи уведомлений
Получение токена
Для работы с API нужен OAuth-токен YooMoney. Вот как его получить:
Шаг 1. Регистрация приложения
Перейдите на страницу регистрации: https://yoomoney.ru/myservices/new
Заполните форму:
| Поле | Что указать |
|---|---|
| Название | Любое, например My App |
| Redirect URI | Рабочий домен, который вы контролируете (например https://example.com). Не используйте localhost или несуществующие домены — редирект не сработает и вы не получите code. |
| Почта для связи | Ваш email |
Важно: Redirect URI должен быть реальным доменом, на который браузер сможет перейти. После авторизации YooMoney перенаправит вас на этот адрес с параметром
codeв URL — вам нужно будет скопировать его из адресной строки.
Нажмите Подтвердить. Вы получите client_id и client_secret — сохраните их.
Шаг 2. Авторизация (получение code)
Откройте в браузере ссылку, подставив ваш client_id и redirect_uri:
https://yoomoney.ru/oauth/authorize?client_id=ВАШ_CLIENT_ID&response_type=code&redirect_uri=https://ваш-домен.com&scope=account-info%20operation-history%20operation-details- Залогиньтесь в YooMoney и разрешите доступ приложению.
- Браузер перенаправит вас на
https://ваш-домен.com?code=XXXXXXXXX. - Скопируйте значение
codeиз адресной строки.
Шаг 3. Обмен code на токен
curl -X POST https://yoomoney.ru/oauth/token \
-d "code=ВАШ_CODE&client_id=ВАШ_CLIENT_ID&grant_type=authorization_code&redirect_uri=https://ваш-домен.com&client_secret=ВАШ_CLIENT_SECRET"В ответе получите access_token:
{"access_token":"4100XXXX.XXXXXXXX..."}Токен бессрочный — действует пока не отзовёте доступ или не запросите авторизацию повторно.
Шаг 4. Проверка
YOOMONEY_TOKEN="ваш_access_token" npm run example:accountЕсли всё правильно — увидите информацию о вашем кошельке.
Установка
npm install @fw42/yoomoney
# или
bun add @fw42/yoomoneyБыстрый старт (из исходников)
Node.js
git clone https://github.com/FastWalker42/yoomoney-sdk.git
cd yoomoney-sdk
npm install
npm run buildBun
git clone https://github.com/FastWalker42/yoomoney-sdk.git
cd yoomoney-sdk
bun installBun нативно исполняет TypeScript, сборка не нужна.
Использование
import { YooMoneyClient } from "@fw42/yoomoney";
const client = new YooMoneyClient({
token: "your_oauth_token",
});
// Последние 10 операций
const operations = await client.getRecentOperations(10);
// Проверить платёж по лейблу (с проверкой суммы и статуса)
const result = await client.checkPaymentByLabel("order-42", { amount: 500 });
if (result.found) {
console.log("Платёж найден!", result.operations);
}
// Информация об аккаунте
const info = await client.getAccountInfo();
console.log(`Баланс: ${info.balance}`);
// Детали операции
const details = await client.getOperationDetails({
operation_id: "1234567",
});
// Итерация по всей истории (автопагинация)
for await (const op of client.getOperationHistoryAll({ type: "deposition" })) {
console.log(op.title, op.amount);
}
// Ожидание платежа (поллинг, таймаут 5 минут, проверка суммы)
const ops = await client.waitForPayment("order-42", {
timeoutMs: 300_000,
intervalMs: 5_000,
amount: 500,
});Генерация ссылок на оплату
Создавайте ссылки на оплату через YooMoney quickpay. Указывайте label чтобы потом идентифицировать платёж.
import { generatePaymentLink, generatePaymentForm } from "@fw42/yoomoney";
// Ссылка — открывается в браузере, пользователь сразу видит форму оплаты
const link = generatePaymentLink({
receiver: "4100118425529732", // ваш кошелёк
sum: 500, // сумма списания с отправителя
label: "order-123", // уникальный ID для идентификации
paymentType: "AC", // AC = карта, PC = кошелёк
successURL: "https://example.com/thanks",
});
console.log(link);
// HTML-форма для встраивания на сайт
const html = generatePaymentForm({
receiver: "4100118425529732",
sum: 500,
label: "order-123",
});Проверка платежей — как это работает
YooMoney не передаёт «memo» или произвольный комментарий от отправителя при проверке. Вместо этого используется механизм label — уникальная метка, которую вы задаёте при создании ссылки на оплату.
Схема работы
1. Генерируете ссылку на оплату с уникальным label (например, order-123)
2. Отправитель переходит по ссылке и оплачивает
3. Проверяете платёж одним из двух способов:
а) Поллинг — периодически запрашиваете историю с фильтром по label
б) Вебхук — YooMoney отправляет POST на ваш сервер при поступлении переводаСпособ 1: Поллинг (простой)
import { YooMoneyClient, generatePaymentLink } from "@fw42/yoomoney";
const client = new YooMoneyClient({ token: "..." });
const label = `order-${Date.now()}`;
// Генерируем ссылку
const link = generatePaymentLink({
receiver: "4100118425529732",
sum: 100,
label,
});
console.log("Отправьте пользователю:", link);
// Ждём оплату (поллинг каждые 5 секунд, таймаут 5 минут)
// amount: 100 — SDK автоматически проверит что сумма >= 100
try {
const ops = await client.waitForPayment(label, {
timeoutMs: 300_000,
intervalMs: 5_000,
amount: 100,
});
console.log("Оплата получена!", ops[0].amount);
} catch {
console.log("Таймаут — оплата не поступила");
}Способ 2: Вебхук (мгновенный)
YooMoney отправляет HTTP POST на ваш сервер при каждом входящем переводе.
- Настройте Notification URL в настройках приложения
- Обрабатывайте уведомления:
import {
parseNotification,
verifyNotificationSignature,
} from "@fw42/yoomoney";
// В вашем HTTP-сервере (Express, Hono, Bun.serve и т.д.)
// requestBody — СЫРАЯ строка тела POST (не парсенная).
async function handleWebhook(requestBody: string) {
// Рекомендуется проверять подпись по СЫРОМУ телу — это гарантирует,
// что подпись вычислена ровно для того набора полей, что прислал YooMoney.
const isValid = await verifyNotificationSignature(
requestBody,
"ваш_секрет_из_настроек_уведомлений",
);
if (!isValid) {
return { status: 403, body: "Invalid signature" };
}
// После проверки подписи — парсим и используем.
const notification = parseNotification(requestBody);
console.log(`Получен платёж: ${notification.amount} руб.`);
console.log(`Label: ${notification.label}`);
console.log(`От: ${notification.sender}`);
return { status: 200, body: "OK" };
}⚠️ Идемпотентность: ЮMoney делает до 3 попыток доставки одного и того же уведомления, если не получил
200 OKв ответ (сразу, через 10 минут, через час). SDK не дедуплицирует уведомления сам — если ваш обработчик необратимо меняет состояние (зачисляет баланс, выполняет заказ), проверяйтеoperation_idна повтор перед обработкой (например, по уникальному индексу в БД), иначе временный сбой ответа на первую попытку может привести к двойной обработке одного платежа.
Куда приходит label и как его получить
Label привязывается к операции автоматически когда отправитель оплачивает по вашей ссылке. Он попадает в историю операций вашего кошелька — того, на который пришёл перевод. Отдельный сервер для этого не нужен.
Вы генерируете ссылку с label → отправитель платит →
в истории ВАШЕГО кошелька появляется операция с этим label →
вы запрашиваете историю с фильтром по label и находите еёПолучить label можно двумя способами:
checkPaymentByLabel(label, opts?)— запрос кoperation-historyс фильтромlabel. По умолчанию возвращает только успешные операции (status === "success"). С опциейamount— только те, где сумма >= указанной.getOperationDetails({ operation_id })— в ответе будет полеlabel, если оно было задано при оплате.
Отправитель не видит label и не может его изменить — он зашит в ссылку/форму оплаты.
Как идентифицировать кто заплатил без memo
YooMoney не поддерживает произвольное «memo» от отправителя. Вместо этого:
Label (рекомендуемый) — ваш главный инструмент. При генерации ссылки задаёте уникальный label (например
user-42-topupилиorder-abc). Этот label привязан к ссылке и возвращается при проверке платежа — как через поллингoperation-history, так и через вебхук.Sender — номер кошелька отправителя (приходит в
operation-detailsи в notification). Если отправитель платит с карты — поле пустое.Amount — если каждому пользователю выставлять уникальную сумму (например, +0.01 * userId), можно идентифицировать по сумме. Ненадёжный метод, только как запасной.
Рекомендуемый паттерн: каждому пользователю генерируете уникальную ссылку со своим label. Проверяете по label через waitForPayment() или checkPaymentByLabel() — это 100% надёжно и не требует вебхуков или сервера.
⚠️ Безопасность: Всегда указывайте ожидаемую сумму при проверке платежа! Пользователь может изменить
sumв URL оплаты и отправить меньше. SDK автоматически отсеет операции с суммой < ожидаемой.// Небезопасно — примет любую сумму: await client.checkPaymentByLabel("order-42"); // Безопасно — проверит что amount >= 1000: await client.checkPaymentByLabel("order-42", { amount: 1000 });
Примеры
Node.js (tsx)
YOOMONEY_TOKEN=<token> npx tsx examples/get-account-info.ts
YOOMONEY_TOKEN=<token> npx tsx examples/get-history.ts
YOOMONEY_TOKEN=<token> npx tsx examples/get-details.ts <operation_id>
YOOMONEY_TOKEN=<token> npx tsx examples/check-payment.ts <label>
npx tsx examples/generate-link.tsBun
YOOMONEY_TOKEN=<token> bun examples/get-account-info.ts
YOOMONEY_TOKEN=<token> bun examples/get-history.ts
YOOMONEY_TOKEN=<token> bun examples/get-details.ts <operation_id>
YOOMONEY_TOKEN=<token> bun examples/check-payment.ts <label>
bun examples/generate-link.tsТесты
# Node.js
npm test
# Bun
bun testAPI
new YooMoneyClient(options)
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| token | string | — | OAuth-токен (обязательный) |
| baseUrl | string | https://yoomoney.ru | Базовый URL |
| timeout | number | 10000 | Таймаут запроса в мс |
| maxRetries | number | 3 | Количество ретраев на 429 / 5xx / сетевые ошибки |
| retryBaseDelay | number | 500 | Базовая задержка для экспоненциального backoff в мс |
Методы клиента
| Метод | Описание |
|---|---|
| getAccountInfo() | Информация об аккаунте |
| getOperationHistory(params?) | Страница истории операций |
| getOperationHistoryAll(params?) | AsyncGenerator по всей истории |
| getOperationDetails({ operation_id }) | Детали операции |
| checkPaymentByLabel(label, opts?) | Проверка входящего платежа по лейблу с валидацией суммы и статуса |
| getRecentOperations(count?) | Последние N операций |
| waitForPayment(label, opts?) | Поллинг до появления платежа с лейблом (поддерживает amount и requireSuccess) |
CheckPaymentOptions
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| amount | number | — | Ожидаемая сумма (сколько вы хотите получить на кошелёк). Если не указана — open-ended режим: принимается любой платёж с подходящим label. |
| requireSuccess | boolean | true | Отсеивает операции с status !== "success" |
| feePayer | "sender" \| "receiver" | "sender" | Кто платит комиссию YooMoney (бизнес-логика SDK, не API). См. таблицу ниже. |
| ignoreFee | boolean | false | Если true, точное сравнение op.amount >= amount без учёта комиссии. |
Семантика проверки суммы (используется только op.amount — это единственное поле, доступное через operation-history):
| amount задан? | ignoreFee | feePayer | Пороговое значение |
|---|---|---|---|
| нет (open-ended) | — | — | ничего не проверяется, принимается любой входящий платёж с этим label |
| да | true | игнорируется | op.amount >= amount (точное сравнение, без всякого допуска) |
| да | false | "sender" (по умолчанию) | op.amount >= amount - ROUNDING_TOLERANCE |
| да | false | "receiver" | op.amount >= amount * (1 - MAX_FEE_RATE) - ROUNDING_TOLERANCE |
Почему 3%? Это максимальная комиссия YooMoney (для банковских карт). Переводы между кошельками дешевле (0.5%), но 3% — безопасный worst-case для любого способа оплаты. Константа
MAX_FEE_RATE = 0.03экспортируется из SDK.
ROUNDING_TOLERANCE = 0.01(1 копейка, экспортируется из SDK) — покрывает округление до копейки при вычисленииsumи комиссии YooMoney. ПриfeePayer: "sender"generatePaymentLinkсчитаетsumточной обратной формулой (amount / (1 - MAX_FEE_RATE), а не приближениемamount * (1 + MAX_FEE_RATE)), поэтому при реальной комиссии ровно 3% зачисленная сумма математически всегда равнаamount— допуск нужен только как запас на округление, а не потому что расхождение действительно ожидается.
feePayerвgeneratePaymentLinkи вcheckPaymentByLabelдолжны совпадать! Иначе логика сломается: например, если ссылка создана сfeePayer="sender"(отправитель платит сверху), а проверка идёт сfeePayer="receiver"(допускаем недоплату 3%), то недобросовестный плательщик сможет заплатить меньше.
WaitForPaymentOptions (extends CheckPaymentOptions)
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| timeoutMs | number | 300000 | Общий таймаут поллинга в мс |
| intervalMs | number | 5000 | Интервал между запросами в мс (минимум 1000) |
| amount, requireSuccess, feePayer, ignoreFee | — | — | Любые поля из CheckPaymentOptions |
PaymentLinkParams
| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| receiver | string | — | Номер кошелька получателя (обязательный) |
| sum | number | — | Сумма, которую отправитель должен заплатить. Опциональная — если опустить, будет создана open-ended ссылка. Если указан feePayer, SDK автоматически скорректирует sum (см. ниже). |
| paymentType | "PC" \| "AC" | — | Метод оплаты: PC = кошелёк YooMoney, AC = банковская карта |
| label | string | — | Уникальный идентификатор платежа (до 64 символов) |
| successURL | string | — | URL для редиректа после успешной оплаты |
| feePayer | "sender" \| "receiver" | — | Кто платит комиссию. Если задан, SDK корректирует sum: "sender" → sum / (1 - MAX_FEE_RATE), "receiver" → sum без изменений |
Корректировка sum в зависимости от feePayer:
| feePayer | sum в ссылке | Что произойдёт |
|------------|----------------|----------------|
| не задан | как передали | YooMoney сам решает (по умолчанию комиссия вычитается из sum) |
| "sender" | sum / (1 - MAX_FEE_RATE) (≈ sum * 1.0309) | Отправитель платит на ~3% больше, получатель получает ровно sum (с точностью до копейки) |
| "receiver" | sum | Отправитель платит sum, получатель получает sum - 3% |
sum * (1 + MAX_FEE_RATE)выглядит как очевидная обратная формула, но это лишь приближение первого порядка — комиссия берётся с уже увеличенной суммы, а не с исходной. Точная формула —sum / (1 - MAX_FEE_RATE). Разница между ними растёт пропорционально сумме (≈0.09% отsumпри 3%): незаметно на маленьких тестовых суммах, но на 1000 ₽ это уже ~90 копеек недостачи.
Примеры использования
Комиссию платит отправитель (рекомендуемый сценарий для фиксированной суммы)
// Хотим получить 500 ₽. SDK автоматически сделает sum = 500 / 0.97 = 515.46.
const link = generatePaymentLink({
receiver: "4100118425529732",
sum: 500,
feePayer: "sender",
label: "order-42",
paymentType: "AC",
});
// Проверяем, что получатель получил ровно 500 ₽ (с точностью до ROUNDING_TOLERANCE)
const result = await client.checkPaymentByLabel("order-42", {
amount: 500,
feePayer: "sender", // важно: то же значение, что и в generatePaymentLink
});Комиссию платит получатель (юзер платит ровно указанную сумму)
// Юзер заплатит ровно 500 ₽. Получатель получит ~485 ₽ (500 - 3%).
const link = generatePaymentLink({
receiver: "4100118425529732",
sum: 500,
feePayer: "receiver",
label: "order-42",
});
// Принимаем платежи, где получатель получил >= 500 * 0.97 - ROUNDING_TOLERANCE ≈ 484.99 ₽
const result = await client.checkPaymentByLabel("order-42", {
amount: 500,
feePayer: "receiver",
});Игнорировать комиссию (точное сравнение, без допуска)
// Полезно для переводов между кошельками YooMoney (там комиссия 0.5%,
// но вы хотите точно знать, что получатель получил >= 500 ₽).
const result = await client.checkPaymentByLabel("order-42", {
amount: 500,
ignoreFee: true,
});Свободное пополнение баланса (open-ended)
// Создаём ссылку без sum — юзер введёт любую сумму
const link = generatePaymentLink({
receiver: "4100118425529732",
label: "topup-user-42",
});
// Проверяем, что пришёл любой платёж с этим label (сумма не важна)
const result = await client.checkPaymentByLabel("topup-user-42");
if (result.found) {
// result.operations[0].amount — сколько фактически зачислено
const credited = result.operations[0].amount;
console.log(`Зачислено: ${credited} ₽`);
}Ожидание платежа с поллингом
// Ждём до 10 минут, проверяя каждые 5 секунд
const ops = await client.waitForPayment("order-99", {
amount: 1000,
feePayer: "sender",
timeoutMs: 600_000,
intervalMs: 5_000,
});
console.log(`Платёж получен: ${ops[0].amount} ₽ зачислено`);Утилиты
| Функция | Описание |
|---|---|
| generatePaymentLink(params) | URL для оплаты через quickpay |
| generatePaymentForm(params, buttonText?) | HTML-форма для встраивания |
| parseNotification(body) | Парсинг тела вебхука |
| verifyNotificationSignature(input, secret) | Проверка HMAC-SHA256 подписи. input может быть сырой URL-encoded строкой, URLSearchParams или распарсенным IncomingNotification. Рекомендуется передавать сырую строку — это гарантирует, что подпись будет вычислена для того же набора полей, что прислал YooMoney. |
Подпись уведомлений: SDK реализует алгоритм HMAC-SHA256 согласно официальной документации YooMoney: удаляет поле
sign, сортирует оставшиеся параметры по алфавиту, применяет URL-кодирование (RFC 3986), объединяет вkey=value&key=value...(пустые значения какkey=), вычисляет HMAC-SHA256 и сравнивает сsign(hex, lowercase) с использованием constant-time сравнения.
Обработка ошибок
Ошибки валидации параметров и HTTP-статус-ошибки — экземпляры YooMoneyError (или её подкласса YooMoneyHttpError), с полем code, по которому удобно ветвить логику. Но не все ошибки SDK обёрнуты: если fetch() сам падает (обрыв сети, DNS, таймаут по AbortController) и все maxRetries попыток исчерпаны, наружу летит сырая исходная ошибка (TypeError/DOMException и т.п.), не YooMoneyError — это учтено в примере ниже веткой else.
import { YooMoneyError, YooMoneyHttpError } from "@fw42/yoomoney";
try {
await client.waitForPayment(label, { amount: 500, timeoutMs: 30_000 });
} catch (err) {
if (err instanceof YooMoneyHttpError) {
// err.statusCode, err.statusText — стабильный HTTP-статус после исчерпания ретраев
console.error(`HTTP ${err.statusCode}: ${err.statusText}`);
} else if (err instanceof YooMoneyError && err.code === "timeout") {
console.log("Платёж не пришёл за отведённое время");
} else if (err instanceof YooMoneyError) {
console.error(`Ошибка API: ${err.code} — ${err.message}`);
} else {
// Сетевая ошибка (fetch упал, ретраи исчерпаны) — НЕ YooMoneyError.
console.error("Сетевая ошибка:", err);
}
}YooMoneyHttpError extends YooMoneyError и всегда имеет code === "http_error", плюс statusCode/statusText. Бросается методом post() после исчерпания maxRetries на ответах, не являющихся response.ok (например, стабильный 401/403/5xx).
Значения code у YooMoneyError, которые бросает сам SDK (валидация параметров, до похода в API):
| code | Откуда | Причина |
|---|---|---|
| invalid_token | new YooMoneyClient() | token не передан или не строка |
| invalid_label | checkPaymentByLabel, waitForPayment, generatePaymentLink, generatePaymentForm | label пустой или длиннее 64 символов |
| invalid_amount | checkPaymentByLabel, waitForPayment | amount не положительное конечное число |
| invalid_fee_payer | checkPaymentByLabel, waitForPayment, generatePaymentLink, generatePaymentForm | feePayer — не "sender"/"receiver" |
| invalid_interval | waitForPayment | intervalMs < 1000 |
| timeout | waitForPayment | Платёж с этим label не найден за timeoutMs |
| illegal_param_operation_id | getOperationDetails | operation_id не передан |
| invalid_content_type | внутренний post() | YooMoney ответил не application/json (обычно значит проблему с URL/окружением, не с данными) |
| invalid_params / invalid_receiver / invalid_sum | generatePaymentLink, generatePaymentForm | Невалидные PaymentLinkParams (нет receiver, sum не положительное число и т.п.) |
Отдельно: если сам YooMoney API вернул {"error": "..."} (например, illegal_param_operation_id от самого API, а не от SDK-валидации), SDK пробрасывает это значение как code без изменений — так что err.code может содержать и коды ошибок YooMoney напрямую, не только перечисленные выше.
Сетевые сбои (обрыв соединения, таймаут по AbortController) после исчерпания maxRetries пробрасываются как есть, не оборачиваются в YooMoneyError — см. пример выше.
Лицензия
MIT
