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.
- Customer completes PayPal payment
- PayPal sends IPN notification to your server
- Your server verifies the payment with PayPal
- A unique download token is generated and stored
- Customer receives download link with token
- 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
- Log in to your PayPal account
- Go to Account Settings → Notifications
- Click Update next to Instant Payment Notifications
- Enter your IPN URL:
https://yourdomain.com/ipn_listener.php - Click Receive IPN messages (Enabled)
- 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">