• 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 WordPress Options API

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

Securing Stripe Webhook Endpoints in WordPress with the Options API

Integrating Stripe webhooks into a custom WordPress plugin requires a robust and secure approach. This guide details how to leverage the WordPress Options API to store and manage Stripe webhook secrets, ensuring that your webhook endpoint can reliably verify incoming Stripe events without exposing sensitive credentials directly in your code.

Storing Stripe Webhook Secrets Securely

Hardcoding Stripe webhook secrets directly into your plugin files is a significant security risk. A more secure and flexible method is to store these secrets in the WordPress database using the Options API. This allows you to update secrets without modifying plugin code and keeps them out of version control.

Using `add_option()` and `update_option()`

The WordPress Options API provides functions like `add_option()` and `update_option()` to manage site-wide settings. We’ll use these to store the Stripe webhook signing secret.

Example: Setting the Secret via a Plugin Activation Hook

When your plugin is activated, you can set an initial webhook secret. It’s crucial to provide a default or prompt the user to enter it via the WordPress admin area later. For this example, we’ll assume a placeholder and emphasize the need for a real secret.

<?php
/**
 * Plugin Name: My Secure Stripe Integration
 * Description: Securely integrates Stripe webhooks using the Options API.
 * Version: 1.0
 * Author: Your Name
 */

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

/**
 * Activate plugin.
 * Sets up initial options.
 */
function my_secure_stripe_activate() {
    // IMPORTANT: Replace 'whsec_...' with your actual Stripe webhook signing secret.
    // It's highly recommended to prompt the user to enter this via an admin settings page.
    $default_webhook_secret = 'whsec_YOUR_DEFAULT_SECRET_HERE';

    // Add the option if it doesn't exist.
    if ( false === get_option( 'my_secure_stripe_webhook_secret' ) ) {
        add_option( 'my_secure_stripe_webhook_secret', $default_webhook_secret, '', 'yes' );
    }
}
register_activation_hook( __FILE__, 'my_secure_stripe_activate' );

/**
 * Deactivate plugin.
 * Cleans up options.
 */
function my_secure_stripe_deactivate() {
    // Optionally delete the option on deactivation.
    // delete_option( 'my_secure_stripe_webhook_secret' );
}
register_deactivation_hook( __FILE__, 'my_secure_stripe_deactivate' );

// ... rest of your plugin code ...
?>

Retrieving the Secret

When your webhook handler needs to verify a Stripe event, you’ll retrieve the secret using `get_option()`.

<?php
/**
 * Retrieves the Stripe webhook signing secret from WordPress options.
 *
 * @return string|false The webhook secret, or false if not set.
 */
function get_my_secure_stripe_webhook_secret() {
    return get_option( 'my_secure_stripe_webhook_secret' );
}
?>

Implementing the Webhook Endpoint

Your custom plugin needs a publicly accessible endpoint to receive Stripe webhook POST requests. This endpoint will be configured in your Stripe dashboard.

Creating the Endpoint URL

Use WordPress’s rewrite rules and query variables to create a clean, permalink-friendly URL for your webhook handler. This avoids issues with query parameters and ensures a consistent endpoint.

<?php
/**
 * Add custom query variable for webhook endpoint.
 *
 * @param array $vars Existing query vars.
 * @return array Modified query vars.
 */
function my_secure_stripe_add_query_vars( $vars ) {
    $vars[] = 'my_stripe_webhook';
    return $vars;
}
add_filter( 'query_vars', 'my_secure_stripe_add_query_vars' );

/**
 * Add rewrite rule for webhook endpoint.
 */
function my_secure_stripe_add_rewrite_rules() {
    add_rewrite_rule(
        '^my-stripe-webhook/?$', // Regex for the URL path.
        'index.php?my_stripe_webhook=1', // Target to WordPress index.php with our query var.
        'top' // Priority.
    );
}
add_action( 'init', 'my_secure_stripe_add_rewrite_rules' );

/**
 * Flush rewrite rules on plugin activation.
 */
function my_secure_stripe_flush_rewrite_rules() {
    my_secure_stripe_add_rewrite_rules();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_secure_stripe_flush_rewrite_rules' );

/**
 * Flush rewrite rules on plugin deactivation.
 */
function my_secure_stripe_flush_rewrite_rules_deactivate() {
    flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'my_secure_stripe_flush_rewrite_rules_deactivate' );

/**
 * Handle the webhook endpoint.
 */
function my_secure_stripe_handle_webhook() {
    // Check if our custom query variable is set.
    if ( get_query_var( 'my_stripe_webhook' ) ) {
        // This is our webhook endpoint.
        // We'll process the Stripe event here.
        my_secure_stripe_process_stripe_event();
        exit; // Important to exit after processing.
    }
}
add_action( 'template_redirect', 'my_secure_stripe_handle_webhook' );
?>

Processing and Verifying Stripe Events

Inside the webhook handler, you must verify the incoming request’s signature using the secret stored in the WordPress options. This prevents malicious actors from sending fake events.

<?php
/**
 * Processes and verifies an incoming Stripe webhook event.
 */
function my_secure_stripe_process_stripe_event() {
    // Ensure this is a POST request.
    if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
        http_response_code( 405 ); // Method Not Allowed
        wp_die( 'Invalid request method.' );
    }

    // Retrieve the webhook secret from WordPress options.
    $webhook_secret = get_my_secure_stripe_webhook_secret();

    if ( ! $webhook_secret || 'whsec_YOUR_DEFAULT_SECRET_HERE' === $webhook_secret ) {
        // Log this error and alert administrators.
        error_log( 'Stripe webhook secret is not configured or is the default placeholder.' );
        http_response_code( 500 ); // Internal Server Error
        wp_die( 'Webhook secret not configured.' );
    }

    // Retrieve the raw POST data and the signature header.
    $payload = @file_get_contents( 'php://input' );
    $sig_header = null;

    if ( isset( $_SERVER['HTTP_STRIPE_SIGNATURE'] ) ) {
        $sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
    } elseif ( isset( $_SERVER['HTTP_X_STRIPE_SIGNATURE'] ) ) { // Some servers might use X-Forwarded-For style
        $sig_header = $_SERVER['HTTP_X_STRIPE_SIGNATURE'];
    }

    if ( ! $payload || ! $sig_header ) {
        http_response_code( 400 ); // Bad Request
        wp_die( 'Missing payload or signature header.' );
    }

    // Verify the webhook signature.
    try {
        // Use the Stripe PHP library for verification.
        // Ensure you have the Stripe PHP SDK installed via Composer.
        // e.g., composer require stripe/stripe-php
        \Stripe\Stripe::setApiKey( get_option( 'my_secure_stripe_live_secret_key' ) ); // Assuming you store your API key too.

        $event = \Stripe\Webhook::constructEvent(
            $payload, $sig_header, $webhook_secret
        );
    } catch ( \UnexpectedValueException $e ) {
        // Invalid payload
        http_response_code( 400 );
        wp_die( 'Invalid payload.' );
    } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
        // Invalid signature
        http_response_code( 400 );
        wp_die( 'Invalid signature.' );
    }

    // If verification is successful, process the event.
    if ( $event ) {
        // Handle the event based on its type.
        handle_stripe_event_type( $event );

        // Respond to Stripe with a 200 OK to acknowledge receipt.
        http_response_code( 200 );
        echo json_encode( array( 'status' => 'success' ) );
        exit;
    } else {
        // Should not happen if exceptions are caught, but as a fallback.
        http_response_code( 500 );
        wp_die( 'Failed to construct event.' );
    }
}

/**
 * Placeholder function to handle different Stripe event types.
 *
 * @param object $event The Stripe event object.
 */
function handle_stripe_event_type( $event ) {
    // Log the event for debugging.
    error_log( 'Received Stripe event: ' . $event->type );

    switch ( $event->type ) {
        case 'payment_intent.succeeded':
            $paymentIntent = $event->data->object; // contains a \Stripe\PaymentIntent
            // Process payment_intent.succeeded
            error_log( 'PaymentIntent was successful!' );
            // Example: Update order status in your custom database table or post meta.
            // update_order_status( $paymentIntent->metadata->order_id, 'paid' );
            break;
        case 'payment_method.attached':
            $paymentMethod = $event->data->object; // contains a \Stripe\PaymentMethod
            // Process payment_method.attached
            error_log( 'PaymentMethod was attached to a Customer!' );
            break;
        // ... handle other event types
        default:
            // Unexpected event type
            error_log( 'Received unknown Stripe event type: ' . $event->type );
    }
}
?>

Admin Settings for Webhook Secret Management

To make managing the webhook secret user-friendly, create a simple settings page in the WordPress admin area. This allows administrators to input and update the secret without touching code.

Creating an Admin Menu Page

<?php
/**
 * Add admin menu page for Stripe settings.
 */
function my_secure_stripe_add_admin_menu() {
    add_options_page(
        'Secure Stripe Settings',
        'Secure Stripe',
        'manage_options',
        'my-secure-stripe-settings',
        'my_secure_stripe_settings_page_html'
    );
}
add_action( 'admin_menu', 'my_secure_stripe_add_admin_menu' );
?>

Rendering the Settings Form

<?php
/**
 * Renders the HTML for the settings page.
 */
function my_secure_stripe_settings_page_html() {
    // Check user capabilities.
    if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    // Save settings if form submitted.
    if ( isset( $_POST['my_secure_stripe_webhook_secret'] ) && check_admin_referer( 'my_secure_stripe_save_settings', 'my_secure_stripe_nonce' ) ) {
        $new_secret = sanitize_text_field( $_POST['my_secure_stripe_webhook_secret'] );
        update_option( 'my_secure_stripe_webhook_secret', $new_secret );
        // You might also want to save your Stripe API keys here.
        // update_option( 'my_secure_stripe_live_secret_key', sanitize_text_field( $_POST['my_secure_stripe_live_secret_key'] ) );
    }

    // Get current values.
    $webhook_secret = get_option( 'my_secure_stripe_webhook_secret', '' );
    // $live_secret_key = get_option( 'my_secure_stripe_live_secret_key', '' );

    ?>
    <div class="wrap">
        <h1></h1>
        <form action="" method="post">
            <table class="form-table">
                <tr>
                    <th><label for="my_secure_stripe_webhook_secret">Stripe Webhook Signing Secret</label></th>
                    <td>
                        <input type="text" id="my_secure_stripe_webhook_secret" name="my_secure_stripe_webhook_secret" value="" class="regular-text">
                        <p class="description">Find this in your Stripe Dashboard under Developers &gt; Webhooks. It starts with 'whsec_'.</p>
                    </td>
                </tr>
                <!-- Add fields for Stripe API keys if needed -->
                <!--
                <tr>
                    <th><label for="my_secure_stripe_live_secret_key">Stripe Live Secret Key</label></th>
                    <td>
                        <input type="text" id="my_secure_stripe_live_secret_key" name="my_secure_stripe_live_secret_key" value="" class="regular-text">
                        <p class="description">Your Stripe secret key, starts with 'sk_live_'.</p>
                    </td>
                </tr>
                -->
            </table>
            <?php wp_nonce_field( 'my_secure_stripe_save_settings', 'my_secure_stripe_nonce' ); ?>
            <?php submit_button( 'Save Settings' ); ?>
        </form>
    </div>
    <?php
}
?>

Important Considerations and Best Practices

  • Stripe PHP SDK: Ensure the Stripe PHP SDK is installed in your WordPress environment, typically via Composer. Add `composer require stripe/stripe-php` to your plugin’s development workflow.
  • Error Logging: Implement robust error logging for webhook processing. Use `error_log()` or a more sophisticated logging solution to capture issues during signature verification or event handling.
  • Idempotency: Design your webhook handlers to be idempotent. This means that processing the same event multiple times should not cause unintended side effects. Stripe may occasionally resend events.
  • Security: Always use `check_admin_referer()` for form submissions and `esc_attr()`, `esc_html()`, `sanitize_text_field()` for input and output to prevent XSS and other vulnerabilities.
  • Environment Variables: For production environments, consider using environment variables or a more secure secrets management system instead of relying solely on WordPress options for highly sensitive keys. However, for typical plugin distribution, the Options API is a good balance of security and usability.
  • Testing: Use the Stripe CLI (`stripe listen –forward-to localhost:8000/wp-json/my-stripe-webhook/v1/`) or Stripe’s test mode to thoroughly test your webhook endpoint and event handling logic.
  • HTTPS: Your webhook endpoint URL must be served over HTTPS.

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

  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators
  • Step-by-Step Guide: Refactoring legacy hooks to use Active Record Wrapper pattern in theme layers
  • Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Next.js headless configurations
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in member profile directories
  • Step-by-Step Guide to building a custom interactive mapping module block for Gutenberg using Svelte standalone templates

Categories

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

Recent Posts

  • How to analyze and reduce CPU consumption of custom Command Query Responsibility Segregation (CQRS) event mediators
  • Step-by-Step Guide: Refactoring legacy hooks to use Active Record Wrapper pattern in theme layers
  • Step-by-Step Guide to building a custom custom analytics tracker block for Gutenberg using Next.js headless configurations

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (868)
  • Debugging & Troubleshooting (652)
  • Security & Compliance (634)
  • 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