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) irequire(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',
})
| Opcja | Typ | Opis |
|---|---|---|
service | string | Nazwa serwisu (Punktu Płatności) z panel.dpay.pl (wymagane) |
secretHash | string | Klucz Hash (Secret Hash) do generowania sum kontrolnych (wymagane) |
timeout | number | Timeout żądań HTTP w milisekundach (domyślnie 30000) |
httpClient | HttpClient | Własny klient HTTP (testy, proxy, retry) |
baseUrls | Record<string, string> | Nadpisanie hostów API (klucze: apiPayments, panel, gateway) |
onRequest / onResponse | (context) => void | Hooki do logowania i metryk; checksum i dane karty są w nich już zredagowane |
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:
| Serwis | Zakres |
|---|---|
dpay.payments | Rejestracja płatności, szczegóły transakcji |
dpay.refunds | Zwroty i sprawdzanie dostępności zwrotu |
dpay.banks | Lista banków pay-by-link |
dpay.blik | Aliasy BLIK OneClick i Recurring |
dpay.cards | Płatności kartowe server-to-server |
dpay.payouts | Szczegóły wypłat 1:1 |
dpay.ipn | Weryfikacja 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
| Pole | Opis |
|---|---|
description | Opis transakcji widoczny dla klienta |
custom | Własne dane identyfikujące zamówienie (wracają w IPN) |
payer | E-mail oraz imię i nazwisko klienta ({ email, firstName, lastName }) |
channel | Płatność bezpośrednia wskazanym kanałem banku |
creditCard, paypal, paysafecard, installment, blik | Włączenie lub wyłączenie metod na bramce |
noBanks | Ukrycie listy banków |
blikCode + userAgent + userIp | Płatność BLIK Level 0 (kod 6-cyfrowy) |
blikAlias + userAgent + userIp | Płatność aliasem BLIK OneClick |
registerBlikAlias | Rejestracja aliasu OneClick przy płatności |
registerCardRecurring | Rejestracja mandatu card recurring |
cardRecurringAlias | Obciążenie zapisanej karty (MIT) |
payout | Instrukcja wypłaty 1:1 |
efaktura + invoice | Płatność za eFakturę (KSeF) |
phoneNumber + currencyCode | MB WAY |
billingAddress, shippingAddress, products | Obiekty 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())
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).
OKdpay.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.
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 HTTP | Przyczyna odmowy |
|---|---|
400 | Kwota przekracza dostępną kwotę zwrotu |
401 | Kanał płatności nie obsługuje zwrotów |
402 | Transakcja nie została opłacona |
406 | Niewystarczające saldo na pokrycie zwrotu |
409 | Wniosek o zwrot został już złożony |
410 | Transakcja została już zwrócona |
411 | Transakcje 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.
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ątek | Kiedy |
|---|---|
TransportError | Błąd sieci lub timeout - status płatności nieznany, użyj payments.details() |
AuthenticationError | 401 - błędny checksum lub secret hash |
InvalidRequestError | 400/422 - błędy walidacji (fieldErrors) |
AccessDeniedError | 403 - brak uprawnień do operacji |
NotFoundError | 404 - zasób nie istnieje |
RateLimitError | 429 - limit żądań (retryAfter) |
PaymentRejectedError | Rejestracja odrzucona (np. błędny kod BLIK; transactionId) |
CardPaymentError | Odrzucenie płatności kartowej (errorCode) |
SignatureVerificationError | Nieprawidłowy podpis IPN |
CardEncryptionError | Błąd szyfrowania danych karty |
ApiServerError | 5xx lub niepoprawna odpowiedź API |
DPayValueError | Niepoprawny 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')