Dynamic Checkout Links
Generate custom checkout links with dynamic product settings that can be posted on your website or sent directly to customers. Dynamic settings allow you to modify product information, pricing, and custom fields on a per-link basis.
Overview Dynamic
Checkout Links enable you to:
- Customize product details (name, description, pricing) per checkout link
- Pre-fill customer information to streamline checkout
- Apply dynamic discounts and promotional pricing
- Set link expiration dates for time-limited offers
- Pass custom fields for tracking and integration purposes
Generating Checkout Links
Step 1: Access the Link Generator
Navigate to Developer Tools → Purchase Link Generator in your PayPro Global account.
Step 2: Configure Basic Settings
Select your product(s) and configure the following optional parameters:
| Parameter | Description |
|---|---|
| Page Template | Choose the checkout page design |
| Language | Set the checkout language |
| Currency | Pre-select the currency |
| First Name | Pre-fill customer's first name |
| Last Name | Pre-fill customer's last name |
| Pre-fill customer's email | |
| Country | Pre-select customer's country |
| City | Pre-fill customer's city |
| Address | Pre-fill customer's address |
| Payment Method | Pre-select payment method |
Step 3: Generate the Link
- Click Generate Link to create your checkout URL
- Use Test Mode checkbox to generate a test link before going live
- For products with cross-sell/upsell campaigns, choose whether to include them
Dynamic Settings Configuration
Enabling Dynamic Settings
Dynamic settings must be enabled in your product configuration: Product Setup → Step 2: Pricing Configuration → Dynamic Settings
Available Dynamic Parameters
When dynamic settings are enabled, you can customize:
1. Product Information
- Product name
- Product description
- Discount percentage
2. Dynamic Pricing
- Custom price per currency
- SKU for price tracking
- Multiple currency support
3. Dynamic Recurring Pricing (Subscriptions only)
- Custom recurring price per currency
- Recurring SKU (Available only when "First charge amount is different than recurring charges" is enabled)
4. Custom Fields
- Pass custom tracking data via
x-CustomFieldName=Valueformat - Retrieve data in webhooks via
ORDER_CUSTOM_FIELDS
5. Link Expiration
Set checkout link expiration date and time (UTC) Format: yyyy-mm-ddThh:mm:ss
Encryption Implementation
When to Use Encryption
Dynamic settings can be configured as encrypted or non-encrypted in your product setup.
Encryption is recommended for:
- Sensitive pricing information
- Preventing link tampering
- Secure promotional campaigns
Encryption Requirements
Key Length: Exactly 32 characters (256 bits)
IV Length: Exactly 16 characters (128 bits)
Algorithm: AES-256 with CBC mode and PKCS7 padding
Encoding: URL-safe Base64 (replace + with - and / with _ )
Dynamic Settings String Format
Parameter Syntax:
| Setting | Format | Example |
|---|---|---|
| Product ID | ID=XXXXX | ID=12345 |
| Product Name | Name=Value | Name=Premium Plan |
| Description | Description=Value | Description=Full access to all features |
| Discount | DiscountPercentage=XX.YY | DiscountPercentage=15.00 |
| Price | Price[CURRENCY][Amount]=XX.YY | Price[USD][Amount]=99.99 |
| Price SKU | Price[CURRENCY][Sku]=Value | Price[USD][Sku]=PLAN-USD |
| Recurring Price | RecurringPrice[CURRENCY][Amount]=XX.YY | RecurringPrice[USD][Amount]=49.99 |
| Recurring SKU | RecurringPrice[CURRENCY][Sku]=Value | RecurringPrice[USD][Sku]=REC-USD |
| Custom Field | x-FieldName=Value | x-CampaignId=SUMMER2026 |
The first currency in the query string will be used as the product's base currency.
URL Parameter Reference
Non-Encrypted Dynamic Settings
When encryption is disabled, data is passed via individual URL parameters:
https://store.payproglobal.com/checkout?
&products[i][ID]=ID
&products[i][name]=NAME
&products[i][description]=DESCRIPTION
&products[i][discountpercentage]=PERCENTAGE
&products[i][LinkExpiration]=yyyy-mm-ddThh:mm:ss
&products[i][price][CURRENCY_CODE][amount]=AMOUNT
&products[i][recurringprice][CURRENCY_CODE][amount]=AMOUNT
&products[i][price][CURRENCY_CODE][sku]=SKU
&products[i][recurringprice][CURRENCY_CODE][sku]=SKU
Hash Protection
If hash protection is enabled in your product settings, an additional parameter will be added:
&products[i][hash]=HASH_VALUE
Encrypted Dynamic Settings
When encryption is enabled, all dynamic data is passed via a single parameter:
products[N][data]=EncryptedData
Where N is the product index in the order (starting from 1).
Example:
https://store.payproglobal.com/checkout? &products[1][ID]=12345&products[1][data]=ENCRYPTED_STRING_HERE
Code Examples
C# Implementation
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class AESEncryption
{
private byte[] Key;
private byte[] IV;
public AESEncryption(string key, string iv)
{
Key = Encoding.ASCII.GetBytes(key);
IV = Encoding.ASCII.GetBytes(iv);
if (Key.Length != 32)
throw new Exception("The Key must be exactly 32 characters (256 bits)!");
if (IV.Length != 16)
throw new Exception("The IV must be exactly 16 characters (128 bits)!");
}
public string Encrypt(string plainText)
{
return ToUrlSafeBase64(EncryptBytes(plainText));
}
private byte[] EncryptBytes(string plainText)
{
using (RijndaelManaged aes = new RijndaelManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
aes.KeySize = 256;
aes.Key = Key;
aes.IV = IV;
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (MemoryStream msEncrypt = new MemoryStream())
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
swEncrypt.Close();
return msEncrypt.ToArray();
}
}
}
public string Decrypt(string cipherText)
{
return DecryptBytes(FromUrlSafeBase64(cipherText));
}
private string DecryptBytes(byte[] cipherText)
{
using (RijndaelManaged aes = new RijndaelManaged())
{
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
aes.KeySize = 256;
aes.Key = Key;
aes.IV = IV;
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
private string ToUrlSafeBase64(byte[] input)
{
return Convert.ToBase64String(input).Replace("+", "-").Replace("/", "_");
}
private byte[] FromUrlSafeBase64(string input)
{
return Convert.FromBase64String(input.Replace("-", "+").Replace("_", "/"));
}
}
// Usage Example
class Program
{
static void Main(string[] args)
{
// Your encryption key (32 characters)
string key = "abcdefghijklmnopabcdefghijklmnop";
// Your initialization vector (16 characters)
string iv = "abcdefghijklmnop";
// Your product ID from PayPro Global
int productId = 11111;
// Format your dynamic settings string
string dynamicSettings = "x-CustomerID=12345&Name=Premium Plan&" +
"Description=Annual Subscription&" +
"DiscountPercentage=10&Price[USD][Amount]=99.99&" +
"Price[USD][Sku]=PLAN-USD";
var aes = new AESEncryption(key, iv);
string encrypted = aes.Encrypt(dynamicSettings);
Console.WriteLine("Encrypted data: " + encrypted);
Console.WriteLine("Decrypted data: " + aes.Decrypt(encrypted));
// Build checkout URL with product ID and encrypted data
// Note: Product index always starts at [1], not [0]
string checkoutUrl = $"https://store.payproglobal.com/checkout?products[1][id]=
{productId}&products[1][data]={encrypted}";
Console.WriteLine("\nCheckout URL: " + checkoutUrl);
}
}
PHP Implementation
<?php
class AESEncryption {
private $key;
private $iv;
public function __construct($key, $iv) {
if (strlen($key) !== 32) {
throw new Exception("Key must be exactly 32 characters!");
}
if (strlen($iv) !== 16) {
throw new Exception("IV must be exactly 16 characters!");
}
$this->key = $key;
$this->iv = $iv;
}
public function encrypt($plainText) {
$encrypted = openssl_encrypt(
$plainText,
'AES-256-CBC',
$this->key,
OPENSSL_RAW_DATA,
$this->iv
);
return $this->toUrlSafeBase64($encrypted);
}
public function decrypt($cipherText) {
$decoded = $this->fromUrlSafeBase64($cipherText);
return openssl_decrypt(
$decoded,
'AES-256-CBC',
$this->key,
OPENSSL_RAW_DATA,
$this->iv
);
}
private function toUrlSafeBase64($data) {
return str_replace(['+', '/'], ['-', '_'], base64_encode($data));
}
private function fromUrlSafeBase64($data) {
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
}
}
// Usage Example
$key = "abcdefghijklmnopabcdefghijklmnop"; // 32 characters
$iv = "abcdefghijklmnop"; // 16 characters
$dynamicSettings = "x-CustomerID=12345&Name=Premium Plan&Description=Annual Subscription&" .
"DiscountPercentage=10&Price[USD][Amount]=99.99&" .
"Price[USD][Sku]=PLAN-USD";
$aes = new AESEncryption($key, $iv);
$encrypted = $aes->encrypt($dynamicSettings);
$productId = 11111;
echo "Encrypted: " . $encrypted . "\n";
echo "Decrypted: " . $aes->decrypt($encrypted) . "\n";
// Build checkout URL
$checkoutUrl = "https://store.payproglobal.com/checkout?products[1][id]=".$productId."&products[1][data]=" . urlencode($encrypted);
echo "Checkout URL: " . $checkoutUrl;
?>
Python Implementation
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
class AESEncryption:
def __init__(self, key, iv):
if len(key) != 32:
raise ValueError("Key must be exactly 32 characters!")
if len(iv) != 16:
raise ValueError("IV must be exactly 16 characters!")
self.key = key.encode('utf-8')
self.iv = iv.encode('utf-8')
def encrypt(self, plain_text):
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
padded_data = pad(plain_text.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return self._to_url_safe_base64(encrypted)
def decrypt(self, cipher_text):
decoded = self._from_url_safe_base64(cipher_text)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
decrypted = unpad(cipher.decrypt(decoded), AES.block_size)
return decrypted.decode('utf-8')
def _to_url_safe_base64(self, data):
return base64.b64encode(data).decode('utf-8').replace('+', '-').replace('/', '_')
def _from_url_safe_base64(self, data):
return base64.b64decode(data.replace('-', '+').replace('_', '/'))
# Usage Example
key = "abcdefghijklmnopabcdefghijklmnop" # 32 characters
iv = "abcdefghijklmnop" # 16 characters
productId = 11111
dynamic_settings = "x-CustomerID=12345&Name=Premium Plan&Description=Annual Subscription&DiscountPercentage=10&Price[USD][Amount]=99.99&Price[USD][Sku]=PLAN-USD";
aes = AESEncryption(key, iv)
encrypted = aes.encrypt(dynamic_settings)
checkoutUrl = f"https://store.payproglobal.com/checkout?products[1][id]={productId}&products[1][data]={encrypted}"
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {aes.decrypt(encrypted)}")
print(f"Checkout URL: {checkoutUrl}")
Security Best Practices
Key Management
✅ DO:
- Generate unique, cryptographically secure keys for production
- Store keys securely (environment variables, secrets management)
- Use different keys for test and production environments
- Rotate keys periodically
❌ DON'T:
- Hard-code keys in your source code
- Share keys via email or unsecured channels
- Use the example keys provided in documentation
- Reuse keys across different environments
URL Safety
- Always use URL-safe Base64 encoding (replace
+with-and/with_) - Validate expiration timestamps before generating links
- Use hash protection when available for additional security
- Consider implementing rate limiting on checkout URL generation
Testing Your Implementation
Verification Steps
- Generate a test checkout URL with encrypted dynamic settings
- Open the URL in a browser
- Verify the checkout page displays your custom values:
- Product name matches your encrypted data
- Price reflects your dynamic pricing
- Custom fields are present (check via webhooks after test purchase)
Debug Mode
Enable Test Mode in the Purchase Link Generator to:
- Test without processing real payments
- Verify encryption/decryption works correctly
- Check custom field transmission via test webhooks
Common Issues
| Issue | Solution |
|---|---|
| "Invalid data parameter" | Verify key/IV lengths (32/16 chars) and URL-safe Base64 encoding |
| Original price shows instead of dynamic | Check encryption format and parameter names |
| Link expired immediately | Ensure LinkExpiration uses UTC timezone format: yyyy-mm-ddThh:mm:ss |
| Custom fields not received | Verify x- prefix and check webhook payload |
| Encrypted data appears corrupted | Use URL-safe Base64 (replace + with - and / with _) |
Additional Resources
Support
Need help implementing dynamic checkout links? Email: [email protected]