npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

novapay

v3.0.1

Published

NovaPay Internet Acquiring and Checkout API client for Node.js

Readme

NovaPay Node.js Library

CI npm install size license

TypeScript-клієнт для зовнішнього API NovaPay — Internet Acquiring і Checkout. Запити підписуються автоматично, postback-и перевіряються автоматично, усі payload-и типізовані.

📖 Documentation in English

Зміст · Вимоги · Встановлення · Швидкий старт · 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
                                                                       ▼
                                                                     voided

expireSession переводить неоплачену сесію в 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.

Ліцензія

MIT