PayPal IPN Token Verification for Secure Downloads

Author: JJustis | Published: 2026-02-11 00:32:03

PayPal IPN Token Verification

Secure download authorization using PayPal Instant Payment Notification

Introduction

This guide demonstrates how to set up a secure system where PayPal payments generate unique download tokens. After a successful payment, PayPal's IPN (Instant Payment Notification) system sends a verification to your server, which then authorizes file downloads using secure tokens.

How it works:
  1. Customer completes PayPal payment
  2. PayPal sends IPN notification to your server
  3. Your server verifies the payment with PayPal
  4. A unique download token is generated and stored
  5. Customer receives download link with token
  6. Token is verified before allowing download

Prerequisites

  • A web server with PHP 7.4+ (or Node.js/Python alternative)
  • MySQL or PostgreSQL database
  • PayPal Business account
  • SSL certificate (required for production)
  • cURL extension enabled

Step 1: Database Setup

Create Database and Tables

First, create the database structure to store payment and token information.

mysql -u root -p

Create the database and tables:

CREATE DATABASE paypal_downloads;
USE paypal_downloads;

CREATE TABLE payments (
    id INT AUTO_INCREMENT PRIMARY KEY,
    transaction_id VARCHAR(255) UNIQUE NOT NULL,
    payer_email VARCHAR(255) NOT NULL,
    payer_id VARCHAR(255),
    payment_status VARCHAR(50) NOT NULL,
    payment_amount DECIMAL(10, 2) NOT NULL,
    payment_currency VARCHAR(3) NOT NULL,
    item_name VARCHAR(255),
    item_number VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_transaction (transaction_id),
    INDEX idx_email (payer_email)
);

CREATE TABLE download_tokens (
    id INT AUTO_INCREMENT PRIMARY KEY,
    token VARCHAR(64) UNIQUE NOT NULL,
    transaction_id VARCHAR(255) NOT NULL,
    file_name VARCHAR(255) NOT NULL,
    download_count INT DEFAULT 0,
    max_downloads INT DEFAULT 3,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_downloaded_at TIMESTAMP NULL,
    is_active BOOLEAN DEFAULT TRUE,
    INDEX idx_token (token),
    INDEX idx_transaction (transaction_id),
    FOREIGN KEY (transaction_id) REFERENCES payments(transaction_id)
);

CREATE TABLE ipn_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ipn_data TEXT NOT NULL,
    verification_status VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Step 2: Configuration File

Create config.php

Create a configuration file with your database and PayPal settings.

nano /var/www/html/config.php

Add the following configuration:

<?php
// Database Configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'paypal_downloads');
define('DB_USER', 'your_db_user');
define('DB_PASS', 'your_db_password');

// PayPal Configuration
define('PAYPAL_EMAIL', 'your-paypal-email@example.com');

// Use sandbox for testing, live for production
define('PAYPAL_URL', 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr');
// For production use: https://ipnpb.paypal.com/cgi-bin/webscr

// Download Settings
define('MAX_DOWNLOADS', 3);
define('TOKEN_EXPIRY_HOURS', 48);
define('DOWNLOAD_DIR', '/var/www/html/protected_files/');

// Site Configuration
define('SITE_URL', 'https://yourdomain.com');

// Database Connection
function getDBConnection() {
    try {
        $pdo = new PDO(
            "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME,
            DB_USER,
            DB_PASS,
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
        return $pdo;
    } catch(PDOException $e) {
        error_log("Database Connection Failed: " . $e->getMessage());
        die("Database connection failed");
    }
}
?>

Step 3: IPN Listener

Create ipn_listener.php

This script receives and verifies PayPal IPN notifications, then generates download tokens.

nano /var/www/html/ipn_listener.php

Add the IPN listener code:

<?php
require_once 'config.php';

// Read POST data from PayPal
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();

foreach ($raw_post_array as $keyval) {
    $keyval = explode('=', $keyval);
    if (count($keyval) == 2) {
        $myPost[$keyval[0]] = urldecode($keyval[1]);
    }
}

// Read the IPN message sent from PayPal and prepend 'cmd=_notify-validate'
$req = 'cmd=_notify-validate';
foreach ($myPost as $key => $value) {
    $value = urlencode($value);
    $req .= "&$key=$value";
}

// Post IPN data back to PayPal to validate
$ch = curl_init(PAYPAL_URL);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $req);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Connection: Close'));

$res = curl_exec($ch);
curl_close($ch);

// Log the IPN data
$pdo = getDBConnection();
$stmt = $pdo->prepare("INSERT INTO ipn_log (ipn_data, verification_status) VALUES (?, ?)");
$stmt->execute([json_encode($myPost), $res]);

// Verify the IPN
if (strcmp($res, "VERIFIED") == 0) {
    
    // Check that payment_status is Completed
    if ($myPost['payment_status'] == 'Completed') {
        
        // Check that receiver_email is your PayPal email
        if ($myPost['receiver_email'] == PAYPAL_EMAIL) {
            
            // Process the payment
            $transaction_id = $myPost['txn_id'];
            $payer_email = $myPost['payer_email'];
            $payer_id = $myPost['payer_id'] ?? '';
            $payment_amount = $myPost['mc_gross'];
            $payment_currency = $myPost['mc_currency'];
            $item_name = $myPost['item_name'] ?? '';
            $item_number = $myPost['item_number'] ?? '';
            
            // Check if transaction already exists
            $stmt = $pdo->prepare("SELECT id FROM payments WHERE transaction_id = ?");
            $stmt->execute([$transaction_id]);
            
            if ($stmt->rowCount() == 0) {
                // Insert payment record
                $stmt = $pdo->prepare("
                    INSERT INTO payments 
                    (transaction_id, payer_email, payer_id, payment_status, 
                     payment_amount, payment_currency, item_name, item_number)
                    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                ");
                $stmt->execute([
                    $transaction_id, $payer_email, $payer_id, 'Completed',
                    $payment_amount, $payment_currency, $item_name, $item_number
                ]);
                
                // Generate download token
                $token = bin2hex(random_bytes(32));
                $file_name = $item_number; // Use item_number as filename
                $expires_at = date('Y-m-d H:i:s', strtotime('+' . TOKEN_EXPIRY_HOURS . ' hours'));
                
                $stmt = $pdo->prepare("
                    INSERT INTO download_tokens 
                    (token, transaction_id, file_name, max_downloads, expires_at)
                    VALUES (?, ?, ?, ?, ?)
                ");
                $stmt->execute([
                    $token, $transaction_id, $file_name, MAX_DOWNLOADS, $expires_at
                ]);
                
                // Send email with download link
                sendDownloadEmail($payer_email, $token, $item_name);
            }
        }
    }
} else if (strcmp($res, "INVALID") == 0) {
    // Log for investigation
    error_log("Invalid IPN: " . json_encode($myPost));
}

function sendDownloadEmail($email, $token, $item_name) {
    $download_url = SITE_URL . "/download.php?token=" . $token;
    
    $subject = "Your Download is Ready - " . $item_name;
    $message = "Thank you for your purchase!\n\n";
    $message .= "Your download link:\n";
    $message .= $download_url . "\n\n";
    $message .= "This link will expire in " . TOKEN_EXPIRY_HOURS . " hours.\n";
    $message .= "You can download the file up to " . MAX_DOWNLOADS . " times.\n";
    
    $headers = "From: noreply@yourdomain.com\r\n";
    $headers .= "Reply-To: support@yourdomain.com\r\n";
    
    mail($email, $subject, $message, $headers);
}

http_response_code(200);
?>

Step 4: Download Handler

Create download.php

This script verifies the token and serves the file download.

nano /var/www/html/download.php

Add the download handler code:

<?php
require_once 'config.php';

// Get token from URL
$token = $_GET['token'] ?? '';

if (empty($token)) {
    die("Invalid download link");
}

$pdo = getDBConnection();

// Verify token
$stmt = $pdo->prepare("
    SELECT dt.*, p.payer_email 
    FROM download_tokens dt
    JOIN payments p ON dt.transaction_id = p.transaction_id
    WHERE dt.token = ? AND dt.is_active = 1
");
$stmt->execute([$token]);
$download = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$download) {
    die("Invalid or inactive download token");
}

// Check expiration
if (strtotime($download['expires_at']) < time()) {
    die("This download link has expired");
}

// Check download count
if ($download['download_count'] >= $download['max_downloads']) {
    die("Download limit exceeded");
}

// Verify file exists
$file_path = DOWNLOAD_DIR . $download['file_name'];
if (!file_exists($file_path)) {
    die("File not found");
}

// Update download count
$stmt = $pdo->prepare("
    UPDATE download_tokens 
    SET download_count = download_count + 1,
        last_downloaded_at = NOW()
    WHERE token = ?
");
$stmt->execute([$token]);

// Serve the file
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($download['file_name']) . '"');
header('Content-Length: ' . filesize($file_path));
header('Cache-Control: no-cache, must-revalidate');
header('Expires: 0');

readfile($file_path);
exit;
?>

Step 5: PayPal Button Integration

Create product.html

Create a simple product page with a PayPal button.

nano /var/www/html/product.html

Add the PayPal button form:

<!DOCTYPE html>
<html>
<head>
    <title>Digital Product</title>
</head>
<body>
    <h1>Premium eBook - $9.99</h1>
    
    <form action="https://www.sandbox.paypal.com/cgi-bin/webscr" method="post">
        <!-- For production use: https://www.paypal.com/cgi-bin/webscr -->
        
        <input type="hidden" name="cmd" value="_xclick">
        <input type="hidden" name="business" value="your-paypal-email@example.com">
        <input type="hidden" name="item_name" value="Premium eBook">
        <input type="hidden" name="item_number" value="ebook.pdf">
        <input type="hidden" name="amount" value="9.99">
        <input type="hidden" name="currency_code" value="USD">
        
        <!-- IPN URL -->
        <input type="hidden" name="notify_url" value="https://yourdomain.com/ipn_listener.php">
        
        <!-- Return URLs -->
        <input type="hidden" name="return" value="https://yourdomain.com/thank-you.html">
        <input type="hidden" name="cancel_return" value="https://yourdomain.com/cancelled.html">
        
        <input type="submit" value="Buy Now with PayPal">
    </form>
</body>
</html>

Step 6: Server Configuration

Set File Permissions

Create and secure the protected files directory.

sudo mkdir -p /var/www/html/protected_files
sudo chown www-data:www-data /var/www/html/protected_files
sudo chmod 750 /var/www/html/protected_files

Protect Directory with .htaccess

Prevent direct access to files.

sudo nano /var/www/html/protected_files/.htaccess

Add these rules:

Order Deny,Allow
Deny from all

Set PHP File Permissions

sudo chown www-data:www-data /var/www/html/*.php
sudo chmod 644 /var/www/html/*.php

Step 7: PayPal Configuration

Enable IPN in PayPal

  1. Log in to your PayPal account
  2. Go to Account SettingsNotifications
  3. Click Update next to Instant Payment Notifications
  4. Enter your IPN URL: https://yourdomain.com/ipn_listener.php
  5. Click Receive IPN messages (Enabled)
  6. Click Save

Step 8: Testing

Use PayPal Sandbox

Create test accounts at developer.paypal.com

Test IPN Manually

Use PayPal's IPN Simulator to test your listener.

# View IPN logs
sudo tail -f /var/log/apache2/error.log

# Check database entries
mysql -u root -p paypal_downloads -e "SELECT * FROM ipn_log ORDER BY created_at DESC LIMIT 5;"
mysql -u root -p paypal_downloads -e "SELECT * FROM download_tokens ORDER BY created_at DESC LIMIT 5;"

Advanced Features

Token Status Check API

Create an endpoint to check token status.

nano /var/www/html/check_token.php
<?php
require_once 'config.php';

header('Content-Type: application/json');

$token = $_GET['token'] ?? '';

if (empty($token)) {
    echo json_encode(['error' => 'Token required']);
    exit;
}

$pdo = getDBConnection();
$stmt = $pdo->prepare("
    SELECT 
        download_count,
        max_downloads,
        expires_at,
        is_active,
        (max_downloads - download_count) as remaining_downloads,
        CASE 
            WHEN expires_at > NOW() THEN 1
            ELSE 0
        END as is_valid
    FROM download_tokens
    WHERE token = ?
");
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);

if ($result) {
    echo json_encode([
        'valid' => (bool)$result['is_valid'] && (bool)$result['is_active'],
        'remaining_downloads' => $result['remaining_downloads'],
        'expires_at' => $result['expires_at']
    ]);
} else {
    echo json_encode(['error' => 'Token not found']);
}
?>

Cleanup Expired Tokens

Create a cron job to clean up expired tokens.

nano /var/www/html/cleanup_tokens.php
<?php
require_once 'config.php';

$pdo = getDBConnection();

// Deactivate expired tokens
$stmt = $pdo->prepare("
    UPDATE download_tokens 
    SET is_active = 0 
    WHERE expires_at < NOW() AND is_active = 1
");
$stmt->execute();

echo "Cleaned up " . $stmt->rowCount() . " expired tokens\n";
?>

Add to crontab:

crontab -e
# Run cleanup daily at 3 AM
0 3 * * * /usr/bin/php /var/www/html/cleanup_tokens.php

Security Best Practices

  • Always use HTTPS in production
  • Validate all PayPal IPN data before processing
  • Store files outside web root when possible
  • Use prepared statements to prevent SQL injection
  • Implement rate limiting on download endpoints
  • Log all IPN notifications for auditing
  • Set appropriate token expiration times
  • Never expose database credentials in error messages
  • Regularly update PHP and dependencies

Troubleshooting

Check IPN is Working

# View recent IPN logs
mysql -u root -p paypal_downloads -e "SELECT * FROM ipn_log ORDER BY created_at DESC LIMIT 10;"

# Check Apache error logs
sudo tail -f /var/log/apache2/error.log

# Test file permissions
ls -la /var/www/html/protected_files/

Test Token Generation

# Check recent tokens
mysql -u root -p paypal_downloads -e "SELECT token, file_name, expires_at, download_count FROM download_tokens ORDER BY created_at DESC LIMIT 5;"

Enable PHP Error Logging

sudo nano /etc/php/8.1/apache2/php.ini

Ensure these settings:

display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log

Restart Apache:

sudo systemctl restart apache2

Going Live

Switch to Production

When ready for production, update these settings:

In config.php:

define('PAYPAL_URL', 'https://ipnpb.paypal.com/cgi-bin/webscr');

In product.html:

<form action="https://www.paypal.com/cgi-bin/webscr" method="post">