• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How to securely integrate Stripe Payment webhook endpoints into WordPress custom plugins using Shortcode API

How to securely integrate Stripe Payment webhook endpoints into WordPress custom plugins using Shortcode API

Securing Stripe Webhook Endpoints in WordPress Custom Plugins

Integrating Stripe webhooks into WordPress custom plugins requires a robust approach to security. This guide details how to implement a secure webhook endpoint using WordPress’s Shortcode API, ensuring data integrity and preventing unauthorized access. We’ll focus on verifying webhook signatures and handling events within the WordPress environment.

Prerequisites

  • A WordPress installation with a custom plugin.
  • Stripe account with API keys.
  • Basic understanding of PHP and WordPress plugin development.
  • Composer installed for dependency management (optional but recommended).

Setting Up the Stripe Webhook Endpoint

We’ll create a shortcode that acts as our webhook endpoint. This shortcode will be registered to a specific URL that Stripe can send POST requests to. It’s crucial to ensure this endpoint is accessible publicly but protected from unauthorized access.

Registering the Shortcode

In your custom plugin’s main PHP file (e.g., my-stripe-plugin.php), add the following code to register a shortcode that will handle the webhook logic.

/**
 * Plugin Name: My Stripe Webhook Plugin
 * Description: Securely integrates Stripe webhooks into WordPress.
 * Version: 1.0
 * Author: Your Name
 */

// Prevent direct access to the file.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Registers the Stripe webhook shortcode.
 */
function my_stripe_webhook_shortcode() {
    // This function will contain the webhook processing logic.
    // For now, we'll just return an empty string as the shortcode itself
    // won't be displayed on a page. The endpoint is accessed via its URL.
    return '';
}
add_shortcode( 'stripe_webhook_handler', 'my_stripe_webhook_shortcode' );

/**
 * Hook into WordPress to process the webhook request.
 * This ensures the webhook is processed even if no page with the shortcode is loaded.
 */
function process_stripe_webhook() {
    // Check if the request is for our specific webhook endpoint.
    // We'll use a query parameter to identify the webhook request.
    // Example URL: https://yourdomain.com/?stripe_webhook=true
    if ( isset( $_GET['stripe_webhook'] ) && $_GET['stripe_webhook'] === 'true' ) {
        // Ensure it's a POST request.
        if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
            http_response_code( 405 ); // Method Not Allowed
            wp_die( 'Method Not Allowed' );
        }

        // Load the webhook processing logic.
        handle_stripe_webhook_request();
    }
}
add_action( 'init', 'process_stripe_webhook' );

/**
 * Core webhook processing function.
 */
function handle_stripe_webhook_request() {
    // Webhook processing logic goes here.
    // This function will be called by process_stripe_webhook.
    // We'll implement signature verification and event handling next.

    // For now, a placeholder response.
    header( 'Content-Type: application/json' );
    echo json_encode( array( 'received' => true ) );
    exit; // Important to exit after processing.
}

In this setup, we’re using the init action hook to intercept requests. The presence of a specific query parameter (?stripe_webhook=true) and a POST request method will trigger our webhook handler. This prevents the shortcode from being processed on regular page loads and ensures only Stripe can trigger the handler.

Verifying the Stripe Signature

Stripe signs its webhook requests using your webhook signing secret. Verifying this signature is paramount to ensure the request genuinely originated from Stripe and hasn’t been tampered with. You can find your webhook signing secret in your Stripe Dashboard under Developers > Webhooks.

Implementing Signature Verification

We’ll use the official Stripe PHP library for signature verification. If you’re not using Composer, you’ll need to download the library and include it manually. For Composer users, add it to your project:

composer require stripe/stripe-php

Then, include the Composer autoloader in your plugin’s main file:

// In your plugin's main file (e.g., my-stripe-plugin.php)
require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php'; // Adjust path if needed

Now, modify the handle_stripe_webhook_request function to include signature verification:

/**
 * Core webhook processing function with signature verification.
 */
function handle_stripe_webhook_request() {
    // Retrieve your webhook signing secret from WordPress options or constants.
    // NEVER hardcode secrets directly in the plugin file.
    $stripe_webhook_secret = get_option( 'my_stripe_webhook_secret' ); // Example: store in WP options

    if ( ! $stripe_webhook_secret ) {
        error_log( 'Stripe webhook secret not configured.' );
        http_response_code( 500 ); // Internal Server Error
        wp_die( 'Server configuration error.' );
    }

    $payload = @file_get_contents( 'php://input' );
    $sig_header = null;

    // Attempt to get the signature header from common locations.
    if ( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ) {
        $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
    } elseif ( function_exists( 'getallheaders' ) ) {
        $headers = getallheaders();
        if ( isset( $headers['Stripe-Signature'] ) ) {
            $sig_header = $headers['Stripe-Signature'];
        }
    }

    if ( ! $sig_header ) {
        error_log( 'Stripe signature header missing.' );
        http_response_code( 400 ); // Bad Request
        wp_die( 'Bad Request: Signature header missing.' );
    }

    try {
        $event = \Stripe\Webhook::constructEvent(
            $payload, $sig_header, $stripe_webhook_secret
        );
    } catch ( \UnexpectedValueException $e ) {
        // Invalid payload
        error_log( 'Stripe webhook payload is invalid: ' . $e->getMessage() );
        http_response_code( 400 ); // Bad Request
        wp_die( 'Bad Request: Invalid payload.' );
    } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
        // Invalid signature
        error_log( 'Stripe webhook signature verification failed: ' . $e->getMessage() );
        http_response_code( 400 ); // Bad Request
        wp_die( 'Bad Request: Invalid signature.' );
    }

    // Handle the event
    header( 'Content-Type: application/json' );
    handle_stripe_event( $event );
}

/**
 * Processes the Stripe event object.
 *
 * @param object $event The Stripe event object.
 */
function handle_stripe_event( $event ) {
    // Log the received event type for debugging.
    error_log( "Stripe webhook received event type: " . $event->type );

    // Handle the event by type.
    switch ( $event->type ) {
        case 'payment_intent.succeeded':
            $paymentIntent = $event->data->object;
            // Process successful payment intent.
            process_payment_intent_succeeded( $paymentIntent );
            break;
        case 'payment_intent.payment_failed':
            $paymentIntent = $event->data->object;
            // Process failed payment intent.
            process_payment_intent_failed( $paymentIntent );
            break;
        // ... handle other event types as needed
        default:
            // Unexpected event type
            error_log( 'Received unknown Stripe event type: ' . $event->type );
    }

    // Return a 200 response to acknowledge receipt of the event.
    echo json_encode( array( 'received' => true ) );
    exit; // Important to exit after processing.
}

/**
 * Placeholder function for processing a successful payment intent.
 *
 * @param object $paymentIntent The Stripe PaymentIntent object.
 */
function process_payment_intent_succeeded( $paymentIntent ) {
    // Example: Update order status in your WordPress database.
    // You'll need to associate the payment intent with an order.
    // This might involve storing the paymentIntent->id in your order meta.
    $order_id = get_order_id_from_payment_intent( $paymentIntent->id ); // Implement this function
    if ( $order_id ) {
        // Update order status, send confirmation emails, etc.
        update_post_meta( $order_id, '_stripe_payment_status', 'succeeded' );
        error_log( "PaymentIntent " . $paymentIntent->id . " succeeded for order " . $order_id );
    } else {
        error_log( "Could not find order for PaymentIntent " . $paymentIntent->id );
    }
}

/**
 * Placeholder function for processing a failed payment intent.
 *
 * @param object $paymentIntent The Stripe PaymentIntent object.
 */
function process_payment_intent_failed( $paymentIntent ) {
    // Example: Update order status to failed.
    $order_id = get_order_id_from_payment_intent( $paymentIntent->id ); // Implement this function
    if ( $order_id ) {
        update_post_meta( $order_id, '_stripe_payment_status', 'failed' );
        error_log( "PaymentIntent " . $paymentIntent->id . " failed for order " . $order_id );
    } else {
        error_log( "Could not find order for PaymentIntent " . $paymentIntent->id );
    }
}

/**
 * Placeholder function to retrieve order ID from PaymentIntent ID.
 * You'll need to implement this based on how you store this association.
 *
 * @param string $payment_intent_id The Stripe PaymentIntent ID.
 * @return int|false The WordPress order ID or false if not found.
 */
function get_order_id_from_payment_intent( $payment_intent_id ) {
    // Example: Query your orders table for a meta field.
    // This is a simplified example. You might need a more complex query.
    $args = array(
        'post_type'      => 'shop_order', // Assuming WooCommerce orders
        'post_status'    => 'any',
        'meta_key'       => '_stripe_payment_intent_id',
        'meta_value'     => $payment_intent_id,
        'posts_per_page' => 1,
    );
    $orders = get_posts( $args );

    if ( ! empty( $orders ) ) {
        return $orders[0]->ID;
    }

    return false;
}

Key points in this section:

  • Retrieve Secret Securely: The webhook signing secret is retrieved using get_option(). It’s crucial to store this sensitive information securely, ideally in WordPress options or environment variables, not directly in the code.
  • Get Raw Payload: file_get_contents('php://input') is used to get the raw request body, which is necessary for signature verification.
  • Get Signature Header: We attempt to retrieve the Stripe-Signature header from both $_SERVER and using getallheaders() for broader compatibility.
  • Stripe Library: \Stripe\Webhook::constructEvent() performs the actual signature verification. It throws exceptions for invalid payloads or signatures, which we catch and handle.
  • Event Handling: The handle_stripe_event() function dispatches to specific handler functions based on the $event->type.
  • Acknowledge Receipt: A 200 OK response is sent back to Stripe immediately after verification and before processing. This tells Stripe the webhook was received successfully, preventing retries. The actual processing happens asynchronously.
  • Exit: exit; is used to ensure no further WordPress output interferes with the JSON response.

Storing the Webhook Signing Secret

You need a way to securely store your Stripe webhook signing secret within your WordPress installation. The recommended method is to use WordPress’s built-in options API.

Adding a Settings Page (Recommended)

For a production-ready plugin, you should create an admin settings page where users can input their webhook signing secret. This avoids manual database edits.

// Add a menu item to the admin menu.
function my_stripe_plugin_menu() {
    add_options_page(
        'My Stripe Plugin Settings',
        'Stripe Webhook',
        'manage_options',
        'my-stripe-plugin',
        'my_stripe_plugin_settings_page'
    );
}
add_action( 'admin_menu', 'my_stripe_plugin_menu' );

// Render the settings page.
function my_stripe_plugin_settings_page() {
    // Check user capabilities.
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    // Save settings if form submitted.
    if ( isset( $_POST['my_stripe_webhook_secret'] ) ) {
        check_admin_referer( 'my_stripe_plugin_save_settings' );
        $secret = sanitize_text_field( $_POST['my_stripe_webhook_secret'] );
        update_option( 'my_stripe_webhook_secret', $secret );
        ?>
        <div class="notice notice-success is-dismissible">
            <p>Settings saved.</p>
        </div>
        
    <div class="wrap">
        <h1>Stripe Webhook Settings</h1>
        <form method="post" action="">
            <table class="form-table">
                <tr>
                    <th><label for="my_stripe_webhook_secret">Webhook Signing Secret</label></th>
                    <td>
                        <input type="text" id="my_stripe_webhook_secret" name="my_stripe_webhook_secret" value="" class="regular-text" />
                        <p class="description">Enter your Stripe webhook signing secret from the Stripe Dashboard (Developers &gt; Webhooks).</p>
                    </td>
                </tr>
            </table>
            <?php wp_nonce_field( 'my_stripe_plugin_save_settings' ); ?>
            <?php submit_button(); ?>
        </form>
    </div>
    



With this settings page, users can securely input their webhook signing secret, which is then stored in the WordPress database via update_option() and retrieved using get_option().

Configuring the Stripe Webhook in the Stripe Dashboard

Once your endpoint is live, you need to tell Stripe where to send events.

  • Go to your Stripe Dashboard.
  • Navigate to Developers > Webhooks.
  • Click "Add endpoint".
  • Endpoint URL: Enter the URL of your WordPress site followed by the webhook trigger parameter. For example: https://yourdomain.com/?stripe_webhook=true
  • Events to send: Select the events you want to listen for (e.g., payment_intent.succeeded, payment_intent.payment_failed).
  • Click "Add endpoint".
  • After creating the endpoint, Stripe will display your **Signing secret**. Copy this secret and paste it into the settings page of your WordPress plugin.

Testing Your Webhook Endpoint

Stripe provides tools to test your webhook integration.

  • Stripe CLI: The Stripe CLI is the most effective way to test webhooks locally. Install it and run stripe listen --forward-to localhost:8000/your-wordpress-path/?stripe_webhook=true (adjust port and path as needed). The CLI will provide a forwarding URL and a signing secret.
  • Stripe Dashboard: You can also manually send test events from the Stripe Dashboard under Developers > Events.

When testing, monitor your WordPress debug log (if enabled) for messages from error_log() calls within your webhook handler. This is invaluable for diagnosing issues.

Security Best Practices and Considerations

  • Never hardcode secrets: Always use WordPress options or environment variables.
  • Use HTTPS: Ensure your WordPress site uses HTTPS to protect data in transit.
  • Rate Limiting: Implement rate limiting on your webhook endpoint if you anticipate high traffic or potential abuse.
  • Idempotency: Design your event handlers to be idempotent. This means processing the same event multiple times should have the same effect as processing it once. Stripe may occasionally send duplicate events.
  • Error Handling and Logging: Robust error handling and detailed logging are critical for debugging and monitoring.
  • Specific Endpoint URL: Avoid using a generic URL that might be accessible by other means. The query parameter approach helps isolate the webhook handler.
  • Keep Stripe Library Updated: Regularly update the stripe/stripe-php library to benefit from security patches and new features.
  • Limit Event Types: Only subscribe to the event types your application actually needs.

By following these steps, you can securely integrate Stripe webhook endpoints into your WordPress custom plugins, ensuring reliable and safe payment processing.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Step-by-Step Guide to building a custom role-based access control editor block for Gutenberg using Vanilla JS Web Components
  • Implementing automated compliance reporting for custom member profile directories ledgers using dompdf library
  • How to construct high-throughput import engines for large affiliate click tracking logs sets using custom XML/JSON parsers
  • Step-by-Step Guide to building a custom dynamic lead collector block for Gutenberg using REST API custom routes
  • Advanced Diagnostics: Locating slow Model-View-Controller (MVC) modular query bottlenecks in WooCommerce custom checkout pipelines

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (647)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (855)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (285)
  • WordPress Theme Development (357)

Recent Posts

  • Step-by-Step Guide to building a custom role-based access control editor block for Gutenberg using Vanilla JS Web Components
  • Implementing automated compliance reporting for custom member profile directories ledgers using dompdf library
  • How to construct high-throughput import engines for large affiliate click tracking logs sets using custom XML/JSON parsers

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (855)
  • Debugging & Troubleshooting (647)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala