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.
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-----"
}
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
}
| Field | Description | Format |
|---|---|---|
PN | Card number (PAN) | Digit string without spaces |
SC | CVV/CVC code | 3 digits (4 for Amex) |
DT | Expiry date | MM/YY |
ID | Transaction identifier | transactionId from Step 1 |
TX | Unix timestamp | Current 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:
| Field | Type | Description |
|---|---|---|
browserJavaEnabled | boolean | Whether Java is enabled in the browser |
browserLanguage | string | Browser language (e.g. "pl-PL") |
browserColorDepth | string | Screen color depth |
browserScreenHeight | string | Screen height in pixels |
browserScreenWidth | string | Screen width in pixels |
browserTZ | string | Timezone in minutes (e.g. "-60" for CET) |
browserUserAgent | string | Full browser User-Agent |
browserAcceptHeader | string | Browser Accept header |
browserJavascriptEnabled | boolean | Whether 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
- 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