Інтеграція з NOWPayments
NOWPayments — hosted платежний шлюз, який обробляє генерацію адрес, моніторинг блокчейну та конвертацію курсів за вас. Правильна інтеграція займає 2–3 дні, але є кілька підводних каменів: верифікація IPN підписів, обробка часткових платежів та ідемпотентна обробка 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.
Статуси та ідемпотентна обробка
NOWPayments присилає webhook при кожній зміні статусу. Одні й ті ж статуси можуть прийти кілька разів (retry при недоступності вашого сервера).
type PaymentStatus =
| 'waiting' // ожидаємо оплату
| 'confirming' // транзакція знайдена, чекаємо confirmations
| 'confirmed' // підтверджено
| 'sending' // NOWPayments конвертує та відправляє
| 'partially_paid' // отримана неповна сума
| 'finished' // успішно завершено
| 'failed' // помилка
| 'refunded' // повернення
| 'expired'; // istik час ожидания
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
-
Alert на
partially_paid— потребує ручного рішення: прийняти, запросити доплату або повернути







