Przejdź do głównej zawartości

Node.js SDK

Dostępne - wersja 0.1.0

Oficjalna biblioteka Node.js do integracji z API dpay.pl. Automatyzuje generowanie sum kontrolnych, weryfikację powiadomień IPN oraz wywołania API: rejestrację płatności, szczegóły transakcji, zwroty, listę banków, aliasy BLIK, płatności kartowe server-to-server i wypłaty 1:1. Kwoty reprezentuje obiekt Money (grosze jako liczba całkowita), a odpowiedzi API mapowane są na typowane obiekty. Napisana w TypeScript z pełnymi typami, dystrybuowana jako ESM i CommonJS, bez żadnych zależności runtime. Wszystkie metody wykonujące żądanie są asynchroniczne i zwracają Promise.


Wymagania

  • Node.js 20 lub nowszy (testowane na 20, 22, 24, 26)
  • Brak zależności runtime - biblioteka korzysta wyłącznie z wbudowanych modułów (fetch, node:crypto)
  • Typy TypeScript są dołączone do pakietu; działa tak samo z import (ESM) i require (CommonJS)

Instalacja

npm install @dpayglobal/dpay-node-sdk

Pakiet jest publikowany na npm, a kod źródłowy dostępny na GitHub.


Konfiguracja

Utwórz instancję klienta DPayClient, podając dane z Panelu dpay.pl:

import { DPayClient } from '@dpayglobal/dpay-node-sdk'

const dpay = new DPayClient({
service: 'nazwa_serwisu',
secretHash: 'twoj_secret_hash',
})
OpcjaTypOpis
servicestringNazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane)
secretHashstringKlucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane)
timeoutnumberTimeout żądań HTTP w milisekundach (domyślnie 30000)
httpClientHttpClientWłasny klient HTTP (testy, proxy, retry)
baseUrlsRecord<string, string>Nadpisanie hostów API (klucze: apiPayments, panel, gateway)
onRequest / onResponse(context) => voidHooki do logowania i metryk; checksum i dane karty są w nich już zredagowane
Timeout w milisekundach

W odróżnieniu od SDK PHP i Pythona (sekundy), tutaj timeout jest w milisekundach, zgodnie z konwencją Node.js. Wartość 30 oznaczałaby 30 ms - użyj 30000.

Klient udostępnia serwisy jako właściwości:

SerwisZakres
dpay.paymentsRejestracja płatności, szczegóły transakcji
dpay.refundsZwroty i sprawdzanie dostępności zwrotu
dpay.banksLista banków pay-by-link
dpay.blikAliasy BLIK OneClick i Recurring
dpay.cardsPłatności kartowe server-to-server
dpay.payoutsSzczegóły wypłat 1:1
dpay.ipnWeryfikacja powiadomień IPN

Kwoty - obiekt Money

Wszystkie kwoty w SDK to obiekt Money, wewnętrznie przechowujący grosze jako liczbę całkowitą. SDK sam dba o właściwy format kwoty dla każdego endpointu (kwota dziesiętna albo grosze), więc nie musisz pamiętać, który endpoint oczekuje którego formatu.

import { Money } from '@dpayglobal/dpay-node-sdk'

Money.pln(1050) // 10.50 PLN
Money.of(500, 'EUR') // 5.00 EUR
Money.fromDecimal('10.50', 'PLN')

money.minor // 1050 (grosze)
money.toDecimal() // "10.50"

Money jest niemutowalny (zamrożony) - nigdy nie wykonuje arytmetyki zmiennoprzecinkowej na kwotach.


Rejestracja płatności

Żądanie to zwykły obiekt opcji. Pola amount, transactionType i urls są wymagane, reszta jest opcjonalna. Suma kontrolna dokładana jest automatycznie:

import { DPayClient, Money } from '@dpayglobal/dpay-node-sdk'

const dpay = new DPayClient({
service: 'nazwa_serwisu',
secretHash: 'twoj_secret_hash',
})

const payment = await dpay.payments.register({
amount: Money.pln(1050),
transactionType: 'transfers',
urls: {
success: 'https://twojsklep.pl/sukces',
fail: 'https://twojsklep.pl/blad',
ipn: 'https://twojsklep.pl/ipn',
},
description: 'Zamówienie #1234',
custom: 'order-1234',
payer: { email: 'klient@example.com' },
})

if (payment.redirectUrl !== null) {
return response.redirect(payment.redirectUrl)
}

if (payment.isPaid) {
// płatność rozliczona inline (np. BLIK Level 0)
}

Typy transakcji można podać jako gołe napisy albo przez stałą TransactionType: TRANSFERS, DCB_GATEWAY, CARD_AUTH, MB_WAY_DIRECT, BIZUM_DIRECT, BLIK_RECURRING, CARD_RECURRING. Nieprawidłowe dane odrzucają zwrócony Promise błędem DPayValueError.

Ważniejsze pola żądania rejestracji

PoleOpis
descriptionOpis transakcji widoczny dla klienta
customWłasne dane identyfikujące zamówienie (wracają w IPN)
payerE-mail oraz imię i nazwisko klienta ({ email, firstName, lastName })
channelPłatność bezpośrednia wskazanym kanałem banku
creditCard, paypal, paysafecard, installment, blikWłączenie lub wyłączenie metod na bramce
noBanksUkrycie listy banków
blikCode + userAgent + userIpPłatność BLIK Level 0 (kod 6-cyfrowy)
blikAlias + userAgent + userIpPłatność aliasem BLIK OneClick
registerBlikAliasRejestracja aliasu OneClick przy płatności
registerCardRecurringRejestracja mandatu card recurring
cardRecurringAliasObciążenie zapisanej karty (MIT)
payoutInstrukcja wypłaty 1:1
efaktura + invoicePłatność za eFakturę (KSeF)
phoneNumber + currencyCodeMB WAY
billingAddress, shippingAddress, productsObiekty wolnej formy przekazywane bez zmian

Obsługa IPN

Weryfikacja podpisu IPN dzieje się przez serwis dpay.ipn. Najprościej przekazać obiekt żądania przychodzącego wprost - SDK sam odczyta surowe body:

import express from 'express'
import { DPayClient, IPN_ACK, SignatureVerificationError } from '@dpayglobal/dpay-node-sdk'

const app = express()
const dpay = new DPayClient({
service: 'nazwa_serwisu',
secretHash: process.env.DPAY_SECRET_HASH,
})

app.post('/ipn', express.raw({ type: '*/*' }), async (req, res) => {
let event
try {
event = await dpay.ipn.constructEventFromRequest(req.body)
} catch (error) {
if (error instanceof SignatureVerificationError) {
return res.status(400).send('Invalid signature')
}
throw error
}

if (event.isTransfer) {
await markOrderAsPaid(event.id, event.amount)
}

if (event.isCapture) {
await markOrderAsCaptured(event.id, event.amount, event.capturePaymentId)
}

res.send(IPN_ACK)
})

app.use(express.json())
Podpis liczy się z surowych bajtów żądania

Najczęstsza przyczyna nieudanej weryfikacji IPN w Node.js to parser body, który konsumuje strumień żądania, zanim SDK zdąży odczytać oryginalne bajty. Trasę IPN zamontuj przed express.json() i użyj express.raw({ type: '*/*' }). Gdy SDK wykryje, że body zostało już przetworzone, rzuci DPayValueError z podpowiedzią.

W Next.js (App Router), Hono czy Deno przekaż standardowy obiekt Request bezpośrednio:

export async function POST(request: Request) {
const event = await dpay.ipn.constructEventFromRequest(request)
// ... obsługa zdarzenia
return new Response(IPN_ACK)
}

Jeśli masz już surowe body jako string lub Buffer, użyj dpay.ipn.constructEvent(rawBody).

Odpowiedź musi być dokładnie OK

dpay.pl uznaje IPN za dostarczony wyłącznie, gdy treść odpowiedzi to dokładnie OK (stała IPN_ACK). Kod HTTP nie jest sprawdzany. Upewnij się, że framework nie dokleja niczego do body.

informacja

Zawsze porównaj event.amount (string, np. "10.00") z kwotą zamówienia w Twojej bazie i przetwarzaj każdą transakcję tylko raz (IPN może przyjść wielokrotnie).


Szczegóły transakcji

const transaction = await dpay.payments.details('identyfikator-transakcji')

transaction.status // 'paid', 'created', 'processing', 'expired', 'captured'
transaction.isPaid // true dla paid i captured
transaction.value.toDecimal() // "29.99"
transaction.refundedAmount // Money
transaction.availableRefundAmount // Money
transaction.isFullyRefunded // boolean
transaction.refunds // TransactionRefund[]

Zwroty

import { Money } from '@dpayglobal/dpay-node-sdk'

// zwrot pełny
const refund = await dpay.refunds.create({ transactionId: 'identyfikator-transakcji' })

// zwrot częściowy z powodem
const refund = await dpay.refunds.create({
transactionId: 'identyfikator-transakcji',
amount: Money.pln(500),
reason: 'reklamacja',
})

refund.isAccepted

Przed zwrotem możesz sprawdzić jego dostępność - metoda zwraca wynik biznesowy (nie odrzuca Promise błędem), także gdy zwrot jest niemożliwy:

const availability = await dpay.refunds.checkAvailability({
transactionId: 'identyfikator-transakcji',
amount: Money.pln(500),
})

if (!availability.isAvailable) {
availability.message // np. "Transakcja nie została opłacona"
availability.httpStatus // stabilny kod przyczyny, patrz tabela
}
Kod HTTPPrzyczyna odmowy
400Kwota przekracza dostępną kwotę zwrotu
401Kanał płatności nie obsługuje zwrotów
402Transakcja nie została opłacona
406Niewystarczające saldo na pokrycie zwrotu
409Wniosek o zwrot został już złożony
410Transakcja została już zwrócona
411Transakcje obciążeniowe nie podlegają zwrotom

Banki

const banks = await dpay.banks.all() // wszystkie banki dpay.pl
const banks = await dpay.banks.forService() // banki dostępne dla Twojego serwisu

banks[0].id
banks[0].name

BLIK - aliasy OneClick i Recurring

const alias = await dpay.blik.alias({ aliasValue: 'DPAY.UID.123456.abc12345' })
alias.isActive
alias.apps

await dpay.blik.unregisterAlias({ aliasValue: 'DPAY.UID.123456.abc12345' })

const status = await dpay.blik.recurringStatus({ aliasValue: 'PAYID-...' })
status.registration

Rejestracja aliasu i płatność aliasem odbywają się przez rejestrację płatności (blikCode + registerBlikAlias, płatność przez blikAlias).


Karty server-to-server

Dane karty szyfrujesz kluczem RSA pobieranym przed każdą próbą płatności:

import { CardData, CardEncryptor } from '@dpayglobal/dpay-node-sdk'

const publicKey = await dpay.cards.publicKey()

const encrypted = new CardEncryptor().encrypt(
new CardData({ pan: '4111111111111111', cvv: '123', expiry: '12/30' }),
transactionId,
publicKey,
)

const result = await dpay.cards.payOtp(transactionId, {
deviceInfo, // dane przeglądarki płatnika
encryptedCardData: encrypted,
cardHolderFirstName: 'Jan',
cardHolderLastName: 'Kowalski',
channelId: 31,
})

if (result.isSuccess) {
// płatność przechwycona
} else if (result.requiresThreeDsForm) {
const html = result.threeDsFormHtml // wyrenderuj w przeglądarce płatnika
} else if (result.hasDccOffer) {
const offer = result.dccOffer // pokaż ofertę DCC płatnikowi
// decyzję odeślij ponownym payOtp z dccDecision: 'accept'
}

Dostępne są także: preAuth() (pre-autoryzacja), capture() i cancel() (przechwycenie i anulowanie), googlePay() oraz applePay(). Odrzucenie płatności przy HTTP 200 odrzuca Promise błędem CardPaymentError z kodem błędu.

Szyfrowanie RSA korzysta z wbudowanego modułu node:crypto, więc karty S2S nie wymagają żadnej dodatkowej biblioteki. Obiekt CardData redaguje sam siebie przy logowaniu - numer karty i CVV nie trafią do logów jawnym tekstem.

informacja

Karty S2S wymagają zgodności PCI-DSS po stronie merchanta. Szczegóły przepływu znajdziesz w dokumentacji kart S2S.


Wypłaty 1:1

const details = await dpay.payouts.details({ withdrawId: 12345 })

details.isProcessed
details.net.toDecimal()
details.receiver

Anulowanie i limity czasu

Każda metoda serwisu przyjmuje opcjonalny obiekt { signal, timeout } jako ostatni argument. timeout (w milisekundach) obowiązuje tylko dla tego wywołania i nadpisuje domyślny z konfiguracji, a signal (AbortSignal) pozwala anulować żądanie, gdy klient odejdzie:

const transaction = await dpay.payments.details('identyfikator-transakcji', {
timeout: 5000,
signal: AbortSignal.timeout(5000),
})

Anulowanie i przekroczenie limitu czasu odrzucają Promise błędem TransportError - status płatności jest wtedy nieznany, więc zweryfikuj go przez payments.details().


Obsługa błędów

Wszystkie wyjątki SDK dziedziczą po DPayError. Metody serwisów są asynchroniczne, więc błędy - zarówno walidacji, jak i sieciowe - przychodzą jako odrzucenie Promise (try/catch z await).

WyjątekKiedy
TransportErrorBłąd sieci lub timeout - status płatności nieznany, użyj payments.details()
AuthenticationError401 - błędny checksum lub secret hash
InvalidRequestError400/422 - błędy walidacji (fieldErrors)
AccessDeniedError403 - brak uprawnień do operacji
NotFoundError404 - zasób nie istnieje
RateLimitError429 - limit żądań (retryAfter)
PaymentRejectedErrorRejestracja odrzucona (np. błędny kod BLIK; transactionId)
CardPaymentErrorOdrzucenie płatności kartowej (errorCode)
SignatureVerificationErrorNieprawidłowy podpis IPN
CardEncryptionErrorBłąd szyfrowania danych karty
ApiServerError5xx lub niepoprawna odpowiedź API
DPayValueErrorNiepoprawny argument przekazany do SDK
import { ApiError, InvalidRequestError, TransportError } from '@dpayglobal/dpay-node-sdk'

try {
const payment = await dpay.payments.register(request)
} catch (error) {
if (error instanceof InvalidRequestError) {
error.fieldErrors // Record<string, string[]>
} else if (error instanceof ApiError) {
error.httpStatus
error.errorCode
} else if (error instanceof TransportError) {
// nie wiadomo, czy żądanie dotarło - zweryfikuj przez payments.details()
}
}

Testowanie integracji

SDK zawiera transport testowy, który kolejkuje odpowiedzi i nagrywa wysłane żądania, więc testy nie wymagają dostępu do sieci. Importujesz go z podścieżki /testing, dzięki czemu nie trafia do bundla produkcyjnego:

import { DPayClient } from '@dpayglobal/dpay-node-sdk'
import { MockHttpClient } from '@dpayglobal/dpay-node-sdk/testing'

const transport = new MockHttpClient()
transport.queueJson(200, { transactionId: 'tx-1', msg: 'https://secure.dpay.pl/pay/1' })

const dpay = new DPayClient({ service: 'test', secretHash: 'test', httpClient: transport })
const payment = await dpay.payments.register(request)

expect(transport.lastRequestBody.value).toBe('10.50')

Więcej informacji