Stripe Subscription Payment Integration in PHP

The Stripe Subscription API provides an easy way to integrate recurring payments on the website. If you want to implement the membership subscription system on the web application, subscription payment is required for recurring billing. Stripe subscription is a quick and effective way to allow your website members to purchase a membership online using their credit cards. In Stripe subscription payment, the buyer is charged recurringly based on the specific interval. The member of your website can subscribe to a plan and make payments with their credit/debit card without leaving the website.

Stripe payment gateway provides one of the most secure and developer-friendly platforms for accepting recurring payments online. Instead of building your own card collection form, you can use the Stripe Payment Element, which supports cards and multiple payment methods while keeping sensitive payment information outside of your server.

In this tutorial, you’ll build a complete Stripe subscription system in PHP using the Stripe Payment Element for collecting payment details, Stripe.js for payment confirmation, and the Stripe PHP SDK for creating customers, subscriptions, and validating successful payments. The application also stores subscription records in a MySQL database and demonstrates how to handle webhook events for payment lifecycle updates.

By the end of this tutorial, you’ll have a production-ready subscription workflow that can be adapted for SaaS applications, membership websites, digital products, or recurring billing platforms.

Features of this Stripe Subscription in PHP Implementation:

  • Stripe Payment Element for secure payment collection
  • Stripe.js for payment confirmation
  • Stripe PHP SDK for creating customers and subscriptions
  • MySQL database for storing subscription records
  • Handling webhook events for payment lifecycle updates

Stripe API Keys

To use the Stripe API, you’ll need to obtain your API keys from the Stripe Dashboard:

  • Log in to your Stripe account.
  • Navigate to the Developers section and select API keys.
  • Copy the Publishable key and Secret key for use in your application.

Test API Keys:
Before going live, you can use the test API keys provided by Stripe to simulate transactions without processing real payments. This allows you to test your subscription system thoroughly before deploying it to production. To obtain test API keys, follow the steps below:

  • Switch to the test mode in your Stripe Dashboard (Switch to sandbox Β» Test mode).
  • Navigate to the Developers section and select API keys.
  • Copy the test Publishable key and Secret key for use in your application.
stripe-developers-api-keys-publishable-secret-codexworld

πŸ“ Project File Structure

The project file structure for the Stripe subscription integration in PHP is organized as follows:

stripe_subscription_in_php/
β”œβ”€β”€ config.php
β”œβ”€β”€ db.php
β”œβ”€β”€ index.php
β”œβ”€β”€ checkout.php
β”œβ”€β”€ subscription_handler.php
β”œβ”€β”€ status.php
β”œβ”€β”€ stripe-php/
β”œβ”€β”€ js/
|    └── checkout.js
└── css/
    └── style.css

Here’s a brief description of each file in the project structure:
config.php: Contains the configuration settings for the Stripe API keys and other necessary configurations.
db.php: Handles the database connection and provides functions for interacting with the MySQL database.
index.php: The main landing page where users can view available subscription plans and initiate the subscription process.
checkout.php: Handles the checkout process, including displaying the Stripe Payment Element and collecting payment details from users.
subscription_handler.php: Processes the subscription creation, customer management, and payment confirmation using the Stripe PHP SDK.
status.php: Displays the status of the subscription, including successful payments, failed payments, and subscription details.
stripe-php/: Contains the Stripe PHP SDK library for interacting with the Stripe API.
js/checkout.js: Contains JavaScript code for handling the Stripe Payment Element and payment confirmation.
css/style.css: Contains the CSS styles for the application, ensuring a user-friendly and visually appealing interface.

πŸš€ How Stripe Subscription Payment Works

The Stripe subscription payment process involves several key steps to ensure a seamless experience for both the website owner and the subscriber. Here’s a breakdown of how it works:

  1. Plan Creation: The website owner creates subscription plans in the MySQL database, specifying the pricing, billing interval (e.g., monthly, yearly), and any trial periods if applicable.
  2. Customer Registration: When a user decides to subscribe, they provide their personal information and payment details through the Stripe Payment Element on the checkout page. This information is securely transmitted to Stripe without exposing sensitive data to the website’s server.
  3. Subscription Creation: Upon successful payment, the application uses the Stripe PHP SDK to create a customer and a subscription in Stripe. The subscription is linked to the customer, and the billing cycle is set according to the chosen plan.
  4. Payment Confirmation: Stripe.js is used to confirm the payment and handle any additional authentication steps required by the card issuer (e.g., 3D Secure). This ensures that the payment is authorized and processed securely.
  5. Webhook Handling: Stripe sends webhook events to the application to notify it of important subscription lifecycle events, such as successful payments, failed payments, subscription cancellations, and renewals. The application listens for these events and updates the subscription records in the MySQL database accordingly.
  6. Subscription Management: Subscribers can manage their subscriptions through the website, including upgrading or downgrading plans, updating payment methods, and canceling subscriptions. The application interacts with the Stripe API to reflect these changes in real-time.

Let’s dive into the implementation details of each step to build a robust Stripe subscription system in PHP.

Step 1: Install Stripe PHP SDK

To get started with the Stripe subscription system in PHP, you need to install the Stripe PHP SDK. This SDK provides a convenient way to interact with the Stripe API and handle subscription-related operations.
You can install the Stripe PHP SDK using Composer, a dependency manager for PHP. Run the following command to install the Stripe PHP SDK:

composer require stripe/stripe-php

Note: If you don’t have Composer installed, or want to install Stripe PHP SDK without Composer, you can download our source code package. Our ready-to-use package includes all the necessary files and configurations to get you started quickly.

Step 2: Create Database Tables

To store subscription plans and subscription records, you need to create the necessary tables in your MySQL database. Below are the SQL statements to create the required tables (plans and subscriptions):

CREATE TABLE `plans` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `slug` varchar(255) DEFAULT NULL,
  `price` decimal(10,2) NOT NULL,
  `currency` varchar(10) NOT NULL DEFAULT 'usd',
  `billing_interval` varchar(20) NOT NULL DEFAULT 'month',
  `billing_interval_count` int(11) NOT NULL DEFAULT 1,
  `interval_label` varchar(100) DEFAULT NULL,
  `description` text DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `subscriptions` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `customer_name` varchar(100) NOT NULL,
  `customer_email` varchar(255) NOT NULL,
  `plan_id` int(11) NOT NULL,
  `stripe_customer_id` varchar(100) NOT NULL,
  `stripe_payment_intent_id` varchar(100) NOT NULL,
  `stripe_subscription_id` varchar(100) NOT NULL,
  `payment_status` varchar(30) NOT NULL,
  `subscription_status` varchar(30) NOT NULL,
  `amount` decimal(10,2) NOT NULL,
  `currency` varchar(3) NOT NULL,
  `started_at` datetime NOT NULL,
  `valid_until` datetime NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT current_timestamp(),
  PRIMARY KEY (`id`),
  KEY `plan_id` (`plan_id`),
  CONSTRAINT `subscriptions_ibfk_1` FOREIGN KEY (`plan_id`) REFERENCES `plans` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Insert sample subscription plans into the plans table to test the subscription system. You can use the following SQL statements to insert sample data:

INSERT INTO `plans` (`name`, `slug`, `price`, `currency`, `billing_interval`, `billing_interval_count`, `interval_label`, `description`) VALUES
('Basic Plan', 'basic-plan', 9.99, 'usd', 'week', 1, 'Weekly', 'Access to basic features and content.'),
('Standard Plan', 'standard-plan', 19.99, 'usd', 'month', 1, 'Monthly', 'Access to standard features and content, including premium articles.'),
('Premium Plan', 'premium-plan', 29.99, 'usd', 'year', 1, 'Yearly', 'Access to all features and content, including exclusive resources and priority support.');

Step 3: Configuration – Stripe API Keys and Database Settings

The configuration file (config.php) contains settings for the database connection, Stripe API keys, and application-specific settings. It is used throughout the application to access these configurations in a centralized manner.

  • Database Configuration: Set the database host, username, password, and database name to establish a connection with the MySQL database.
  • Stripe API Keys: Set the Stripe Publishable key and Secret key obtained from the Stripe Dashboard. These keys are used to authenticate API requests to Stripe.
  • Other Settings: You can include additional settings such as the application name, currency, and any other configurations required for your subscription system.
<?php 
// Start a session to manage user state and data across requests
if (session_status() === PHP_SESSION_NONE) {
    
session_start();
}

// Configuration for database connection, Stripe API keys, and application-specific settings
return [
    
'db' => [
        
'host' => 'localhost',
        
'port' => 3306,
        
'username' => 'root',
        
'password' => '',
        
'database' => 'subscription_db',
        
'charset' => 'utf8mb4'
    
],
    
'stripe' => [
        
'publishable_key' => '_replace_me_with_your_publishable_key_',
        
'secret_key' => '_replace_me_with_your_secret_key_',
        
'webhook_secret' => 'whsec_replace_me'
    
],
    
'app' => [
        
'name' => 'Stripe Subscription Checkout',
        
'currency' => 'usd',
    ]
];
?>

Step 4: Database Connection Setup

The database connection file (db.php) contains helper functions to establish a connection with the MySQL database using the MySQLi extension. It provides a reusable function to connect to the database and handle any connection errors gracefully.

  • getDbConnection(): Use to establish a database connection using the provided configuration settings. It returns a mysqli object representing the connection.
  • getDb(): Use to get a singleton instance of the database connection.
<?php 
// Load the configuration settings
$config = require __DIR__ '/config.php';

// Heper function to establish a database connection using the provided configuration settings. It returns a mysqli object representing the connection.
function getDbConnection(array $config): mysqli
{
    
$mysqli = new mysqli(
        
$config['db']['host'],
        
$config['db']['username'],
        
$config['db']['password'],
        
$config['db']['database'],
        
$config['db']['port']
    );

    if (
$mysqli->connect_errno) {
        throw new 
RuntimeException('Database connection failed: ' $mysqli->connect_error);
    }

    
$mysqli->set_charset($config['db']['charset']);

    return 
$mysqli;
}

// Helper function to get a singleton instance of the database connection. It ensures that only one connection is established and reused throughout the application.
function getDb(): mysqli
{
    static 
$db null;

    if (
$db === null) {
        global 
$config;
        
$db getDbConnection($config);
    }

    return 
$db;
}
?>

Step 5: Landing Page – Display Subscription Plans

The landing page (index.php) displays the available subscription plans to users. It retrieves the plans from the database and presents them in a user-friendly format, allowing users to select a plan and proceed to the checkout page. Each plan includes details such as the name, price, billing interval, and description.

  • Customers choose a plan, enter their name and email address, and click the “Subscribe” button.
  • Subscription creation process is initiated on the server-side through an AJAX request to the subscription_handler.php file.
  • On successful subscription creation, the user is redirected to the checkout page.

First, establish a database connection and load the configuration settings. Then, fetch the available subscription plans from the database and handle any potential errors during the process.

<?php 
// Establish a database connection
require_once __DIR__ '/db.php';
$db getDb();

// Load the configuration settings
$config = require __DIR__ '/config.php';

// Fetch the available subscription plans from the database.
try {
    
$result $db->query('SELECT * FROM plans ORDER BY price ASC');
    
$plans $result->fetch_all(MYSQLI_ASSOC);
} catch (
Throwable $e) {
    die(
$e->getMessage());
}
?>

HTML structure for displaying the subscription plans, including a form for users to enter their name and email address, and a “Subscribe” button to initiate the subscription process.

<!-- Section for selecting a subscription plan -->
<section class="card">
    <h2>Select a plan</h2>
    <div class="plan-list">
        <?php foreach ($plans as $plan): ?>
            <label class="plan-option">
                <input type="radio" name="plan_id" value="<?= (int)$plan['id'?>" <?= ($plan === $plans[0]) ? 'checked' '' ?> />
                <span>
                    <strong><?= htmlspecialchars($plan['name']) ?></strong>
                    <span><?= htmlspecialchars($plan['description']) ?></span>
                </span>
                <strong>$<?= number_format((float)$plan['price'], 2?>/<?= htmlspecialchars($plan['interval_label']) ?></strong>
            </label>
        <?php endforeach; ?>
    </div>
</section>

<!-- Section for entering customer details and submitting the subscription form -->
<section class="card">
    <div class="summary">
        <div>Selected plan</div>
        <div id="selected-plan-name" class="amount"></div>
        <div>Amount due</div>
        <div id="selected-plan-price" class="amount"></div>
    </div>

    <form id="subscription-form">
        <div class="form-group">
            <label for="customer_name">Full name</label>
            <input id="customer_name" name="customer_name" placeholder="John Doe" required />
        </div>
        <div class="form-group">
            <label for="customer_email">Email</label>
            <input id="customer_email" name="customer_email" type="email" placeholder="john.doe@example.com" required />
        </div>
        <button id="submit-button" type="submit">Subscribe securely</button>
        <div id="status-message" class="status-message"></div>
    </form>
</section>

JavaScript code to handle the subscription form submission, send an AJAX request to the server-side script, and manage the response.

<script>
const plans = <?= json_encode($plans?>;
const planOptions = document.querySelectorAll('input[name="plan_id"]');
const selectedPlanName = document.getElementById('selected-plan-name');
const selectedPlanPrice = document.getElementById('selected-plan-price');

// Function to update the displayed selected plan details based on user selection
function updateSelectedPlan() {
    const selectedPlanId = document.querySelector('input[name="plan_id"]:checked').value;
    const selectedPlan = plans.find(plan => plan.id == selectedPlanId);
    selectedPlanName.textContent = selectedPlan.name;
    selectedPlanPrice.textContent = `$${parseFloat(selectedPlan.price).toFixed(2)}/${selectedPlan.interval_label}`;
}

// Add event listeners to update the selected plan details when the user changes their selection
planOptions.forEach((option) => option.addEventListener('change', async () => {
    updateSelectedPlan();
}));

// Initialize the display with the first plan
updateSelectedPlan();

// Handle the subscription form submission and send the data to the server for processing
const form = document.getElementById('subscription-form');
form.addEventListener('submit', async (event) => {
    event.preventDefault();
    const submitButton = form.querySelector('#submit-button');
    submitButton.disabled = true;
    submitButton.textContent = 'Processing...';
    const statusMessage = document.getElementById('status-message');

    try {
        const formData = new FormData(form);
        const selectedPlanId = document.querySelector('input[name="plan_id"]:checked').value;
        formData.append('plan_id', selectedPlanId);
        formData.append('action', 'create_subscription');

        const response = await fetch('subscription_handler.php', {
            method: 'POST',
            body: formData
        });
        const result = await response.json();

        if (result.success) {
            statusMessage.classList.add('success');
            statusMessage.textContent = 'Subscription created successfully! Redirecting...';
            window.location.href = `checkout.php?subscription_id=${encodeURIComponent(result.subscription_id)}`;
        } else {
            throw new Error(result.error || 'An error occurred while creating the subscription.');
        }
    } catch (error) {
        statusMessage.classList.add('error');
        statusMessage.textContent = `Error: ${error.message}`;
    } finally {
        submitButton.disabled = false;
        submitButton.textContent = 'Subscribe securely';
    }
});
</script>

Step 6: Checkout Page – Stripe Payment Element

The checkout page (checkout.php) initializes the Stripe Payment Element, allowing users to enter their payment information securely. It handles the payment processing and redirects the user to the status page upon completion.

First, establish a connection to the database and retrieve the selected plan details based on the subscription_data passed in the SESSION. If the plan is not found, display an error message.

<?php 
// Establish a database connection
require_once __DIR__ '/db.php';
$db getDb();

// Load the configuration settings
$config = require __DIR__ '/config.php';

// Get the subscription ID from the query parameters
$subscriptionId = (string)($_GET['subscription_id'] ?? '');

// Check if the subscription ID is valid and matches the session data; if not, redirect to the index page
if (!$subscriptionId || !isset($_SESSION['subscription_data']) || $_SESSION['subscription_data']['subscription_id'] !== $subscriptionId) {
    
header('Location: index.php');
    exit;
}

// Fetch selected plan details from the database
try {
    
$stmt $db->prepare("SELECT * FROM plans WHERE id = ?");
    
$stmt->bind_param('i'$_SESSION['subscription_data']['plan_id']);
    
$stmt->execute();
    
$plan $stmt->get_result()->fetch_assoc();
    
$stmt->close();
} catch (
Throwable $e) {
    die(
$e->getMessage());
}
?>

Include the Stripe.js library for handling the payment confirmation and the Stripe Payment Element for collecting payment details.

<script src="https://js.stripe.com/dahlia/stripe.js"></script>

Include the JavaScript file (js/checkout.js) that contains the logic for initializing the Stripe Payment Element, handling form submission, and confirming the payment. Pass stripe.publishable_key and client_secret to the JavaScript file using custom attributes.

<script src="js/checkout.js" defer STRIPE_PUBLISHABLE_KEY="<?= htmlspecialchars($config['stripe']['publishable_key']) ?>" STRIPE_CLIENT_SECRET="<?= htmlspecialchars($_SESSION['subscription_data']['client_secret']) ?>"></script>

Define HTML elements to display selected plan details, customer information, and payment form with Stripe Payment Element.

<!-- Section displaying the selected plan details and customer information -->
<section class="card">
    <div class="summary">
        <div>Selected plan</div>
        <div class="amount"><?= htmlspecialchars($plan['name']) ?></div>
        <div>Amount due</div>
        <div class="amount">$<?= number_format((float)$plan['price'], 2?> / <?= htmlspecialchars($plan['interval_label']) ?></div>
        <div>Customer details</div>
        <div class="amount"><?= htmlspecialchars($_SESSION['subscription_data']['customer_name']) ?> (<?= htmlspecialchars($_SESSION['subscription_data']['customer_email']) ?>)</div>
    </div>
</section>

<section class="card">
    <!-- Form for entering payment information and completing the subscription -->
    <form id="subscription-form">
        <div class="form-group">
            <div id="payment-element" class="payment-card"></div>
        </div>
        <button id="submit-button" type="submit">Complete Subscription</button>
    </form>

    <!-- Display processing notification -->
    <div id="frmProcess" class="hidden">
        <span class="ring"></span> Processing...
    </div>

    <!-- Display re-initiate button -->
    <div id="payReinit" class="hidden">
        <button onClick="window.location.href=window.location.href.split('?')[0]">Re-initiate Payment</button>
    </div>

    <div id="status-message" class="status-message"></div>
</section>

Step 7: Checkout Handler Client-Side JS Script

The client-side JavaScript file (js/checkout.js) handles the interaction with the Stripe Payment Element and manages the payment confirmation process. It initializes the Stripe object, mounts the Payment Element, and listens for form submission events.

  • On form submission, it prevents the default behavior and calls the Stripe.js confirmPayment method to confirm the payment using the provided client_secret.
  • It handles the response from Stripe, checking for any errors or successful payment confirmation.
  • If the payment is successful, it sends an AJAX request to the subscription_handler.php server-side script to validate the subscription and insert it into the database.
  • If there is an error, it displays the error message to the user.
// Retrieve the Stripe publishable key and client secret from the script's attributes
const STRIPE_PUBLISHABLE_KEY = document.currentScript.getAttribute('STRIPE_PUBLISHABLE_KEY');
const STRIPE_CLIENT_SECRET = document.currentScript.getAttribute('STRIPE_CLIENT_SECRET');

// Initialize Stripe with the publishable key
const stripe = Stripe(STRIPE_PUBLISHABLE_KEY);

let elements = null;
let paymentElement = null;
const subscrFrm = document.querySelector("#subscription-form");

// Get payment_intent_client_secret param from URL
const clientSecretParam = new URLSearchParams(window.location.search).get(
    "payment_intent_client_secret"
);

// Check whether the payment_intent_client_secret is already exist in the URL
setProcessing(true);
if(!clientSecretParam){
    setProcessing(false);
    
    // Initialize the payment element with the client secret
    initializePaymentElement(STRIPE_CLIENT_SECRET).catch(error => {
        console.error('Error initializing payment element:', error);
        showMessage('Failed to initialize payment form. Please try again.');
    });
}

// Check the PaymentIntent status if the client secret is present in the URL
checkPaymentIntentStatus();

// Function to initialize the Stripe Payment Element
async function initializePaymentElement(clientSecret) {
    elements = stripe.elements({ clientSecret });
    paymentElement = elements.create('payment', {
        layout: "tabs"
    });
    paymentElement.mount("#payment-element");
}

// Handle the form submission for subscription
subscrFrm.addEventListener("submit", async (event) => {
    event.preventDefault();
    
    const submitButton = subscrFrm.querySelector('#submit-button');
    submitButton.disabled = true;
    submitButton.textContent = 'Processing...';
    showMessage('Preparing your subscription...', 'info');

    const { error } = await stripe.confirmPayment({
        elements,
        confirmParams: {
            return_url: window.location.href,
        }
    });

    if (error) {
        console.error('Error confirming payment:', error);
        showMessage(error.message || 'Payment confirmation failed. Please try again.');
        submitButton.disabled = false;
        submitButton.textContent = 'Complete Subscription';
    }
});

// Function to check the status of the PaymentIntent and handle the subscription accordingly
async function checkPaymentIntentStatus() {
    const clientSecret = new URLSearchParams(window.location.search).get("payment_intent_client_secret");
    const subscriptionId = new URLSearchParams(window.location.search).get("subscription_id");

    if(!clientSecret || !subscriptionId) {
        return;
    }

    const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
    if (paymentIntent) {
        switch (paymentIntent.status) {
            case 'succeeded':
                fetch('subscription_handler.php', {
                    method: "POST",
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
                    body: new URLSearchParams({
                        action: 'validate_subscription',
                        payment_intent_id: paymentIntent.id,
                        subscription_id: subscriptionId
                    }).toString()
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        showMessage('Payment succeeded! Your subscription is now active.', 'success');
                        window.location.href = 'status.php?subscription_id='+data.subscription_id;
                    } else {
                        throw new Error(data.error || 'Payment succeeded, but failed to update subscription status. Please contact support.');
                    }
                })
                .catch(error => {
                    showMessage(error.message || 'Payment succeeded, but failed to update subscription status. Please contact support.');
                    setReinit();
                });
                break;

            case 'processing':
                showMessage('Payment processing. We\'ll update you when payment is received.', 'info');
                setReinit();
                break;

            case 'requires_payment_method':
                showMessage('Payment failed. Please try another payment method.');
                setReinit();
                break;

            default:
                showMessage('Something went wrong, please try again.');
                setReinit();
                break;
        }
    } else {
        showMessage('Failed to retrieve payment information. Please try again.');
        setReinit();
    }
}


// Helper function to display messages to the user
function showMessage(message, type = 'error') {
    const messageContainer = document.querySelector("#status-message");
    messageContainer.classList.add(type);
    messageContainer.textContent = message;
}

// Helper function to show or hide the payment form and processing message
function setProcessing(isProcessing) {
    if (isProcessing) {
        subscrFrm.classList.add("hidden");
        document.querySelector("#frmProcess").classList.remove("hidden");
    } else {
        subscrFrm.classList.remove("hidden");
        document.querySelector("#frmProcess").classList.add("hidden");
    }
}

// Helper function to show the reinitialization message and hide the payment form
function setReinit() {
    subscrFrm.classList.add("hidden");
    document.querySelector("#frmProcess").classList.add("hidden");
    document.querySelector("#payReinit").classList.remove("hidden");
}

Step 8: Subscription Handler – Server-Side Logic

The server-side script (subscription_handler.php) handles the subscription creation, customer management, and payment confirmation using the Stripe PHP SDK. It processes the AJAX request from the checkout page, creates a customer and subscription in Stripe, and validates the payment status.

  • The create_subscription action handles the creation of customer, price, and subscription using the Stripe PHP SDK.
    • It retrieves the customer information and selected plan details from the request.
    • It creates a new customer in Stripe using the provided name and email address.
    • It creates a new subscription in Stripe for the customer, specifying the selected plan and billing interval.
    • It returns the client secret to the client-side script for payment confirmation.
  • The validate_subscription action handles the validation of the subscription after payment confirmation.
    • It retrieves the subscription ID and payment intent ID from the request.
    • It checks the payment status of the subscription using the Stripe API.
    • If the payment is successful, it inserts the subscription record into the MySQL database.
    • It returns a success response to the client-side script, which redirects the user to the status page.
<?php 
// Include the database connection helper functions
require_once __DIR__ '/db.php';

// Load the configuration settings
$config = require __DIR__ '/config.php';

// Include the Stripe PHP library and initialize the Stripe client with the secret key from the configuration
require_once __DIR__ '/stripe-php/init.php';
$stripe = new \Stripe\StripeClient($config['stripe']['secret_key']);

// Check if the request method is POST; if not, return a 405 Method Not Allowed response
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    
http_response_code(405);
    echo 
json_encode(['error' => 'Method not allowed']);
    exit;
}

// Establish a database connection using the getDb function and handle any connection errors
try {
    
$db getDb();
} catch (
RuntimeException $e) {
    
http_response_code(500);
    echo 
json_encode(['success' => false'error' => $e->getMessage()]);
    exit;
}

// Retrieve the action parameter from the POST request to determine which operation to perform
$action $_POST['action'] ?? '';

try {
    
// Handle the 'create_subscription' action to create a new subscription
    
if ($action === 'create_subscription') {
        
// Retrieve user input from the POST request
        
$customerName trim($_POST['customer_name'] ?? '');
        
$customerEmail trim($_POST['customer_email'] ?? '');
        
$planId = (int)($_POST['plan_id'] ?? 0);

        
// Validate the required fields; if any are missing, return an error response
        
if (!$customerName || !$customerEmail || !$planId) {
            echo 
json_encode(['success' => false'error' => 'Please complete all required fields.']);
            exit;
        }

        
// Search for an existing customer in Stripe by email to avoid creating duplicate customers
        
$existingCustomers $stripe->customers->search(['query' => "email:'{$customerEmail}'"'limit' => 1]);
        if (
count($existingCustomers->data) > 0) {
            
$customer $existingCustomers->data[0];
        } else {
            
// If no existing customer is found, create a new customer in Stripe
            
$customer $stripe->customers->create([
                
'email' => $customerEmail,
                
'name' => $customerName,
                
'metadata' => ['source' => 'codexworld_subscription_demo']
            ]);
        }

        
// Fetch the selected plan details from the database
        
$stmt $db->prepare('SELECT * FROM plans WHERE id = ?');
        
$stmt->bind_param('i'$planId);
        
$stmt->execute();
        
$plan $stmt->get_result()->fetch_assoc();
        
$stmt->close();

        if (!
$plan) {
            echo 
json_encode(['success' => false'error' => 'Selected plan does not exist.']);
            exit;
        }

        
// Create a new price for the plan in Stripe
        
$billingInterval $plan['billing_interval'] ?: 'month';
        
$intervalCount max(1, (int)($plan['billing_interval_count'] ?: 1));
        
$amount round($plan['price'] * 100);
        
$price $stripe->prices->create([
            
'unit_amount' => $amount,
            
'currency' => $plan['currency'],
            
'recurring' => ['interval' => $billingInterval'interval_count' => $intervalCount],
            
'product_data' => ['name' => $plan['name']]
        ]);
        
$priceId $price->id;

        
// Create a subscription for the customer with the selected plan
        
$subscription $stripe->subscriptions->create([
            
'customer' => $customer->id,
            
'items' => [['price' => $priceId]],
            
'payment_behavior' => 'default_incomplete',
            
'payment_settings' => ['save_default_payment_method' => 'on_subscription'],
            
'billing_mode' => ['type' => 'flexible'],
            
'expand' => ['latest_invoice.confirmation_secret'],
            
'metadata' => ['plan_id' => (int) $plan['id']]
        ]);

        
// Store subscription details in session for later use
        
$_SESSION['subscription_data'] = [
            
'subscription_id' => $subscription->id,
            
'client_secret' => $subscription->latest_invoice->confirmation_secret->client_secret,
            
'customer_id' => $customer->id,
            
'plan_id' => $planId,
            
'customer_name' => $customerName,
            
'customer_email' => $customerEmail
        
];

        
// Return a success response for further processing on the frontend
        
echo json_encode([
            
'success' => true,
            
'subscription_id' => $subscription->id,
            
'client_secret' => $subscription->latest_invoice->confirmation_secret->client_secret
        
]);
        exit;
    }

    
// Handle the 'validate_subscription' action to validate the subscription after payment confirmation
    
if ($action === 'validate_subscription') {
        
$paymentIntentId $_POST['payment_intent_id'] ?? '';
        
$subscriptionId $_POST['subscription_id'] ?? '';

        if (!
$paymentIntentId || !$subscriptionId) {
            echo 
json_encode(['success' => false'error' => 'Missing payment intent ID or subscription ID.']);
            exit;
        }

        
// Retrieve the PaymentIntent from Stripe
        
$paymentIntent $stripe->paymentIntents->retrieve($paymentIntentId);

        
// Check if the PaymentIntent is successful
        
if ($paymentIntent->status === 'succeeded') {
            
// Retrieve the subscription details from Stripe
            
$subscription $stripe->subscriptions->retrieve($subscriptionId);

            
// Retrieve customer details from Stripe using the customer ID associated with the subscription
            
$customer $stripe->customers->retrieve($subscription->customer);

            
// Check if the subscription already exists in the database
            
$stmt $db->prepare('SELECT id FROM subscriptions WHERE stripe_subscription_id = ?');
            
$stmt->bind_param('s'$subscriptionId);
            
$stmt->execute();
            
$stmt->store_result();
            if (
$stmt->num_rows 0) {
                
$stmt->close();
                echo 
json_encode(['success' => true'subscription_id' => $subscriptionId]);
                exit;
            }

            
// Insert subscription details into the database
            
$stmt $db->prepare('INSERT INTO subscriptions (customer_name, customer_email, plan_id, stripe_customer_id, stripe_payment_intent_id, stripe_subscription_id, payment_status, subscription_status, amount, currency, started_at, valid_until) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
            
$started_at date('Y-m-d H:i:s'$subscription->items->data[0]->current_period_start);
            
$valid_until date('Y-m-d H:i:s'$subscription->items->data[0]->current_period_end);
            
$amount = ($paymentIntent->amount 100); // Convert from cents to dollars
            
$stmt->bind_param(
                
'ssisssssdsss',
                
$customer->name,
                
$customer->email,
                
$subscription->metadata->plan_id,
                
$customer->id,
                
$paymentIntent->id,
                
$subscription->id,
                
$paymentIntent->status,
                
$subscription->status,
                
$amount,
                
$paymentIntent->currency,
                
$started_at,
                
$valid_until
            
);
            
$stmt->execute();
            
$insertId $stmt->insert_id;
            
$stmt->close();

            if (
$insertId) {
                
// Clear subscription data from session after successful insertion
                
unset($_SESSION['subscription_data']);
            }

            echo 
json_encode(['success' => true'subscription_id' => $subscription->id'insert_id' => $insertId]);
            exit;
        } else {
            echo 
json_encode(['success' => false'error' => 'Payment not successful.']);
            exit;
        }
    }

    
http_response_code(400);
    echo 
json_encode(['error' => 'Unsupported action.']);
} catch (
Throwable $e) {
    
http_response_code(500);
    echo 
json_encode(['error' => 'Stripe request failed: ' $e->getMessage()]);
}

?>

Step 9: Status Page – Display Subscription Status

The status page (status.php) displays the status of the subscription, including successful payments, failed payments, and subscription details. It retrieves the subscription information from the database and presents it to the user in a clear and organized manner. The page provides feedback on the subscription process and allows users to view their subscription details, including the plan name, payment status, and subscription validity.

<?php 
// Establish a database connection
require_once __DIR__ '/db.php';
$db getDb();

// Load the configuration settings
$config = require __DIR__ '/config.php';

// Retrieve the subscription ID from the query parameters
$subscriptionId = (string)($_GET['subscription_id'] ?? '');

// Fetch the subscription details from the database if a valid subscription ID is provided
if ($subscriptionId) {
    
$stmt $db->prepare("SELECT s.*, p.name AS plan_name, p.price AS plan_price, p.interval_label FROM subscriptions s JOIN plans p ON s.plan_id = p.id WHERE s.stripe_subscription_id = ?");
    
$stmt->bind_param('s'$subscriptionId);
    
$stmt->execute();
    
$subscription $stmt->get_result()->fetch_assoc();
    
$stmt->close();
} else {
    
$subscription null;
}
?> <?php if ($subscription): ?> <h1 class="success">Subscription successful</h1> <p>Your subscription is now active and your billing details have been recorded.</p> <div class="meta"> <div><strong>Customer:</strong> <?= htmlspecialchars($subscription['customer_name']) ?></div> <div><strong>Email:</strong> <?= htmlspecialchars($subscription['customer_email']) ?></div> <div><strong>Plan:</strong> <?= htmlspecialchars($subscription['plan_name']) ?> - $<?= number_format((float)$subscription['plan_price'], 2?> / <?= htmlspecialchars($subscription['interval_label']) ?></div> <div><strong>Subscription ID:</strong> <?= htmlspecialchars($subscription['stripe_subscription_id']) ?></div> <div><strong>Subscription Amount:</strong> $<?= number_format((float)$subscription['amount'], 2?> <?= strtoupper($subscription['currency']) ?></div> <div><strong>Status:</strong> <?= htmlspecialchars($subscription['subscription_status']) ?></div> <div><strong>Validity Period:</strong> <?= htmlspecialchars($subscription['started_at']) ?> to <?= htmlspecialchars($subscription['valid_until']) ?></div> </div> <a href="index.php"><button type="button">Back to homepage</button></a> <?php else: ?> <h1 class="error">We couldn't find that subscription</h1> <p>Please return to the homepage and try again.</p> <a href="index.php"><button type="button">Return to homepage</button></a> <?php endif: ?>

πŸ‘‰ We have completed the implementation of the Stripe subscription payment integration. Now it’s time to test the system and ensure everything is working as expected.

Test Card Numbers

To test the Stripe subscription payment integration, you can use the following test card numbers provided by Stripe. These card numbers simulate different scenarios, such as successful payments, failed payments, and authentication requirements. Use these test cards in the checkout page to verify the functionality of your subscription system.

  • Successful Payment: Use the card number 4242 4242 4242 4242 with any future expiration date, any CVC, and any billing ZIP code to simulate a successful payment.
  • Failed Payment: Use the card number 4000 0000 0000 9995 to simulate a failed payment due to insufficient funds. Use any future expiration date, any CVC, and any billing ZIP code.
  • Authentication Required: Use the card number 4000 00250000 0002 to simulate a payment that requires authentication (3D Secure). Use any future expiration date, any CVC, and any billing ZIP code.

Make Stripe Subscription Live

To make your Stripe subscription system live, you need to replace the test API keys (in config.php) with your actual Stripe API keys from the Stripe Dashboard.

return [ 
    
'stripe' => [
        
'publishable_key' => 'pk_live_your_stripe_publishable_key_here',
        
'secret_key' => 'sk_live_your_stripe_secret_key_here'
    
]
];

πŸŽ‰ Conclusion

Congratulations! You have successfully built a complete recurring subscription payment system in PHP using Stripe Payment Element, Stripe.js, and the Stripe PHP SDK.

The application demonstrates a modern subscription workflow by creating Stripe customers and subscriptions on the server, securely collecting payment information with the Payment Element, confirming payments using Stripe.js, validating successful PaymentIntents, storing subscription records in MySQL, and preparing your application for ongoing subscription management through webhooks.

You can further extend this project by adding features such as subscription upgrades and downgrades, customer billing portals, coupon and promotion code support, trial periods, subscription cancellation, invoice history, and email notifications. These enhancements make the solution suitable for SaaS platforms, membership websites, digital services, and any application that requires secure recurring billing.

Looking for expert assistance to implement or extend this script’s functionality? Submit a Service Request

15 Comments

  1. Michael Said...
  2. Artem Said...
  3. Artem Said...
  4. Charles Said...
  5. Mukesh Said...
  6. Magnus Said...
  7. Sourav Kar Said...
  8. Keith Said...
  9. Raju Said...
  10. Nicolas Said...
  11. NicolΓ² Peroni Said...
  12. Pragna Said...
  13. Martin Teefy Said...
    • CodexWorld Said...

Leave a reply

construction Need this implemented in your project? Request Implementation Help β†’ keyboard_double_arrow_up