YooKassa (ЮKassa) Integration Guide for Node.js and TypeScript (2026)
YooKassa (formerly Yandex.Kassa) is the leading payment aggregator in Russia. One contract gives you: bank cards, SBP (Fast Payment System), SberPay, T-Pay, and cash terminals. This guide covers the full integration in Node.js/TypeScript, including 54-FZ fiscal receipt generation.
What YooKassa Covers
| Payment Method | Notes |
|---|---|
| Visa / Mastercard / Mir | Russian-issued cards work without restrictions |
| SBP (Система быстрых платежей) | Mandatory for most businesses since 2024 |
| SberPay | High conversion for Sber customers |
| T-Pay (Тинькофф) | Popular among younger users |
| YooMoney wallet | Digital wallet |
| Cash via terminals | QIWI, Euroset, etc. |
Setup
npm install yookassa
# or use the REST API directly - no SDK required
Get your credentials from the YooKassa merchant portal:
shopId- your merchant IDsecretKey- API secret key
Test environment: set shopId to 100500 and secretKey to test_<anything> for sandbox.
Basic Payment Flow
1. Your backend creates a payment → YooKassa returns payment object with confirmationUrl
2. Redirect user to confirmationUrl (or use embedded widget)
3. User pays
4. YooKassa sends webhook to your server
5. Your server verifies payment → fulfills order
Step 1: Create a Payment
// payment.service.ts
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
const YOOKASSA_SHOP_ID = process.env.YOOKASSA_SHOP_ID!;
const YOOKASSA_SECRET_KEY = process.env.YOOKASSA_SECRET_KEY!;
const BASE_URL = 'https://api.yookassa.ru/v3';
const yooKassaClient = axios.create({
baseURL: BASE_URL,
auth: {
username: YOOKASSA_SHOP_ID,
password: YOOKASSA_SECRET_KEY,
},
});
interface CreatePaymentOptions {
amount: number; // In rubles (e.g. 1500 = 1500 ₽)
orderId: string;
description: string;
returnUrl: string;
customerEmail?: string;
customerPhone?: string;
metadata?: Record<string, string>;
}
async function createPayment(options: CreatePaymentOptions) {
const idempotenceKey = uuidv4(); // Unique per request - prevents duplicates on retry
const response = await yooKassaClient.post(
'/payments',
{
amount: {
value: options.amount.toFixed(2),
currency: 'RUB',
},
confirmation: {
type: 'redirect',
return_url: options.returnUrl,
},
capture: true, // Auto-capture payment (false = two-step authorization)
description: options.description,
metadata: {
order_id: options.orderId,
...options.metadata,
},
receipt: options.customerEmail || options.customerPhone
? buildReceipt(options) // Required for 54-FZ
: undefined,
},
{
headers: { 'Idempotence-Key': idempotenceKey },
}
);
return response.data as YooKassaPayment;
}
54-FZ Fiscal Receipts (Required for B2C)
If you accept payments from individuals (not companies), Russian law (54-FZ) requires sending a fiscal receipt. YooKassa can send it automatically:
function buildReceipt(options: CreatePaymentOptions) {
return {
customer: {
email: options.customerEmail,
phone: options.customerPhone, // Format: +79001234567
},
items: [
{
description: options.description,
quantity: '1.00',
amount: {
value: options.amount.toFixed(2),
currency: 'RUB',
},
vat_code: 1, // 1 = НДС не применяется (most common for services)
payment_mode: 'full_payment',
payment_subject: 'service', // or 'commodity' for physical goods
},
],
tax_system_code: 2, // 1=ОСН, 2=УСН доходы, 3=УСН доходы-расходы, etc.
};
}
VAT codes:
1- НДС не облагается (most services, simplified tax system)2- НДС 0%5- НДС 20%
Step 2: Webhook Handler
// webhook.controller.ts
import { Request, Response } from 'express';
import crypto from 'crypto';
export async function handleYooKassaWebhook(req: Request, res: Response) {
const event = req.body as YooKassaWebhookEvent;
// YooKassa recommends IP whitelisting instead of signature verification
// Allowed IPs: 185.71.76.0/27, 185.71.77.0/27, 77.75.153.0/25, etc.
// Full list: https://yookassa.ru/developers/using-api/webhooks
switch (event.event) {
case 'payment.succeeded':
await handlePaymentSucceeded(event.object);
break;
case 'payment.canceled':
await handlePaymentCanceled(event.object);
break;
case 'refund.succeeded':
await handleRefundSucceeded(event.object);
break;
}
// Always return 200 - otherwise YooKassa retries for 24 hours
res.status(200).json({ ok: true });
}
async function handlePaymentSucceeded(payment: YooKassaPayment) {
const orderId = payment.metadata.order_id;
await db.orders.update({
where: { id: orderId },
data: {
status: 'paid',
paidAt: new Date(),
yookassaPaymentId: payment.id,
paymentMethod: payment.payment_method.type,
},
});
await fulfillOrder(orderId);
}
Verify Payment on Return URL
async function getPaymentStatus(paymentId: string) {
const response = await yooKassaClient.get(`/payments/${paymentId}`);
return response.data as YooKassaPayment;
}
// Return URL handler
app.get('/payment/return', async (req, res) => {
const paymentId = req.query.paymentId as string;
if (!paymentId) return res.redirect('/cart');
const payment = await getPaymentStatus(paymentId);
if (payment.status === 'succeeded') {
res.render('success');
} else if (payment.status === 'pending') {
// Payment processing - show waiting screen, poll or wait for webhook
res.render('processing', { paymentId });
} else {
res.render('failed');
}
});
Refunds
async function createRefund(paymentId: string, amount?: number) {
const payment = await getPaymentStatus(paymentId);
const response = await yooKassaClient.post(
'/refunds',
{
payment_id: paymentId,
amount: {
// Full refund if amount not specified
value: amount ? amount.toFixed(2) : payment.amount.value,
currency: 'RUB',
},
},
{
headers: { 'Idempotence-Key': uuidv4() },
}
);
return response.data;
}
Split Payments (for Marketplaces)
Split payments let you receive a platform fee while routing the rest to the seller. Requires connecting sellers to your platform via YooKassa self-employed / marketplace functionality.
async function createSplitPayment(options: CreatePaymentOptions & { sellerId: string }) {
const platformFee = options.amount * 0.10; // 10% platform commission
const sellerAmount = options.amount - platformFee;
const response = await yooKassaClient.post('/payments', {
amount: { value: options.amount.toFixed(2), currency: 'RUB' },
confirmation: { type: 'redirect', return_url: options.returnUrl },
capture: true,
description: options.description,
transfers: [
{
account_id: options.sellerId, // Seller's YooKassa account ID
amount: {
value: sellerAmount.toFixed(2),
currency: 'RUB',
},
platform_fee_amount: {
value: platformFee.toFixed(2),
currency: 'RUB',
},
},
],
}, {
headers: { 'Idempotence-Key': uuidv4() },
});
return response.data;
}
TypeScript Types
interface YooKassaPayment {
id: string;
status: 'pending' | 'waiting_for_capture' | 'succeeded' | 'canceled';
amount: { value: string; currency: string };
description: string;
payment_method: { type: 'bank_card' | 'sbp' | 'sberbank' | 'tinkoff_bank' };
metadata: Record<string, string>;
created_at: string;
captured_at?: string;
test: boolean;
}
interface YooKassaWebhookEvent {
type: 'notification';
event: 'payment.succeeded' | 'payment.canceled' | 'refund.succeeded';
object: YooKassaPayment;
}
SBP (Fast Payment System) Specific Flow
SBP shows a QR code that users scan with their banking app:
async function createSBPPayment(options: CreatePaymentOptions) {
return yooKassaClient.post('/payments', {
amount: { value: options.amount.toFixed(2), currency: 'RUB' },
payment_method_data: { type: 'sbp' }, // Force SBP method
confirmation: { type: 'qr' }, // Get QR code
capture: true,
description: options.description,
}, {
headers: { 'Idempotence-Key': uuidv4() },
});
// response.data.confirmation.confirmation_url - QR code image URL
}
Common Issues
| Issue | Solution |
|---|---|
Idempotence-Key already used |
Generate new UUID per retry |
| Webhook not received | Check IP whitelist, respond 200 always |
| Receipt rejected | Verify vat_code matches your tax system |
| SBP not showing | Enable SBP in merchant portal settings |
Payment stuck in pending |
Normal for SBP - wait up to 24h or cancel |