Интеграция с NOWPayments
NOWPayments — hosted платёжный шлюз, который обрабатывает генерацию адресов, мониторинг блокчейна и конвертацию курсов за вас. Правильная интеграция занимает 2–3 дня, но есть несколько нюансов, на которых теряют время: верификация IPN подписей, обработка частичных платежей и idempotent обработка webhook'ов.
Flow платежа
1. Ваш backend → POST /v1/payment → NOWPayments
Получаете: payment_id, pay_address, pay_amount, expiration_estimate_date
2. Показываете клиенту QR-код и адрес для оплаты
3. NOWPayments мониторит блокчейн
4. NOWPayments → IPN Webhook → Ваш backend
payment_status: waiting → confirming → finished/failed/expired
5. Ваш backend верифицирует подпись, обновляет заказ
Создание платежа
interface CreatePaymentRequest {
price_amount: number; // сумма в price_currency
price_currency: string; // 'usd', 'eur'
pay_currency: string; // 'btc', 'eth', 'usdterc20', 'usdttrc20'
order_id: string; // ваш внутренний ID
order_description?: string;
ipn_callback_url: string; // URL для webhook
success_url?: string;
cancel_url?: string;
}
async function createPayment(
orderData: CreatePaymentRequest
): Promise<NOWPaymentsPayment> {
const response = await fetch('https://api.nowpayments.io/v1/payment', {
method: 'POST',
headers: {
'x-api-key': process.env.NOWPAYMENTS_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify(orderData),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`NOWPayments error: ${error.message}`);
}
return response.json();
}
Обратите внимание на pay_currency — это не просто название монеты, а конкретная монета в конкретной сети. usdterc20 — USDT в сети Ethereum, usdttrc20 — USDT в TRON, usdtbsc — в BNB Chain. Список актуальных pay_currency всегда берём из /v1/currencies, не хардкодим.
Верификация IPN подписи — критично
NOWPayments подписывает каждый webhook HMAC-SHA512 вашим IPN-ключом (отдельный от API ключа). Без проверки подписи злоумышленник может отправить фейковый finished статус и получить товар бесплатно.
import * as crypto from 'crypto';
function verifyIPNSignature(
payload: string, // raw request body, не распарсенный
receivedSignature: string,
ipnSecret: string
): boolean {
const hmac = crypto.createHmac('sha512', ipnSecret);
hmac.update(payload);
const computedSignature = hmac.digest('hex');
// Константное время сравнения — защита от timing attacks
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(receivedSignature)
);
}
// Express middleware
app.post('/webhook/nowpayments',
express.raw({ type: 'application/json' }), // raw body!
(req, res) => {
const signature = req.headers['x-nowpayments-sig'] as string;
if (!verifyIPNSignature(
req.body.toString(),
signature,
process.env.NOWPAYMENTS_IPN_SECRET!
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payment = JSON.parse(req.body.toString());
handlePaymentUpdate(payment);
res.status(200).json({ ok: true });
}
);
Важно: для HMAC верификации нужен raw body. Если middleware express.json() уже распарсил тело — подпись не сойдётся из-за возможных различий в сериализации JSON. Используйте express.raw() для webhook endpoint.
Статусы и idempotent обработка
NOWPayments присылает webhook при каждом изменении статуса. Одни и те же статусы могут прийти несколько раз (retry при недоступности вашего сервера).
type PaymentStatus =
| 'waiting' // ожидаем оплату
| 'confirming' // транзакция найдена, ждём confirmations
| 'confirmed' // подтверждено
| 'sending' // NOWPayments конвертирует и отправляет
| 'partially_paid' // получена неполная сумма
| 'finished' // успешно завершено
| 'failed' // ошибка
| 'refunded' // возврат
| 'expired'; // истёк срок ожидания
async function handlePaymentUpdate(data: IPNPayload): Promise<void> {
// Idempotency: проверяем, не обрабатывали ли уже
const existing = await db.query(
'SELECT status FROM payments WHERE nowpayments_id = $1',
[data.payment_id]
);
if (existing.rows[0]?.status === 'finished') {
return; // Уже обработано, игнорируем
}
await db.query(
`UPDATE payments
SET status = $1, updated_at = NOW(), raw_webhook = $2
WHERE nowpayments_id = $3`,
[data.payment_status, JSON.stringify(data), data.payment_id]
);
if (data.payment_status === 'finished') {
await fulfillOrder(data.order_id);
}
if (data.payment_status === 'partially_paid') {
await notifyPartialPayment(data.order_id, data.actually_paid, data.pay_amount);
}
}
Песочница для тестирования
NOWPayments предоставляет sandbox: https://api-sandbox.nowpayments.io. Отдельные API ключи, тестовые транзакции не уходят в реальные сети. Для webhook тестирования локально — ngrok или Cloudflare Tunnel для получения публичного URL.
# Тест через curl
curl -X POST https://api-sandbox.nowpayments.io/v1/payment \
-H "x-api-key: YOUR_SANDBOX_KEY" \
-H "Content-Type: application/json" \
-d '{"price_amount":10,"price_currency":"usd","pay_currency":"btc","order_id":"test-001","ipn_callback_url":"https://your-ngrok-url/webhook/nowpayments"}'
Что стоит реализовать дополнительно
-
Polling как fallback: если webhook не пришёл в течение 30 минут после создания платежа — опрашиваем
/v1/payment/{id}сами -
Хранение
payment_idот NOWPayments в вашей таблице заказов — нужен для reconciliation - Логирование всех сырых webhook payload — помогает при debugging и disputes
-
Алерт на
partially_paid— требует ручного решения: принять, запросить доплату или вернуть







