• 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 Rewrite API custom endpoints

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

Leveraging WordPress Rewrite API for Secure Stripe Webhook Endpoints

Integrating Stripe webhooks into a WordPress environment, especially within custom plugins, demands a robust and secure approach. While WordPress offers various methods for handling incoming requests, directly exposing a PHP file for webhook processing can be brittle and less maintainable. This guide details a production-ready strategy using the WordPress Rewrite API to create custom endpoint URLs for your Stripe webhook handler, ensuring cleaner code, better organization, and enhanced security.

Defining the Custom Rewrite Rule

The core of this solution lies in registering a custom rewrite rule that maps a specific URL pattern to a query variable. This allows WordPress to recognize our custom endpoint without needing a dedicated PHP file in the root directory. We’ll hook into the rewrite_rules_array filter to add our rule.

Consider a scenario where your webhook endpoint will be accessible at yourdomain.com/stripe-webhook/. We’ll map this to a query variable, say stripe_webhook_handler, which we can then use to conditionally load our handler logic.

Registering the Rewrite Rule and Query Variable

Within your custom plugin’s main file or an included initialization file, add the following PHP code. This code registers both the query variable and the rewrite rule.

/**
 * Add custom query variable for Stripe webhook.
 *
 * @param array $vars Existing query variables.
 * @return array Modified query variables.
 */
function my_custom_plugin_add_stripe_webhook_query_var( $vars ) {
    $vars[] = 'stripe_webhook_handler';
    return $vars;
}
add_filter( 'query_vars', 'my_custom_plugin_add_stripe_webhook_query_var' );

/**
 * Add custom rewrite rule for Stripe webhook.
 */
function my_custom_plugin_add_stripe_webhook_rewrite_rule() {
    add_rewrite_rule(
        '^stripe-webhook/?$', // Regex for the URL pattern.
        'index.php?stripe_webhook_handler=1', // Rewrite to index.php with our query var.
        'top' // 'top' ensures this rule is checked before WordPress's default rules.
    );
}
add_action( 'init', 'my_custom_plugin_add_stripe_webhook_rewrite_rule', 10 );

/**
 * Flush rewrite rules on plugin activation/deactivation.
 * This is crucial for the rewrite rules to take effect.
 */
function my_custom_plugin_flush_rewrites_on_activation() {
    // Ensure the rewrite rule is added before flushing.
    my_custom_plugin_add_stripe_webhook_rewrite_rule();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_custom_plugin_flush_rewrites_on_activation' );

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

After adding this code, you must either deactivate and reactivate your plugin or manually flush WordPress’s rewrite rules. This can be done by navigating to Settings > Permalinks in the WordPress admin and clicking “Save Changes”. This action writes the new rules to your .htaccess file (for Apache) or Nginx configuration.

Implementing the Webhook Handler Logic

Now that our custom endpoint is registered, we need to hook into WordPress to detect when our stripe_webhook_handler query variable is set and then execute our webhook processing logic. We’ll use the template_redirect action for this.

Conditional Execution of Webhook Logic

The following code snippet should be added to your plugin’s main file or an included initialization file. It checks if the stripe_webhook_handler query variable is present and, if so, proceeds to process the Stripe webhook request.

/**
 * Handle Stripe webhook requests.
 */
function my_custom_plugin_handle_stripe_webhook() {
    // Check if our custom query variable is set.
    if ( intval( get_query_var( 'stripe_webhook_handler' ) ) === 1 ) {

        // Ensure the request method is POST.
        if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
            wp_die( 'Invalid request method.', 'Error', array( 'response' => 405 ) ); // Method Not Allowed
        }

        // Retrieve the raw POST data.
        $raw_payload = file_get_contents( 'php://input' );
        if ( ! $raw_payload ) {
            wp_die( 'Missing request payload.', 'Error', array( 'response' => 400 ) ); // Bad Request
        }

        // Verify the Stripe signature.
        // IMPORTANT: Replace 'YOUR_STRIPE_WEBHOOK_SECRET' with your actual webhook signing secret.
        // You can find this in your Stripe Dashboard under Developers -> Webhooks.
        $stripe_signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
        $webhook_secret = defined('MY_CUSTOM_PLUGIN_STRIPE_WEBHOOK_SECRET') ? MY_CUSTOM_PLUGIN_STRIPE_WEBHOOK_SECRET : get_option('my_custom_plugin_stripe_webhook_secret'); // Example of using a constant or option

        if ( empty( $webhook_secret ) ) {
            // Log an error or handle this critical configuration issue.
            error_log( 'Stripe webhook secret is not configured.' );
            wp_die( 'Server configuration error.', 'Error', array( 'response' => 500 ) ); // Internal Server Error
        }

        try {
            // Use the Stripe PHP library to verify the signature.
            // Ensure you have the Stripe PHP SDK installed: composer require stripe/stripe-php
            \Stripe\Stripe::setApiKey( defined('MY_CUSTOM_PLUGIN_STRIPE_SECRET_KEY') ? MY_CUSTOM_PLUGIN_STRIPE_SECRET_KEY : get_option('my_custom_plugin_stripe_secret_key') ); // Set your Stripe API key
            $event = \Stripe\Webhook::constructEvent(
                $raw_payload,
                $stripe_signature,
                $webhook_secret
            );
        } catch ( \UnexpectedValueException $e ) {
            // Invalid payload
            wp_die( 'Invalid payload.', 'Error', array( 'response' => 400 ) ); // Bad Request
        } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
            // Invalid signature
            wp_die( 'Invalid signature.', 'Error', array( 'response' => 400 ) ); // Bad Request
        } catch ( Exception $e ) {
            // Other errors
            error_log( 'Stripe webhook verification failed: ' . $e->getMessage() );
            wp_die( 'Webhook verification failed.', 'Error', array( 'response' => 500 ) ); // Internal Server Error
        }

        // At this point, the event is verified. Process the event.
        // The $event object contains the Stripe event details.
        // Example: Handle a 'payment_intent.succeeded' event.
        if ( $event->type === 'payment_intent.succeeded' ) {
            $paymentIntent = $event->data->object;
            // Your logic here: update order status, send emails, etc.
            // Example: Log the event for debugging.
            error_log( 'Stripe payment_intent.succeeded: ' . $paymentIntent->id );

            // Respond to Stripe with a 200 OK status to acknowledge receipt.
            // Do NOT send a response before processing is complete or you risk Stripe retrying.
            status_header( 200 );
            echo json_encode( array( 'received' => true ) );
            exit; // Terminate script execution.

        } elseif ( $event->type === 'charge.succeeded' ) {
            // Handle other event types as needed.
            $charge = $event->data->object;
            error_log( 'Stripe charge.succeeded: ' . $charge->id );
            status_header( 200 );
            echo json_encode( array( 'received' => true ) );
            exit;
        }
        // Add more event type handlers as necessary.

        // If the event type is not handled, acknowledge receipt but log it.
        error_log( 'Stripe webhook received unhandled event type: ' . $event->type );
        status_header( 200 );
        echo json_encode( array( 'received' => true, 'message' => 'Unhandled event type' ) );
        exit;
    }
}
add_action( 'template_redirect', 'my_custom_plugin_handle_stripe_webhook' );



Security Considerations and Best Practices

Security is paramount when handling payment webhooks. The following points are critical for a production-ready implementation:

  • Signature Verification: Always verify the Stripe signature. This ensures that the request genuinely originated from Stripe and has not been tampered with. The Stripe\Webhook::constructEvent method is essential for this.
  • Webhook Signing Secret: Store your Stripe webhook signing secret securely. Avoid hardcoding it directly in your plugin's main file. Use WordPress options (update_option, get_option) or define it as a constant in your wp-config.php file for better security.
  • HTTPS: Ensure your WordPress site is served over HTTPS. Stripe requires webhooks to be sent to HTTPS endpoints.
  • Idempotency: Design your webhook handler to be idempotent. This means that processing the same event multiple times should have the same effect as processing it once. Stripe may occasionally send duplicate events, and your handler should gracefully handle this.
  • Error Handling and Logging: Implement comprehensive error handling and logging. Log any verification failures, unexpected event types, or processing errors. This is crucial for debugging and monitoring.
  • HTTP Status Codes: Respond to Stripe with appropriate HTTP status codes. A 200 OK indicates successful receipt and processing. Other codes (e.g., 400 Bad Request, 405 Method Not Allowed, 500 Internal Server Error) signal issues.
  • Rate Limiting: While Stripe has its own retry mechanisms, consider implementing basic rate limiting on your webhook endpoint if you anticipate extremely high traffic or potential abuse, though this is less common for typical webhook loads.
  • Environment Variables: For sensitive API keys and secrets, consider using environment variables managed by your hosting environment rather than WordPress options or constants, especially in more complex deployment scenarios.

Storing Sensitive Credentials

Instead of hardcoding secrets, it's best practice to store them outside the codebase. Here are two common methods:

Method 1: Using WordPress Options API

You can use the WordPress Options API to store your Stripe API key and webhook signing secret. This allows you to manage them via the WordPress admin or programmatically.

// To save the secret (e.g., from a plugin settings page):
$webhook_secret = 'whsec_...'; // Retrieved from Stripe
$api_key = 'sk_test_...'; // Retrieved from Stripe

update_option( 'my_custom_plugin_stripe_webhook_secret', $webhook_secret );
update_option( 'my_custom_plugin_stripe_secret_key', $api_key );

// To retrieve them in the webhook handler (as shown in the example above):
$webhook_secret = get_option( 'my_custom_plugin_stripe_webhook_secret' );
$api_key = get_option( 'my_custom_plugin_stripe_secret_key' );

Method 2: Using wp-config.php Constants

For enhanced security, especially in production environments, define constants in your wp-config.php file.

// Add these lines to your wp-config.php file:
define( 'MY_CUSTOM_PLUGIN_STRIPE_WEBHOOK_SECRET', 'whsec_...' );
define( 'MY_CUSTOM_PLUGIN_STRIPE_SECRET_KEY', 'sk_test_...' );

// Then, in your plugin code, you can access them directly:
$webhook_secret = defined('MY_CUSTOM_PLUGIN_STRIPE_WEBHOOK_SECRET') ? MY_CUSTOM_PLUGIN_STRIPE_WEBHOOK_SECRET : null;
$api_key = defined('MY_CUSTOM_PLUGIN_STRIPE_SECRET_KEY') ? MY_CUSTOM_PLUGIN_STRIPE_SECRET_KEY : null;

Using constants in wp-config.php prevents these sensitive values from being stored in the database and makes them less accessible to potential database breaches. Ensure your wp-config.php file has strict file permissions.

Testing Your Webhook Endpoint

Thorough testing is crucial. Stripe provides tools to help you test your webhook integration:

  • Stripe CLI: The Stripe Command Line Interface is invaluable for local testing. You can forward events from Stripe to your local development environment. Install it and run: stripe listen --forward-to localhost:8000/stripe-webhook/ (adjust the port and path as needed).
  • Stripe Dashboard: In your Stripe Dashboard, navigate to Developers > Webhooks. You can resend historical events to your webhook endpoint to test how your handler processes them.
  • Logging: Utilize WordPress's error_log() function extensively during development to inspect payloads, event types, and verification results.

Conclusion

By employing the WordPress Rewrite API, you can create clean, organized, and secure webhook endpoints for services like Stripe. This approach abstracts the webhook logic away from direct file access, integrates seamlessly with WordPress's routing, and allows for robust security measures like signature verification and secure credential management. Remember to always prioritize security best practices and thorough testing to ensure reliable 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

  • How to implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • 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

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 (635)
  • 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 implement custom Filesystem API endpoints with token authentication in Gutenberg blocks
  • 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

Top Categories

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