Skip to main content

Server-to-Server (S2S) Card Payments

Server-to-Server integration allows you to process card payments (Visa, Mastercard) directly on your website, without redirecting to an external gateway. Card data is encrypted with an RSA public key, ensuring transaction security.

PCI DSS certification required

S2S integration requires PCI DSS certification, as payment card data is processed on your infrastructure. Enabling this integration mode requires prior approval from dpay - contact us before starting implementation.

Learn more about the requirements on the PCI DSS - information for merchants page.

Flow diagram

Step 1: Register the payment

Register the transaction the same way as in the standard integration:

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://myshop.com/success",
"url_fail": "https://myshop.com/error",
"url_ipn": "https://myshop.com/api/ipn",
"checksum": "..."
}'

Save the transactionId from the response - it will be needed in the following steps.

Step 2: Retrieve the RSA public key

Retrieve the public key used to encrypt card data:

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

Response

{
"publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...\n-----END PUBLIC KEY-----"
}
Caching the public key

The public key does not change frequently. You can cache it on the server side, but check for updates every 24 hours.

Step 3: Prepare and encrypt card data

Card data JSON structure

Prepare a JSON object with card data in the following format:

{
"PN": "4111111111111111",
"SC": "123",
"DT": "12/25",
"ID": "abc-def-123-456",
"TX": 1700000000
}
FieldDescriptionFormat
PNCard number (PAN)Digit string without spaces
SCCVV/CVC code3 digits (4 for Amex)
DTExpiry dateMM/YY
IDTransaction identifiertransactionId from Step 1
TXUnix timestampCurrent time in seconds

RSA/PKCS#1 encryption

Card data must be encrypted using the RSA algorithm with PKCS#1 v1.5 padding, then encoded in Base64.

TypeScript (using 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('Card data encryption failed');
}

return encrypted; // Already in Base64 format
}

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('Invalid public key');
}

$encrypted = '';
$result = openssl_public_encrypt($cardData, $encrypted, $publicKey, OPENSSL_PKCS1_PADDING);
if (!$result) {
throw new Exception('Encryption failed');
}

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')

Step 4: Submit the card payment

Send the encrypted card data to the card payment endpoint:

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

Request parameters

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

DeviceInfo object

DeviceInfo is required for the 3D Secure verification process. All fields are mandatory:

FieldTypeDescription
browserJavaEnabledbooleanWhether Java is enabled in the browser
browserLanguagestringBrowser language (e.g. "pl-PL")
browserColorDepthstringScreen color depth
browserScreenHeightstringScreen height in pixels
browserScreenWidthstringScreen width in pixels
browserTZstringTimezone in minutes (e.g. "-60" for CET)
browserUserAgentstringFull browser User-Agent
browserAcceptHeaderstringBrowser Accept header
browserJavascriptEnabledbooleanWhether JavaScript is enabled

Collecting DeviceInfo (JavaScript)

function getDeviceInfo() {
return {
browserJavaEnabled: navigator.javaEnabled ? navigator.javaEnabled() : 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,
browserAcceptHeader: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
browserJavascriptEnabled: true,
};
}

Step 5: Handle 3D Secure

If the bank requires 3D Secure authorization, the response will contain redirect data:

3DS response

{
"error": false,
"status": "3DS_REQUIRED",
"redirectType": "FORM",
"redirectUrl": "https://acs-bank.example.com/3ds",
"redirectParams": {
"PaReq": "eJzVWFmTo...",
"TermUrl": "https://api-payments.dpay.pl/api/v1_0/cards/3ds/callback/abc-def",
"MD": "abc-def-123-456"
}
}

Handling FORM redirect

When redirectType is FORM, you need to dynamically create an HTML form and submit it automatically:

function handle3DS(response) {
if (response.redirectType === 'FORM') {
const form = document.createElement('form');
form.method = 'POST';
form.action = response.redirectUrl;

Object.entries(response.redirectParams).forEach(([key, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = value;
form.appendChild(input);
});

document.body.appendChild(form);
form.submit();
}
}

After the 3DS process completes, the customer will be redirected to url_success or url_fail, and dpay.pl will send an IPN notification.

Success response (without 3DS)

If 3DS is not required, the payment is processed immediately:

{
"error": false,
"status": "paid",
"transactionId": "abc-def-123-456"
}

Full example - 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;

// Step 1: Register the payment
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;

// Step 2: Retrieve the public key
const keyResponse = await axios.get(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key'
);

// Step 3: Encrypt card data
const encrypt = new JSEncrypt();
encrypt.setPublicKey(keyResponse.data.publicKey);

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

// Step 4: Submit the payment
const payResponse = await axios.post(
`https://api-payments.dpay.pl/api/v1_0/cards/payment/${transactionId}/pay/card-otp`,
{
encryptedCardData,
deviceInfo,
}
);

return payResponse.data;
}

Security

warning
  • Never store card data (numbers, CVVs, expiry dates) on your server
  • Never log card data in log files
  • RSA encryption must take place on the client side (in the browser) so that raw card data never reaches your server
  • Use HTTPS across the entire website