novapay
v3.0.1
Published
NovaPay Internet Acquiring and Checkout API client for Node.js
Maintainers
Readme
NovaPay Node.js Library
TypeScript-клієнт для зовнішнього API NovaPay — Internet Acquiring і Checkout. Запити підписуються автоматично, postback-и перевіряються автоматично, усі payload-и типізовані.
Зміст · Вимоги · Встановлення · Швидкий старт · Postback-и · API · Конфігурація · Помилки · Приклад застосунку · Розробка
Вимоги
- Node.js 20.3+ — використовує глобальний
fetch,AbortSignal.anyіnode:crypto - Збірки ESM і CommonJS, типи вбудовані
- Без runtime-залежностей, а опублікованим типам не потрібен
@types/node
Встановлення
npm install novapayШвидкий старт
import { createClient, NovaPayEnvironment } from 'novapay';
const client = createClient({
privateKeyPem: process.env.MERCHANT_PRIVATE_KEY_PEM!,
novapayPublicKeyPem: process.env.NOVAPAY_PUBLIC_KEY_PEM!,
environment: NovaPayEnvironment.Production,
merchantId: '<ваш-merchant-id>',
});
const session = await client.acquiring.createSession({
client_phone: '+380501112233',
callback_url: 'https://your.api/novapay/postback',
});
const payment = await client.acquiring.addPayment({
session_id: session.id,
amount: 100.5,
});
console.log('Редірект клієнта на:', payment.url);createSession повертає { id } — це і є ідентифікатор сесії для всіх наступних викликів.
Передайте use_hold: true у addPayment, щоб заблокувати кошти зараз і списати пізніше через completeHold.
Checkout
Той самий клієнт, той самий хост, інші шляхи. Checkout додатково збирає дані доставки.
const session = await client.checkout.createSession({
callback_url: 'https://your.api/novapay/checkout-postback',
client_phone: '+380501112233',
});
const payment = await client.checkout.addPayment({
session_id: session.id,
amount: 250,
});Postback-и
NovaPay підписує postback-и своїм публічним RSA-ключем (не ключем мерчанта) і передає підпис у заголовку x-sign-v2 — це інший заголовок, ніж x-sign на вихідних запитах.
Підпис рахується по сирому тілу запиту. Перевіряйте до парсингу:
JSON.stringify(req.body)не збігається з оригіналом і перевірка провалиться.
import express from 'express';
import { NovaPaySignatureError, WEBHOOK_HEADER_X_SIGN } from 'novapay';
const app = express();
app.use(express.json({ verify: (req, _res, buf) => ((req as any).rawBody = buf) }));
app.post('/novapay/postback', (req, res) => {
const xSign = req.get(WEBHOOK_HEADER_X_SIGN);
if (!xSign) return res.sendStatus(400);
try {
const postback = client.parsePostback((req as any).rawBody, xSign);
console.log(postback.id, postback.status); // `id` — це ідентифікатор сесії
} catch (err) {
if (err instanceof NovaPaySignatureError) return res.sendStatus(401);
throw err; // забутий ключ — це поламаний деплой, він не має відповідати 401
}
res.sendStatus(200);
});parsePostback спочатку перевіряє підпис і лише потім декодує — саме в такому порядку, щоб
перезібране тіло ніколи не виявилось тим, що ви перевіряли.
Форма payload-а залежить від версії постбеку — налаштування мерчанта на боці NovaPay.
v1 (за замовчуванням) — окремий POST на кожен платіж з external_id, amount і products
на верхньому рівні; v2 — один POST на сесію, платежі згруповані в payments[]. Вкажіть
клієнту версію вашого мерчанта — і parsePostback повертатиме відповідний тип:
import { PostbackVersion } from 'novapay';
const client = createClient({ privateKeyPem, merchantId, novapayPublicKeyPem, postbackVersion: PostbackVersion.v2 });
const postback = client.parsePostback(rawBody, xSign); // AcquiringPostbackV2Це впливає лише на типізацію — формат на дроті визначають налаштування NovaPay для вашого
мерчанта, тому передана postbackVersion має співпадати з postback_version, налаштованою
для вашого мерчанта на боці NovaPay (не впевнені яка — спитайте їхню підтримку). При розбіжності
типи описуватимуть поля, які ніколи не приходять.
Для checkout-postback передайте CheckoutPostbackV1 / CheckoutPostbackV2 як тип-аргумент:
const postback = client.parsePostback<CheckoutPostbackV2>(rawBody, xSign);
postback.delivery?.express_waybills;rawBody приймає string | Uint8Array, тому Buffer з будь-якого body-парсера підходить як є.
Ловіть NovaPaySignatureError і нічого ширшого. Інші два throw-и — не 401: NovaPayConfigError
означає, що ви не передали novapayPublicKeyPem, а SyntaxError — що NovaPay надіслав підписане
тіло, яке не є JSON. Відповідь 401 на будь-який із них ховає поламаний деплой під циклом ретраїв
NovaPay — хай буде 500 і хтось прокинеться.
Якщо потрібен тільки булевий результат, є verifyPostback(rawBody, xSign). Обидва кидають
NovaPayConfigError, якщо в createClient не передали novapayPublicKeyPem; при незбігу підпису
parsePostback кидає NovaPaySignatureError, а verifyPostback повертає false.
Перевірка без клієнта:
import { verifyPostbackSignature } from 'novapay';
const ok = verifyPostbackSignature(rawBody, xSign, process.env.NOVAPAY_PUBLIC_KEY_PEM!);Типи payload-ів: AcquiringPostbackV1 / AcquiringPostbackV2 та CheckoutPostbackV1 /
CheckoutPostbackV2 — названі за версією постбеку мерчанта.
API
Кожен метод приймає тіло запиту, необов'язкові RequestOptions і повертає проміс.
| Метод | Шлях | Повертає |
|---|---|---|
| createSession(body) | POST /v1/session | SessionCreateResponse |
| addPayment(body) | POST /v1/payment | SessionPaymentResponse |
| getStatus(body) | POST /v1/get-status | SessionStatusResponse |
| completeHold(body) | POST /v1/complete-hold | null |
| voidSession(body) | POST /v1/void | null |
| expireSession(body) | POST /v1/expire | null |
client.checkout має ті самі шість методів. createSession і addPayment ходять на /v1/checkout/*, решта — спільні з acquiring.
Дві відмінності checkout, про які варто знати:
checkout.addPaymentповертаєCheckoutPaymentResponse—{ url, session_id }безid. Ідентифікатор транзакції з'являється пізніше вgetStatus, уoperations[].transaction_id.- Сесія checkout стартує зі статусом
precreated, acquiring — зcreated.
completeHold, voidSession і expireSession не повертають даних — тіло відповіді це літеральний null. Вони спрацювали, якщо не кинули помилку. Побачити результат можна лише через getStatus.
Життєвий цикл сесії
createSession ──▶ created / precreated
│
клієнт оплачує
├── use_hold: true ──▶ holded ──completeHold──▶ paid
└── use_hold: false ─────────────────────────▶ paid
│
voidSession
▼
voidedexpireSession переводить неоплачену сесію в expired. voidSession працює як на звичайному списанні, так і на вже підтвердженому холді.
SessionStatusResponse
const s = await client.acquiring.getStatus({ session_id });
s.status; // 'created' | 'precreated' | 'holded' | 'paid' | 'voided' | 'expired'
s.transaction_status; // 'APPROVED' | 'REFUNDED' | null
s.amount; // '1.00' — десятковий рядок, не число
s.pan; // '424242xxxxxx4242' — маскована, null до оплати
s.paytype; // 'card'; порожній рядок (не null) до оплати
s.card_type; // 'VISA'
s.approval_code; // '1785182032.057'
s.operations; // [{ transaction_id, external_id, amount, refunded_amount, status }]Три речі, на яких можна обпектися:
- Суми — десяткові рядки (
'1.00'), не числа. Не порівнюйте їх через===з числом. refunded_amountлишаєтьсяnullнавіть після успішного void. Визначайте повернення поstatus === 'voided'абоtransaction_status === 'REFUNDED'.rrnлишивсяnullнавіть на оплаченій сесії, аprocessing_resultдо оплатиnull, після —''. Жодне з них не є надійним сигналом успіху — орієнтуйтесь наstatus.
status і transaction_status — відкриті union-и: відомі значення автодоповнюються, нерозпізнані все одно проходять перевірку типів, тож новий статус від NovaPay не зламає вашу збірку.
Типи запитів теж експортуються: CreateSessionRequest, AddPaymentRequest, CompleteHoldRequest, SessionIdRequest, CreateCheckoutSessionRequest, AddCheckoutPaymentRequest.
Конфігурація
createClient({
privateKeyPem, // обов'язково — приватний RSA-ключ мерчанта, підписує запити
merchantId, // обов'язково — йде як merchant_id у тіло кожного запиту
novapayPublicKeyPem, // публічний RSA-ключ NovaPay, перевіряє postback-и
environment, // NovaPayEnvironment.Test (за замовчуванням) | .Production
acquiringBaseUrl, // перевизначає хост (staging, моки)
checkoutBaseUrl, // перевизначає хост (staging, моки)
timeoutMs, // таймаут на запит за замовчуванням, 30_000
fetchFn, // власний fetch — проксі, інструментація, тести
postbackVersion, // PostbackVersion.v1 (за замовчуванням) | .v2 — тип payload-а parsePostback
});Обидва PEM-ключі парсяться тут, тому невалідний ключ кине помилку з createClient на старті, а не
під час першого реального платежу.
Опції на один виклик
Кожен метод приймає другим аргументом RequestOptions:
await client.acquiring.getStatus(
{ session_id },
{ signal: req.signal, timeoutMs: 5_000 },
);| Опція | Опис |
|---|---|
| signal | Скасування з боку викликача, комбінується з таймаутом — перериває той, що спрацював першим. Передайте сигнал запиту вашого сервера, щоб обривати вихідний виклик, коли клієнт закрив вкладку. |
| timeoutMs | Перевизначає timeoutMs клієнта лише для цього виклику. |
Acquiring і Checkout використовують спільний хост для кожного середовища:
| environment | Константа | Хост |
|---|---|---|
| NovaPayEnvironment.Test (за замовчуванням) | TEST_BASE_URL | https://api-qecom.novapay.ua |
| NovaPayEnvironment.Production | PRODUCTION_BASE_URL | https://api-ecom.novapay.ua |
getExternalApiBaseUrl(env) віддає хост поза клієнтом.
Тестові (QE) ключі — мерчант 2, його приватний ключ і публічний ключ NovaPay — опубліковані в
Автентифікація, тестові картки — у
Тестові дані. Продуктові ключі генеруються в адмін-панелі
Acquiring3 (інструкція).
Про ключі в змінних середовища: PEM — багаторядковий. У .env або візьміть значення в лапки зі справжніми переносами рядків, або зберігайте у base64 і декодуйте на старті — PEM з літеральними \n не розпарситься.
Помилки
Усе, що кидає SDK, наслідує NovaPayError, тому один instanceof ловить будь-яку помилку. Нижче дерево розгалужується за тим, що з помилкою реально можна зробити:
NovaPayError
├── NovaPayConfigError виклик неправильний — битий PEM, забута опція. Правити код.
├── NovaPaySignatureError postback не збігся з x-sign-v2. Відхилити запит.
└── NovaPayApiError NovaPay відповів не-2xx.
├── NovaPayProcessingError задокументована бізнесова відмова.
└── NovaPayValidationError тіло не пройшло валідацію схеми.import {
NovaPayApiError,
NovaPayProcessingError,
NovaPayValidationError,
} from 'novapay';
try {
await client.acquiring.addPayment({ /* … */ });
} catch (err) {
if (err instanceof NovaPayValidationError) {
// Ви передали щось не те. Повторювати не можна.
console.error(err.paths, err.uuid); // ['client_phone'], 'e7638147-…'
} else if (err instanceof NovaPayProcessingError) {
// NovaPay відмовив в операції. Розгалужуйтесь по коду.
if (err.code === 'SessionAlreadyRefundedError') return;
console.error(err.code, err.error, err.uuid);
} else if (err instanceof NovaPayApiError) {
// 5xx, HTML від шлюзу, будь-що незадокументоване.
console.error(err.status, err.responseBody);
}
throw err;
}Перевіряйте підкласи перед NovaPayApiError — вони його наслідують, тому сам instanceof NovaPayApiError матчить усі три.
NovaPayApiError — будь-яка відповідь не-2xx:
| Властивість | Опис |
|---|---|
| status | HTTP-код відповіді |
| responseJson | Розпарсене тіло, або null якщо це був не JSON |
| responseBody | Сирий текст відповіді |
NovaPayProcessingError — коректний запит, відхилений з бізнесової причини:
| Властивість | Опис |
|---|---|
| code | 'SessionNotFoundError' | 'SessionAlreadyRefundedError' | 'NotFoundError' | … — розгалужуйтесь по ньому |
| error | Текст для людини, напр. 'session already refunded'. Не контракт |
| description | Додаткова деталь, часто порожня |
| uuid | Серверний id для кореляції — вказуйте в зверненнях до підтримки |
NovaPayValidationError — тіло запиту не пройшло валідацію:
| Властивість | Опис |
|---|---|
| errors | [{ path: 'client_phone', code: 'invalid_type', message: '…' }] |
| paths | Тільки імена відхилених полів, напр. ['client_phone'] |
| uuid | Серверний id для кореляції |
Підклас видається лише тоді, коли поля, які він обіцяє, реально є у відповіді. Обрізане чи хибно помічене 4xx залишиться звичайним NovaPayApiError, а не віддасть вам code: undefined — тому err.code ніколи не бреше.
uuid свідомо не входить у err.message: агрегатори логів групують по тексту помилки, і id запиту всередині дав би одну групу на кожен запит замість однієї на кожен тип.
NovaPayConfigError означає поламаний деплой, а не невдалий платіж. Битий PEM кидається на етапі createClient, до будь-якого запиту; відсутній novapayPublicKeyPem виявиться лише на першому виклику verifyPostback/parsePostback — до нього ключ нікому не потрібен.
NovaPaySignatureError кидає тільки parsePostback, і він означає рівно одне: цей postback не підписаний NovaPay. Відповідайте 401 і викидайте його.
Скасування відхиляє проміс, а не резолвить: таймаут timeoutMs кидає TimeoutError, а переданий signal — свою причину скасування (за замовчуванням AbortError). Обидва — DOMException, а не NovaPayApiError: запит до NovaPay не дійшов.
Друге, що лежить поза деревом NovaPayError, — власний SyntaxError від JSON.parse у parsePostback: після перевірки підпису байти доказово належать NovaPay, тому класифікувати тут уже нічого — помилки мерчанта в цьому місці бути не може.
Автоматичних ретраїв немає. addPayment не ідемпотентний — сліпий повтор може списати з клієнта двічі. При таймауті чи 5xx спочатку викличте getStatus, щоб дізнатися, що реально сталося.
Приклад застосунку
Робочий магазин на Express + hbs — кнопки hold і звичайного списання, сторінки success/fail, підписані postback-и та список покупок із completeHold / voidSession:
npm run exampleНалаштування ngrok — у example/README.md.
Розробка
npm test # vitest
npm run typecheck # tsc --noEmit
npm run lint # biome
npm run build # tsup → dist/Issues і pull request-и: github.com/NovaPay/novapay-node.
Довідник API
Повні списки полів, значення статусів і схеми postback-ів: NovaPay API Reference.
