Skip to main content

Card data encryption (RSA/PKCS#1)

For Server-to-Server (S2S) card payments, card data must be encrypted with an RSA public key before it is sent to dpay.pl. This guide describes the entire encryption process in detail.

GET/api/v1_0/cards/public-keyComplete contract for the RSA public-key endpoint used to encrypt card data.Full contract in the API Reference

Why is encryption required?

Raw card data (card number, CVV, and expiry date) is sensitive information covered by the PCI DSS standard. RSA encryption ensures that:

  • Card data is readable only by dpay.pl (only dpay.pl holds the private key)
  • Even if the data is intercepted in transit, it is useless without the private key
  • Your server never has access to raw card data (encryption takes place in the customer's browser)

Encryption algorithm

  • Algorithm: RSA
  • Padding: PKCS#1 v1.5
  • Output encoding: Base64

Step 1: Retrieve the public key

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

Response

The endpoint returns the raw PEM key as text (not JSON):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

The key is in PEM format (X.509 SubjectPublicKeyInfo). Read the response body as a string; do not parse it as JSON.

tip

The public key may be rotated without notice. Retrieve a new public key before every transaction.

Step 2: Prepare the JSON object

Prepare a JSON object with the card data and transaction metadata:

{
"PN": "4111111111111111",
"SC": "123",
"DT": "12/25",
"ID": "abc-def-123-456",
"TX": 1700000000
}

Field descriptions

FieldNameDescriptionFormatExample
PNPAN (Primary Account Number)Card numberDigits without spaces"4111111111111111"
SCSecurity CodeCVV/CVC code3 digits (4 for Amex)"123"
DTDate (Expiry)Expiry dateMM/YY"12/25"
IDTransaction IDTransaction identifierUUID from registration"abc-def-123-456"
TXTimestampOperation timeUnix timestamp (seconds)1700000000
TX field

The TX field protects against replay attacks. dpay.pl rejects a request if its timestamp is more than 600 seconds old. The exception is a payment made after 3D Secure confirmation, for which the accepted window is 3600 seconds. Always use the current time.

Step 3: Encryption

TypeScript / JavaScript (JSEncrypt)

The JSEncrypt library is the most popular choice for RSA encryption in the browser:

npm install jsencrypt
import JSEncrypt from 'jsencrypt';

interface CardData {
PN: string; // Card number
SC: string; // CVV
DT: string; // Expiry date MM/YY
ID: string; // Transaction ID
TX: number; // Unix timestamp
}

async function encryptCardData(
cardNumber: string,
cvv: string,
expiryDate: string,
transactionId: string
): Promise<string> {
// 1. Fetch the public key (the response is raw PEM text, not JSON)
const response = await fetch(
'https://api-payments.dpay.pl/api/v1_0/cards/public-key'
);
const publicKey = await response.text();

// 2. Prepare the card data object
const cardData: CardData = {
PN: cardNumber.replace(/\s/g, ''), // Remove spaces
SC: cvv,
DT: expiryDate, // Format: MM/YY
ID: transactionId,
TX: Math.floor(Date.now() / 1000),
};

// 3. Encrypt with RSA/PKCS#1
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);

const encrypted = encrypt.encrypt(JSON.stringify(cardData));
if (!encrypted) {
throw new Error('Card data encryption failed');
}

// 4. The result is already Base64-encoded
return encrypted;
}

// === Usage example ===

const encryptedData = await encryptCardData(
'4111 1111 1111 1111',
'123',
'12/25',
'abc-def-123-456'
);

// Send encryptedData to the server
await fetch('/api/pay/card', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ encryptedCardData: encryptedData }),
});

PHP (OpenSSL)

Encryption in PHP uses the built-in OpenSSL extension:

<?php
/**
* RSA/PKCS#1 card data encryption
*
* WARNING: In production, encryption should take place on the client side
* (in the browser), so raw card data never reaches your server.
* This example is intended for demonstration and testing purposes.
*/
function encryptCardData(
string $cardNumber,
string $cvv,
string $expiryDate,
string $transactionId,
string $publicKeyPem
): string {
// 1. Prepare the JSON object
$cardData = json_encode([
'PN' => preg_replace('/\s/', '', $cardNumber),
'SC' => $cvv,
'DT' => $expiryDate,
'ID' => $transactionId,
'TX' => time(),
], JSON_UNESCAPED_SLASHES);

// 2. Load the public key
$publicKey = openssl_pkey_get_public($publicKeyPem);
if (!$publicKey) {
throw new RuntimeException(
'Unable to load the public key: ' . openssl_error_string()
);
}

// 3. Encrypt with RSA/PKCS#1
$encrypted = '';
$result = openssl_public_encrypt(
$cardData,
$encrypted,
$publicKey,
OPENSSL_PKCS1_PADDING
);

if (!$result) {
throw new RuntimeException(
'Encryption failed: ' . openssl_error_string()
);
}

// 4. Encode as Base64
return base64_encode($encrypted);
}

// === Usage example ===

// Fetch the public key (the response is raw PEM text, not JSON)
$ch = curl_init('https://api-payments.dpay.pl/api/v1_0/cards/public-key');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$publicKeyPem = curl_exec($ch);
curl_close($ch);

$encryptedCardData = encryptCardData(
'4111111111111111',
'123',
'12/25',
'abc-def-123-456',
$publicKeyPem
);

echo $encryptedCardData; // Base64-encoded encrypted data

Python (pycryptodome)

pip install pycryptodome
import json
import time
import base64
import re
import requests
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5


def encrypt_card_data(
card_number: str,
cvv: str,
expiry_date: str,
transaction_id: str,
public_key_pem: str,
) -> str:
"""Encrypt card data with RSA/PKCS#1."""

# 1. Prepare the JSON object
card_data = json.dumps({
'PN': re.sub(r'\s', '', card_number),
'SC': cvv,
'DT': expiry_date,
'ID': transaction_id,
'TX': int(time.time()),
})

# 2. Load the public key
key = RSA.import_key(public_key_pem)

# 3. Encrypt with RSA/PKCS#1 v1.5
cipher = PKCS1_v1_5.new(key)
encrypted = cipher.encrypt(card_data.encode('utf-8'))

# 4. Encode as Base64
return base64.b64encode(encrypted).decode('utf-8')


# === Usage example ===

# Fetch the public key (the response is raw PEM text, not JSON)
response = requests.get('https://api-payments.dpay.pl/api/v1_0/cards/public-key')
public_key_pem = response.text

encrypted = encrypt_card_data(
'4111111111111111',
'123',
'12/25',
'abc-def-123-456',
public_key_pem,
)

print(encrypted)

Java

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

public class CardEncryption {

public static String encryptCardData(
String cardNumber,
String cvv,
String expiryDate,
String transactionId,
String publicKeyPem
) throws Exception {

// 1. Prepare the JSON object
String cardData = String.format(
"{\"PN\":\"%s\",\"SC\":\"%s\",\"DT\":\"%s\",\"ID\":\"%s\",\"TX\":%d}",
cardNumber.replaceAll("\\s", ""),
cvv,
expiryDate,
transactionId,
System.currentTimeMillis() / 1000
);

// 2. Load the public key
String keyContent = publicKeyPem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");

byte[] keyBytes = Base64.getDecoder().decode(keyContent);
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);

// 3. Encrypt with RSA/PKCS#1
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(cardData.getBytes("UTF-8"));

// 4. Encode as Base64
return Base64.getEncoder().encodeToString(encrypted);
}
}

Validate card data before encryption

It is good practice to validate card data on the client side before encryption:

Luhn algorithm (card number validation)

function isValidCardNumber(number) {
const digits = number.replace(/\s/g, '');
if (!/^\d{13,19}$/.test(digits)) return false;

let sum = 0;
let isEven = false;

for (let i = digits.length - 1; i >= 0; i--) {
let digit = parseInt(digits[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}

return sum % 10 === 0;
}

Expiry date validation

function isValidExpiryDate(date) {
const match = date.match(/^(\d{2})\/(\d{2})$/);
if (!match) return false;

const month = parseInt(match[1], 10);
const year = parseInt('20' + match[2], 10);

if (month < 1 || month > 12) return false;

const now = new Date();
const expiry = new Date(year, month); // First day of the following month

return expiry > now;
}

CVV validation

function isValidCVV(cvv) {
return /^\d{3,4}$/.test(cvv);
}

Security best practices

  1. Encrypt in the browser - raw card data should be encrypted on the client side (in JavaScript) before it reaches your server
  2. Never log card data - do not write card numbers, CVVs, or expiry dates to logs
  3. HTTPS is mandatory - your entire site must be available over HTTPS
  4. Do not store card data - do not save encrypted or raw card data in your database
  5. Use a current timestamp - the TX field must contain the current time; dpay.pl rejects requests with a timestamp older than 600 seconds (3600 seconds when payment follows 3D Secure confirmation)
danger

Failure to follow these security rules may result in loss of PCI DSS certification and legal liability for payment card data breaches.