Przejdź do głównej zawartości

Płatności kartowe Server-to-Server (S2S)

Integracja Server-to-Server pozwala na przetwarzanie płatności kartowych (Visa, Mastercard) bezpośrednio na Twojej stronie, bez przekierowania do zewnętrznej bramki. Dane karty są szyfrowane kluczem publicznym RSA, co zapewnia bezpieczeństwo transakcji.

Wymagana certyfikacja PCI DSS

Integracja S2S wymaga posiadania certyfikacji PCI DSS, ponieważ dane kart płatniczych są przetwarzane po stronie Twojej infrastruktury. Uruchomienie tego trybu integracji wymaga wcześniejszej zgody dpay - skontaktuj się z nami przed rozpoczęciem implementacji.

Dowiedz się więcej o wymaganiach na stronie PCI DSS - informacje dla merchantów.

Schemat działania

Karty zagraniczne i DCC

Jeśli posiadacz karty płaci kartą zagraniczną, a Twój serwis ma aktywny mechanizm Dynamic Currency Conversion, w odpowiedzi z /pay/card-otp może pojawić się oferta przewalutowania (redirectType: "DCC_OFFER"). Pełen opis flow znajdziesz na stronie Dynamic Currency Conversion (DCC).

GET/api/v1_0/cards/public-keyPobierz klucz publiczny RSA do szyfrowania danych karty.Pełny kontrakt w API Reference POST/api/v1_0/cards/payment/{transactionId}/pay/card-otpWyślij zaszyfrowane dane karty i obsłuż 3D Secure.Pełny kontrakt w API Reference

Krok 1: Rejestracja płatności

Zarejestruj transakcję tak samo jak w standardowej integracji:

curl -X POST https://api-payments.dpay.pl/api/v1_0/payments/register \
-H "Content-Type: application/json" \
-d '{
"transactionType": "transfers",
"service": "abc123",
"value": "99.99",
"url_success": "https://mojsklep.pl/sukces",
"url_fail": "https://mojsklep.pl/blad",
"url_ipn": "https://mojsklep.pl/api/ipn",
"checksum": "..."
}'

Zapisz transactionId z odpowiedzi - będzie potrzebny w następnych krokach.

Krok 2: Pobranie klucza publicznego RSA

Pobierz klucz publiczny używany do szyfrowania danych karty:

GET https://api-payments.dpay.pl/api/v1_0/cards/public-key

Odpowiedź

Endpoint zwraca klucz jako surowy tekst PEM (nie JSON):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----

Treść odpowiedzi możesz przekazać bezpośrednio do biblioteki szyfrującej - nie parsuj jej jako JSON.

Cache klucza publicznego

Klucz publiczny nie zmienia się często. Możesz go cache'ować po stronie serwera, ale sprawdzaj aktualizacje co 24 godziny.

Krok 3: Przygotowanie i szyfrowanie danych karty

Struktura JSON z danymi karty

Przygotuj obiekt JSON z danymi karty w następującym formacie:

{
"PN": "4111111111111111",
"SC": "123",
"DT": "12/25",
"ID": "abc-def-123-456",
"TX": 1700000000
}
PoleOpisFormat
PNNumer karty (PAN)Ciąg cyfr bez spacji
SCKod CVV/CVC3 cyfry (4 dla Amex)
DTData ważnościMM/YY
IDIdentyfikator transakcjitransactionId z Kroku 1
TXTimestamp UnixAktualny czas w sekundach

Szyfrowanie RSA/PKCS#1

Dane karty muszą być zaszyfrowane algorytmem RSA z paddingiem PKCS#1 v1.5, a następnie zakodowane w Base64.

TypeScript (z użyciem JSEncrypt)

import JSEncrypt from 'jsencrypt';

function encryptCardData(
cardNumber: string,
cvv: string,
expiryDate: string,
transactionId: string,
publicKey: string
): string {
const cardData = JSON.stringify({
PN: cardNumber,
SC: cvv,
DT: expiryDate,
ID: transactionId,
TX: Math.floor(Date.now() / 1000),
});

const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);

const encrypted = encrypt.encrypt(cardData);
if (!encrypted) {
throw new Error('Szyfrowanie danych karty nie powiodło się');
}

return encrypted; // Już w formacie Base64
}

PHP (OpenSSL)

function encryptCardData(
string $cardNumber,
string $cvv,
string $expiryDate,
string $transactionId,
string $publicKeyPem
): string {
$cardData = json_encode([
'PN' => $cardNumber,
'SC' => $cvv,
'DT' => $expiryDate,
'ID' => $transactionId,
'TX' => time(),
]);

$publicKey = openssl_pkey_get_public($publicKeyPem);
if (!$publicKey) {
throw new Exception('Nieprawidłowy klucz publiczny');
}

$encrypted = '';
$result = openssl_public_encrypt($cardData, $encrypted, $publicKey, OPENSSL_PKCS1_PADDING);
if (!$result) {
throw new Exception('Szyfrowanie nie powiodło się');
}

return base64_encode($encrypted);
}

Python

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
import json
import base64
import time

def encrypt_card_data(card_number, cvv, expiry_date, transaction_id, public_key_pem):
card_data = json.dumps({
'PN': card_number,
'SC': cvv,
'DT': expiry_date,
'ID': transaction_id,
'TX': int(time.time()),
})

key = RSA.import_key(public_key_pem)
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(card_data.encode('utf-8'))

return base64.b64encode(encrypted).decode('utf-8')

Krok 4: Wysłanie płatności kartowej

Wyślij zaszyfrowane dane karty na endpoint płatności kartowej:

POST https://api-payments.dpay.pl/api/v1_0/cards/payment/{transactionId}/pay/card-otp
Content-Type: application/json

Parametry zapytania

{
"encryptedCardData": "Base64-encoded-encrypted-data...",
"deviceInfo": {
"browserAcceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"browserJavaEnabled": "false",
"browserLanguage": "pl-PL",
"browserColorDepth": "24",
"browserScreenHeight": "1080",
"browserScreenWidth": "1920",
"browserTZ": "-60",
"browserUserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"systemFamily": "Windows",
"geoLocalization": "52.2297,21.0122",
"deviceID": "device-unique-id",
"applicationName": "Chrome"
}
}

Parametry opcjonalne (dla kolejnych wywołań)

PoleTypKiedy wysłać
threeDsConfirmedbooleantrue - po ukończeniu challenge 3DS w przeglądarce klienta. Wyślij ponownie ten sam encryptedCardData.
dccDecision"accept" / "reject"Decyzja klienta po ofercie DCC. Wyślij ponownie ten sam encryptedCardData. Patrz DCC.

Obiekt DeviceInfo

DeviceInfo jest wymagany do procesu weryfikacji 3D Secure. Wszystkie pola poza browserJavaEnabled są obowiązkowe - brak któregokolwiek zwróci HTTP 422:

PoleTypWymaganeOpis
browserAcceptHeaderstringTakNagłówek Accept przeglądarki
browserJavaEnabledstringNie"true" / "false" - czy Java jest włączona w przeglądarce
browserLanguagestringTakJęzyk przeglądarki (np. "pl-PL")
browserColorDepthstringTakGłębia kolorów ekranu
browserScreenHeightstringTakWysokość ekranu w pikselach
browserScreenWidthstringTakSzerokość ekranu w pikselach
browserTZstringTakStrefa czasowa w minutach (np. "-60" dla CET)
browserUserAgentstringTakPełny User-Agent przeglądarki
systemFamilystringTakRodzina systemu operacyjnego (np. "Windows", "iOS")
geoLocalizationstringTakLokalizacja urządzenia (np. współrzędne z Geolocation API lub "unknown")
deviceIDstringTakIdentyfikator urządzenia (maks. 64 znaki); akceptowany jest też alias deviceId
applicationNamestringTakNazwa aplikacji/przeglądarki (maks. 64 znaki)

Zbieranie DeviceInfo (JavaScript)

function getDeviceInfo() {
return {
browserAcceptHeader: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
browserJavaEnabled: navigator.javaEnabled?.() ? 'true' : 'false',
browserLanguage: navigator.language || 'pl-PL',
browserColorDepth: String(screen.colorDepth),
browserScreenHeight: String(screen.height),
browserScreenWidth: String(screen.width),
browserTZ: String(new Date().getTimezoneOffset()),
browserUserAgent: navigator.userAgent,
systemFamily: navigator.platform || 'Unknown',
geoLocalization: 'unknown', // np. wspolrzedne z Geolocation API, gdy klient wyrazil zgode
deviceID: 'device-unique-id', // wlasny stabilny identyfikator urzadzenia
applicationName: 'Chrome',
};
}

Krok 5: Obsługa odpowiedzi

Odpowiedź z endpointu /pay/card-otp zawiera pole message.redirectType, które determinuje dalszą akcję frontendu:

redirectTypePole z treściąZnaczenie
SUCCESS(puste)Płatność zaksięgowana - przekieruj klienta na stronę sukcesu
FORMredirectText (Base64 HTML)Wymagana autoryzacja 3D Secure - wyrenderuj formularz
URLredirectText (URL)Przekierowanie na zewnętrzny adres
DCC_OFFERdccOffer (obiekt)Karta zagraniczna - prezentacja oferty przewalutowania. Patrz DCC

Obsługa 3D Secure (FORM)

Jeśli wymagana jest autoryzacja 3D Secure, odpowiedź zawiera w polu message.redirectText kompletny formularz HTML zakodowany w Base64 (z automatycznym wysłaniem po wstrzyknięciu do strony):

Odpowiedź z 3DS

{
"success": true,
"status": "success",
"message": {
"redirectText": "<Base64 HTML z formularzem auto-submit>",
"redirectType": "FORM",
"dccOffer": null
}
}

Obsługa przekierowania FORM

Gdy message.redirectType to FORM, zdekoduj message.redirectText z Base64 i wstrzyknij otrzymany HTML do strony. Zdekodowana treść zawiera formularz threeDsForm z ukrytymi polami oraz skrypt wysyłający go automatycznie. Skrypt osadzony przez innerHTML nie wykona się, dlatego po wstrzyknięciu wyślij formularz ręcznie:

function handle3DS(response) {
const { redirectType, redirectText } = response.message;

if (redirectType === 'FORM') {
const html = atob(redirectText);

const container = document.createElement('div');
container.innerHTML = html;
document.body.appendChild(container);

// Skrypt auto-submit z redirectText nie wykona sie przy innerHTML -
// formularz wysylamy recznie.
document.getElementById('threeDsForm').submit();
}
}

Po zakończeniu procesu 3DS klient zostanie przekierowany na url_success lub url_fail, a dpay.pl wyśle powiadomienie IPN.

Powrót z 3DS

Jeśli Twoja integracja obsługuje powrót z challenge bez przeładowania strony, wywołaj endpoint /pay/card-otp ponownie z threeDsConfirmed: true oraz tym samym encryptedCardData. Bramka sfinalizuje płatność i zwróci SUCCESS.

Odpowiedź sukcesu (bez 3DS)

Jeśli 3DS nie jest wymagane, płatność zostaje przetworzona natychmiast:

{
"success": true,
"status": "success",
"message": {
"redirectText": "",
"redirectType": "SUCCESS",
"dccOffer": null
}
}

Pole message.dccOffer jest obecne zawsze i ma wartość null, gdy oferta przewalutowania nie występuje.

Błędy i okno ważności zaszyfrowanych danych

Gdy przetwarzanie się nie powiedzie, endpoint zwraca HTTP 200 z success: false, a message jest wtedy tekstem komunikatu:

{
"success": false,
"status": "error",
"message": "E104B Invalid card data"
}

Rodzina kodów E101-E104 dotyczy zaszyfrowanego payloadu karty:

KodPrzyczyna
E101 Invalid card dataPole ID w zaszyfrowanych danych nie zgadza się z identyfikatorem transakcji z adresu URL
E103 Invalid card dataZaszyfrowany payload jest niekompletny (brak któregoś z pól PN, SC, DT, ID, TX)
E104A Invalid card dataZnacznik czasu TX różni się od czasu serwera o więcej niż 3600 sekund
E104B Invalid card dataPayload starszy niż 600 sekund użyty bez threeDsConfirmed: true
Okno ważności payloadu

Zaszyfrowane dane karty są ważne maksymalnie 600 sekund od momentu wygenerowania (TX). Przy ponownym wywołaniu z threeDsConfirmed: true (powrót z challenge 3DS) okno wydłuża się do 3600 sekund. Generuj encryptedCardData bezpośrednio przed wysłaniem płatności.

Komunikaty przetwarzania płatności

Gdy błąd dotyczy przetwarzania płatności (a nie payloadu karty), message zawiera jeden z poniższych komunikatów. Zalecamy pokazać go użytkownikowi (lub jego odpowiednik w Twoim języku) i zastosować zalecane działanie:

messageZnaczenieZalecane działanie
Payment was declined by the card issuer.Wydawca karty odrzucił transakcję (brak środków, limit, blokada).Poproś o inną kartę lub kontakt z bankiem. Nie ponawiaj tą samą kartą.
Invalid card details.Numer karty / typ karty jest nieprawidłowy lub nieobsługiwany.Poproś o poprawienie danych karty.
Card already registered.Ta karta jest już zapisana dla tego serwisu (dotyczy zapisu karty do płatności cyklicznych).Użyj istniejącego aliasu karty zamiast zapisywać ją ponownie.
Card could not be saved. Please try again.Zapis karty (tokenizacja) nie powiódł się.Ponów zapis karty.
Card is not available for recurring charges. Please re-add the card.Zapisana karta nie ma poprawnej referencji do obciążeń cyklicznych.Zapisz kartę ponownie (nowy alias).
Capture was declined by the card issuer.Wydawca odrzucił przechwycenie preautoryzacji.Skontaktuj się z BOK; sprawdź czy preautoryzacja nie wygasła.
Cancellation was declined by the card issuer.Anulowanie preautoryzacji zostało odrzucone.Skontaktuj się z BOK.
Refund is not available for this transaction.Zwrot dla tej transakcji nie jest dostępny (np. transakcja nie jest opłacona lub minęło okno zwrotu).Sprawdź status transakcji; użyj check-refund-availability.
The requested transaction was not found.Wskazana transakcja/referencja nie istnieje.Zweryfikuj identyfikator transakcji.
Payment service is temporarily unavailable. Please try again.Chwilowy problem po stronie operatora płatności.Ponów po krótkiej chwili (transient).
The payment could not be processed.Płatność nie została zrealizowana z innego powodu.Ponów lub poproś o inną metodę płatności.
Invalid request data.Żądanie zawiera nieprawidłowe dane.Sprawdź parametry zapytania.
Payment configuration error.Błąd konfiguracji serwisu płatniczego.Skontaktuj się z BOK dpay.
Dopasowanie komunikatów

Odpowiedź błędu zawiera pole message (tekst). Powyższa lista może być rozszerzana o nowe komunikaty - buduj logikę odporną na nieznane wartości (traktuj je jak The payment could not be processed.), a nie tylko na dokładne dopasowanie znanych stringów.

Pełny przykład - Node.js

const crypto = require('crypto');
const axios = require('axios');
const JSEncrypt = require('jsencrypt');

async function processCardPayment(orderData, cardData, deviceInfo) {
const service = process.env.DPAY_SERVICE;
const secretHash = process.env.DPAY_SECRET_HASH;

// Krok 1: Rejestracja płatności
const checksum = crypto
.createHash('sha256')
.update(`${service}|${secretHash}|${orderData.value}|${orderData.urlSuccess}|${orderData.urlFail}|${orderData.urlIpn}`)
.digest('hex');

const registerResponse = await axios.post(
'https://api-payments.dpay.pl/api/v1_0/payments/register',
{
transactionType: 'transfers',
service,
value: orderData.value,
url_success: orderData.urlSuccess,
url_fail: orderData.urlFail,
url_ipn: orderData.urlIpn,
checksum,
}
);

const { transactionId } = registerResponse.data;

// Krok 2: Pobranie klucza publicznego (odpowiedz to surowy PEM, nie JSON)
const keyResponse = await axios.get(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key',
{ responseType: 'text' }
);

// Krok 3: Szyfrowanie danych karty
const encrypt = new JSEncrypt();
encrypt.setPublicKey(keyResponse.data);

const encryptedCardData = encrypt.encrypt(JSON.stringify({
PN: cardData.number,
SC: cardData.cvv,
DT: cardData.expiry,
ID: transactionId,
TX: Math.floor(Date.now() / 1000),
}));

// Krok 4: Wysłanie płatności
const payResponse = await axios.post(
`https://api-payments.dpay.pl/api/v1_0/cards/payment/${transactionId}/pay/card-otp`,
{
encryptedCardData,
deviceInfo,
}
);

return payResponse.data;
}

Testowanie w trybie sandbox

Gdy serwis ma włączony tryb testowy, scenariusz symulacji wybierany jest po numerze karty (PAN) przekazanym w encryptedCardData do /pay/card-otp. Przechodzisz pełny flow (rejestracja, pobranie klucza, szyfrowanie, płatność) - różnica polega na tym, że zapytanie nie trafia do procesora kart, a wynik jest symulowany:

Numer kartyScenariusz
4111111111111111Visa - płatność zakończona sukcesem
5555555555554444Mastercard - płatność zakończona sukcesem
4012888888881881Visa - sukces z 3DS challenge (kaskada FORMSUCCESS)
4000000000000002Visa - odmowa
4000000000000069Visa - karta wygasła
4000000000000127Visa - brak środków
4000000000000085Visa - timeout
5105105105105100Mastercard - odmowa
5346930000008110Mastercard - oferta DCC (EUR→PLN, ważność 30 minut)
5346930000008128Mastercard - karta nie kwalifikuje się do DCC, transparentny SUCCESS
5346930000008136Mastercard - oferta DCC z ważnością 60 sekund (test wygaśnięcia)
5346930000008144Mastercard - DCC + 3DS po decyzji (kaskada DCC_OFFERFORMSUCCESS)
informacja

Symulacja działa wyłącznie gdy serwis ma włączony tryb testowy. Numer testowy podajesz w polu PN szyfrowanego payloadu - dokładnie tak samo, jak prawdziwy numer karty w produkcji. Scenariusz z 3DS challenge (4012888888881881) zwraca FORM z testową stroną autoryzacji, po której wywołujesz /pay/card-otp ponownie z threeDsConfirmed: true.

Pełna lista numerów testowych i szczegóły działania dostępne na stronie Środowisko testowe.

Bezpieczeństwo

ostrzeżenie
  • Nigdy nie przechowuj danych kart (numerów, CVV, dat ważności) na swoim serwerze
  • Nigdy nie loguj danych kart w plikach dziennika
  • Szyfrowanie RSA musi odbywać się po stronie klienta (w przeglądarce), aby surowe dane karty nigdy nie trafiły na Twój serwer
  • Użyj HTTPS na całej stronie