• 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 WP HTTP API

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

Securing Stripe Webhook Endpoints in WordPress with WP HTTP API

Integrating Stripe webhooks into a custom WordPress plugin requires a robust and secure approach. This document outlines a production-ready strategy leveraging WordPress’s built-in WP HTTP API for reliable communication and essential security measures to validate incoming Stripe events.

1. Setting Up the Webhook Endpoint

Your custom plugin needs a dedicated endpoint to receive Stripe webhook notifications. This endpoint should be registered within your plugin’s main file or a dedicated webhook handler class. We’ll use WordPress’s rewrite rules to create a clean URL structure.

1.1. Plugin Activation Hook for Rewrite Rules

On plugin activation, register a custom rewrite rule and flush the rewrite rules to make the new endpoint accessible. This ensures the URL is recognized by WordPress.

/**
 * Plugin activation hook.
 */
function my_stripe_plugin_activate() {
    my_stripe_plugin_add_rewrite_rule();
    flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'my_stripe_plugin_activate' );

/**
 * Add custom rewrite rule for webhook endpoint.
 */
function my_stripe_plugin_add_rewrite_rule() {
    add_rewrite_rule(
        '^my-stripe-webhook/?$', // Regex for the URL: /my-stripe-webhook/
        'index.php?my_stripe_webhook=1', // Query var to trigger our handler
        'top' // Priority
    );
    add_rewrite_tag( '%my_stripe_webhook%', '1' ); // Register the query var
}

1.2. Handling the Webhook Request

Hook into the query_vars filter to add your custom query variable and then use the template_redirect action to intercept requests matching your rewrite rule. This is where the core webhook processing logic will reside.

/**
 * Add custom query variable.
 */
function my_stripe_plugin_add_query_vars( $vars ) {
    $vars[] = 'my_stripe_webhook';
    return $vars;
}
add_filter( 'query_vars', 'my_stripe_plugin_add_query_vars' );

/**
 * Handle the webhook request.
 */
function my_stripe_plugin_handle_webhook() {
    global $wp_query;

    if ( isset( $wp_query->query_vars['my_stripe_webhook'] ) && $wp_query->query_vars['my_stripe_webhook'] == 1 ) {
        // Ensure it's a POST request
        if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
            wp_die( 'Invalid request method.', 405 ); // Method Not Allowed
        }

        // Process the Stripe webhook
        my_stripe_plugin_process_stripe_webhook();

        // Prevent WordPress from loading a template
        exit;
    }
}
add_action( 'template_redirect', 'my_stripe_plugin_handle_webhook' );

2. Securely Verifying Stripe Webhook Signatures

Stripe signs its webhook requests with a signature to ensure their authenticity. It’s critical to verify this signature to prevent malicious actors from sending fake events. Stripe’s PHP library provides a convenient way to do this.

2.1. Retrieving the Stripe Signing Secret

The signing secret is unique to each webhook endpoint and is found in your Stripe dashboard. Store this securely, ideally in your WordPress site’s wp-config.php file or a secure options storage mechanism. Never hardcode secrets directly in your plugin files.

// In wp-config.php:
// define( 'MY_STRIPE_SIGNING_SECRET', 'whsec_...' );

// In your plugin file:
function my_stripe_plugin_get_signing_secret() {
    // Prioritize wp-config.php, then fallback to a secure option if necessary.
    if ( defined( 'MY_STRIPE_SIGNING_SECRET' ) ) {
        return MY_STRIPE_SIGNING_SECRET;
    }
    // Example of retrieving from WordPress options (ensure it's set securely)
    // return get_option( 'my_stripe_webhook_signing_secret' );
    return false; // Or throw an exception if not configured
}

2.2. Implementing Signature Verification

The Stripe PHP library’s Webhook::constructEvent method is the standard way to verify the signature. It requires the raw request body, the Stripe-Signature header, and your signing secret.

/**
 * Process the Stripe webhook request and verify signature.
 */
function my_stripe_plugin_process_stripe_webhook() {
    // Ensure Stripe PHP library is loaded.
    // You might include it via Composer or a manual include.
    if ( ! class_exists( 'Stripe\Stripe' ) ) {
        // Handle error: Stripe library not found.
        wp_send_json_error( 'Stripe library not loaded.', 500 );
        return;
    }

    $signing_secret = my_stripe_plugin_get_signing_secret();
    if ( ! $signing_secret ) {
        // Handle error: Signing secret not configured.
        wp_send_json_error( 'Webhook signing secret not configured.', 500 );
        return;
    }

    // Get the raw POST body
    $payload = @file_get_contents( 'php://input' );
    if ( ! $payload ) {
        wp_send_json_error( 'Failed to get request payload.', 400 );
        return;
    }

    // Get the signature header
    $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-
        $sig_header = $_SERVER['HTTP_X_STRIPE_SIGNATURE'];
    }

    if ( ! $sig_header ) {
        wp_send_json_error( 'Stripe-Signature header missing.', 400 );
        return;
    }

    try {
        // Verify the webhook signature
        $event = \Stripe\Webhook::constructEvent(
            $payload,
            $sig_header,
            $signing_secret
        );
    } catch ( \UnexpectedValueException $e ) {
        // Invalid payload
        error_log( 'Stripe webhook payload error: ' . $e->getMessage() );
        wp_send_json_error( 'Invalid payload.', 400 );
        return;
    } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
        // Invalid signature
        error_log( 'Stripe webhook signature verification error: ' . $e->getMessage() );
        wp_send_json_error( 'Invalid signature.', 400 );
        return;
    }

    // If verification is successful, $event will be a Stripe Event object.
    // Now, process the event based on its type.
    my_stripe_plugin_handle_event( $event );

    // Respond to Stripe with a 200 OK to acknowledge receipt.
    wp_send_json_success( 'Webhook received and processed.', 200 );
}

3. Processing Stripe Events

Once the signature is verified, you can safely process the event. The $event object contains details about what happened in Stripe. Use a switch statement on $event->type to handle different event notifications.

3.1. Event Handling Logic

This function will contain the core logic for reacting to various Stripe events, such as payment success, failed payments, customer updates, etc. It’s crucial to make this processing idempotent where possible.

/**
 * Handle the Stripe event based on its type.
 *
 * @param \Stripe\Event $event The Stripe event object.
 */
function my_stripe_plugin_handle_event( $event ) {
    // Log the event for debugging purposes.
    error_log( 'Received Stripe event: ' . $event->type );

    // Ensure Stripe API keys are set for further API calls if needed.
    // This should be done once, perhaps on plugin load.
    // \Stripe\Stripe::setApiKey( MY_STRIPE_SECRET_KEY );

    switch ( $event->type ) {
        case 'payment_intent.succeeded':
            // Handle successful payment intent.
            $paymentIntent = $event->data->object;
            my_stripe_plugin_handle_payment_intent_succeeded( $paymentIntent );
            break;

        case 'payment_intent.payment_failed':
            // Handle failed payment intent.
            $paymentIntent = $event->data->object;
            my_stripe_plugin_handle_payment_intent_failed( $paymentIntent );
            break;

        case 'charge.succeeded':
            // Handle successful charge (older API, but still relevant for some use cases).
            $charge = $event->data->object;
            my_stripe_plugin_handle_charge_succeeded( $charge );
            break;

        case 'customer.subscription.created':
            // Handle new subscription.
            $subscription = $event->data->object;
            my_stripe_plugin_handle_subscription_created( $subscription );
            break;

        // ... handle other event types as needed
        // See: https://stripe.com/docs/webhooks#event-types

        default:
            // Unexpected event type.
            error_log( 'Unhandled Stripe event type: ' . $event->type );
            break;
    }
}

/**
 * Example handler for payment_intent.succeeded.
 *
 * @param object $paymentIntent The Stripe PaymentIntent object.
 */
function my_stripe_plugin_handle_payment_intent_succeeded( $paymentIntent ) {
    // Example: Update order status in WordPress, send email, etc.
    $order_id = $paymentIntent->metadata->order_id ?? null; // Assuming you pass order_id in metadata
    if ( $order_id ) {
        // Update order status in your custom order system or WooCommerce.
        // Example: update_post_meta( $order_id, '_payment_status', 'paid' );
        error_log( "Payment for Order ID {$order_id} succeeded." );
    } else {
        error_log( "PaymentIntent succeeded, but no order_id found in metadata." );
    }
}

/**
 * Example handler for payment_intent.payment_failed.
 *
 * @param object $paymentIntent The Stripe PaymentIntent object.
 */
function my_stripe_plugin_handle_payment_intent_failed( $paymentIntent ) {
    $order_id = $paymentIntent->metadata->order_id ?? null;
    if ( $order_id ) {
        // Update order status to failed, notify customer.
        // Example: update_post_meta( $order_id, '_payment_status', 'failed' );
        error_log( "Payment for Order ID {$order_id} failed." );
    } else {
        error_log( "PaymentIntent failed, but no order_id found in metadata." );
    }
}

// Add similar handlers for other event types...

4. Using WP HTTP API for Outbound Requests (Optional but Recommended)

While not strictly necessary for receiving webhooks, you might need to make API calls to Stripe or other services from within your webhook handler (e.g., to retrieve more details about an object if not fully provided in the webhook payload, or to update an order status in a separate system). The WP HTTP API is the standard WordPress way to handle these outbound HTTP requests.

4.1. Making a GET Request with WP HTTP API

This example shows how to fetch data from an external API using wp_remote_get. Ensure you handle potential errors and timeouts.

/**
 * Example of fetching data from an external API using WP HTTP API.
 *
 * @param string $url The URL to fetch data from.
 * @return array|WP_Error The decoded JSON response or a WP_Error object.
 */
function my_stripe_plugin_fetch_external_data( $url ) {
    $response = wp_remote_get( $url, array(
        'timeout'     => 15, // Timeout in seconds
        'headers'     => array(
            'Accept' => 'application/json',
            // Add any necessary authentication headers here
            // 'Authorization' => 'Bearer YOUR_API_KEY'
        ),
    ) );

    if ( is_wp_error( $response ) ) {
        error_log( 'WP HTTP GET Error: ' . $response->get_error_message() );
        return $response;
    }

    $body = wp_remote_retrieve_body( $response );
    $status_code = wp_remote_retrieve_response_code( $response );

    if ( $status_code !== 200 ) {
        error_log( "WP HTTP GET Error: Received status code {$status_code} from {$url}" );
        return new WP_Error( 'http_request_failed', 'Unexpected HTTP status code: ' . $status_code );
    }

    $data = json_decode( $body, true );
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        error_log( 'WP HTTP GET Error: Failed to decode JSON response.' );
        return new WP_Error( 'json_decode_error', 'Failed to decode JSON response.' );
    }

    return $data;
}

4.2. Making a POST Request with WP HTTP API

For sending data, use wp_remote_post. This is useful for updating external systems with information derived from Stripe webhooks.

/**
 * Example of sending data to an external API using WP HTTP API.
 *
 * @param string $url The URL to send data to.
 * @param array  $data The data to send.
 * @return array|WP_Error The decoded JSON response or a WP_Error object.
 */
function my_stripe_plugin_send_external_data( $url, $data ) {
    $response = wp_remote_post( $url, array(
        'timeout'     => 15,
        'headers'     => array(
            'Content-Type' => 'application/json',
            // Add any necessary authentication headers here
        ),
        'body'        => json_encode( $data ),
    ) );

    if ( is_wp_error( $response ) ) {
        error_log( 'WP HTTP POST Error: ' . $response->get_error_message() );
        return $response;
    }

    $body = wp_remote_retrieve_body( $response );
    $status_code = wp_remote_retrieve_response_code( $response );

    if ( $status_code < 200 || $status_code >= 300 ) { // Check for success range
        error_log( "WP HTTP POST Error: Received status code {$status_code} from {$url}" );
        return new WP_Error( 'http_request_failed', 'Unexpected HTTP status code: ' . $status_code );
    }

    $decoded_body = json_decode( $body, true );
    if ( json_last_error() !== JSON_ERROR_NONE ) {
        error_log( 'WP HTTP POST Error: Failed to decode JSON response.' );
        return new WP_Error( 'json_decode_error', 'Failed to decode JSON response.' );
    }

    return $decoded_body;
}

5. Best Practices and Considerations

  • Idempotency: Design your webhook handlers to be idempotent. If Stripe retries sending a webhook (e.g., due to a network error on your end), processing it multiple times should not cause duplicate actions. Use unique identifiers from Stripe (like payment_intent.id or charge.id) to track processed events.
  • Error Handling and Logging: Implement comprehensive error logging. Use error_log() to record issues during signature verification, event processing, or outbound requests. This is crucial for debugging and monitoring.
  • Security: Never expose your Stripe secret keys or signing secrets publicly. Use wp-config.php or secure WordPress options. Ensure your server is configured to use TLS/SSL.
  • Stripe Library Management: Use Composer to manage the Stripe PHP library. This ensures you have the latest security patches and features. Integrate Composer’s autoloading into your plugin.
  • Testing: Utilize Stripe’s CLI or the Stripe Dashboard’s webhook testing feature to send test events to your endpoint. This is invaluable for verifying your implementation.
  • Asynchronous Processing: For very long-running tasks within a webhook handler, consider offloading the work to a background job queue (e.g., using WP-Cron with a delay, or a dedicated queue system) to avoid webhook timeouts and ensure a quick response to Stripe.
  • Environment Configuration: Differentiate between Stripe test and live keys/secrets. Ensure your webhook endpoints are configured correctly in Stripe for each environment.

By following these guidelines, you can build a secure, reliable, and maintainable Stripe webhook integration within your custom WordPress plugin, leveraging the power of the WP HTTP API for all your communication needs.

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

  • Optimizing p99 database query response latency in multi-site Singleton Registry Pattern custom tables
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using React components
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in customer support tickets
  • Optimizing p99 database query response latency in multi-site Domain-driven architecture (DDD) blocks custom tables
  • How to design a modular Action-hook Event Mediator architecture for enterprise-level custom plugins

Categories

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

Recent Posts

  • Optimizing p99 database query response latency in multi-site Singleton Registry Pattern custom tables
  • Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using React components
  • Troubleshooting guide: Resolving memory leak spikes caused by unclosed custom database loops in customer support tickets

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • 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