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.
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
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).
/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.
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
}
| Pole | Opis | Format |
|---|---|---|
PN | Numer karty (PAN) | Ciąg cyfr bez spacji |
SC | Kod CVV/CVC | 3 cyfry (4 dla Amex) |
DT | Data ważności | MM/YY |
ID | Identyfikator transakcji | transactionId z Kroku 1 |
TX | Timestamp Unix | Aktualny 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ń)
| Pole | Typ | Kiedy wysłać |
|---|---|---|
threeDsConfirmed | boolean | true - 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:
| Pole | Typ | Wymagane | Opis |
|---|---|---|---|
browserAcceptHeader | string | Tak | Nagłówek Accept przeglądarki |
browserJavaEnabled | string | Nie | "true" / "false" - czy Java jest włączona w przeglądarce |
browserLanguage | string | Tak | Język przeglądarki (np. "pl-PL") |
browserColorDepth | string | Tak | Głębia kolorów ekranu |
browserScreenHeight | string | Tak | Wysokość ekranu w pikselach |
browserScreenWidth | string | Tak | Szerokość ekranu w pikselach |
browserTZ | string | Tak | Strefa czasowa w minutach (np. "-60" dla CET) |
browserUserAgent | string | Tak | Pełny User-Agent przeglądarki |
systemFamily | string | Tak | Rodzina systemu operacyjnego (np. "Windows", "iOS") |
geoLocalization | string | Tak | Lokalizacja urządzenia (np. współrzędne z Geolocation API lub "unknown") |
deviceID | string | Tak | Identyfikator urządzenia (maks. 64 znaki); akceptowany jest też alias deviceId |
applicationName | string | Tak | Nazwa 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:
redirectType | Pole z treścią | Znaczenie |
|---|---|---|
SUCCESS | (puste) | Płatność zaksięgowana - przekieruj klienta na stronę sukcesu |
FORM | redirectText (Base64 HTML) | Wymagana autoryzacja 3D Secure - wyrenderuj formularz |
URL | redirectText (URL) | Przekierowanie na zewnętrzny adres |
DCC_OFFER | dccOffer (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.
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:
| Kod | Przyczyna |
|---|---|
E101 Invalid card data | Pole ID w zaszyfrowanych danych nie zgadza się z identyfikatorem transakcji z adresu URL |
E103 Invalid card data | Zaszyfrowany payload jest niekompletny (brak któregoś z pól PN, SC, DT, ID, TX) |
E104A Invalid card data | Znacznik czasu TX różni się od czasu serwera o więcej niż 3600 sekund |
E104B Invalid card data | Payload starszy niż 600 sekund użyty bez threeDsConfirmed: true |
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:
message | Znaczenie | Zalecane 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. |
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 karty | Scenariusz |
|---|---|
4111111111111111 | Visa - płatność zakończona sukcesem |
5555555555554444 | Mastercard - płatność zakończona sukcesem |
4012888888881881 | Visa - sukces z 3DS challenge (kaskada FORM → SUCCESS) |
4000000000000002 | Visa - odmowa |
4000000000000069 | Visa - karta wygasła |
4000000000000127 | Visa - brak środków |
4000000000000085 | Visa - timeout |
5105105105105100 | Mastercard - odmowa |
5346930000008110 | Mastercard - oferta DCC (EUR→PLN, ważność 30 minut) |
5346930000008128 | Mastercard - karta nie kwalifikuje się do DCC, transparentny SUCCESS |
5346930000008136 | Mastercard - oferta DCC z ważnością 60 sekund (test wygaśnięcia) |
5346930000008144 | Mastercard - DCC + 3DS po decyzji (kaskada DCC_OFFER → FORM → SUCCESS) |
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
- 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