• 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 REST API Controllers

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

Securing Stripe Webhook Endpoints in WordPress with REST API Controllers

Integrating Stripe webhooks into a custom WordPress plugin requires a robust and secure approach. This guide details how to leverage WordPress’s REST API controllers to create a dedicated, authenticated endpoint for receiving and processing Stripe events, ensuring data integrity and preventing unauthorized access.

Prerequisites

  • A working WordPress installation.
  • A custom plugin structure.
  • Basic understanding of WordPress plugin development and the REST API.
  • A Stripe account and API keys.

Setting Up the REST API Endpoint

We’ll use WordPress’s built-in REST API to create a custom endpoint. This provides a structured way to handle incoming requests and leverages WordPress’s authentication and authorization mechanisms. The key is to register a new route that will listen for POST requests from Stripe.

Registering the Route

Within your custom plugin’s main file or an included file, add the following PHP code to register your webhook endpoint. We’ll use the `rest_api_init` action hook.

add_action( 'rest_api_init', function () {
    register_rest_route( 'my-stripe-plugin/v1', '/webhook', array(
        'methods'  => 'POST',
        'callback' => 'my_stripe_plugin_handle_webhook',
        'permission_callback' => '__return_true', // We'll handle security manually
    ) );
} );

function my_stripe_plugin_handle_webhook( WP_REST_Request $request ) {
    // Webhook processing logic will go here
    return new WP_REST_Response( array( 'message' => 'Webhook received' ), 200 );
}

In this setup:

  • my-stripe-plugin/v1 is the namespace and version for our API.
  • /webhook is the specific endpoint path.
  • POST specifies the HTTP method.
  • my_stripe_plugin_handle_webhook is the callback function that will process the incoming Stripe event.
  • 'permission_callback' => '__return_true' is used because we will implement our own security checks within the callback function, rather than relying on WordPress’s default user-based permissions.

Verifying the Stripe Signature

Stripe sends a signature with each webhook request, which is crucial for verifying that the request genuinely originated from Stripe and hasn’t been tampered with. This signature is sent in the Stripe-Signature HTTP header.

Implementing Signature Verification

You’ll need your Stripe webhook signing secret. You can find this in your Stripe dashboard under Developers > Webhooks > Select your endpoint > Signing secret. It’s recommended to store this secret securely, for example, in your wp-config.php file.

// Add this to your wp-config.php
define( 'STRIPE_WEBHOOK_SECRET', 'whsec_YOUR_SIGNING_SECRET' );

// Modify the callback function to include verification
function my_stripe_plugin_handle_webhook( WP_REST_Request $request ) {
    $signature_header = $request->get_header( 'Stripe-Signature' );
    $payload = $request->get_body(); // Get the raw request body

    if ( ! $signature_header || ! $payload ) {
        return new WP_REST_Response( array( 'error' => 'Missing signature or payload' ), 400 );
    }

    // Ensure the signing secret is defined
    if ( ! defined( 'STRIPE_WEBHOOK_SECRET' ) || empty( STRIPE_WEBHOOK_SECRET ) ) {
        error_log( 'Stripe webhook secret is not configured.' );
        return new WP_REST_Response( array( 'error' => 'Server configuration error' ), 500 );
    }

    try {
        // Use Stripe's PHP library for verification
        // Make sure you have the Stripe PHP SDK installed via Composer
        // e.g., composer require stripe/stripe-php
        \Stripe\Stripe::setApiKey( get_option( 'stripe_secret_key' ) ); // Assuming you store your Stripe secret key in WP options

        $event = \Stripe\Webhook::constructEvent(
            $payload, $signature_header, STRIPE_WEBHOOK_SECRET
        );

        // If verification is successful, $event will be populated
        // Process the event
        my_stripe_plugin_process_stripe_event( $event );

        return new WP_REST_Response( array( 'message' => 'Webhook received and processed' ), 200 );

    } catch ( \UnexpectedValueException $e ) {
        // Invalid payload
        error_log( "Stripe webhook error: Invalid payload - " . $e->getMessage() );
        return new WP_REST_Response( array( 'error' => 'Invalid payload' ), 400 );
    } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
        // Invalid signature
        error_log( "Stripe webhook error: Invalid signature - " . $e->getMessage() );
        return new WP_REST_Response( array( 'error' => 'Invalid signature' ), 400 );
    } catch ( Exception $e ) {
        // Other errors
        error_log( "Stripe webhook error: " . $e->getMessage() );
        return new WP_REST_Response( array( 'error' => 'An internal error occurred' ), 500 );
    }
}

Important Notes:

  • Stripe PHP SDK: This code assumes you have the Stripe PHP SDK installed in your WordPress environment, typically via Composer. If you’re not using Composer for your plugin, you’ll need to include the SDK manually or find an alternative method for signature verification.
  • API Key: The example uses get_option( 'stripe_secret_key' ). You should store your Stripe secret key securely (e.g., in wp-config.php or WordPress options) and retrieve it here. For webhook verification, only the signing secret is strictly necessary, but you’ll likely need your API key for other Stripe operations.
  • Error Logging: Robust error logging is critical for debugging webhook issues. Use error_log() to record any problems encountered during signature verification or event processing.

Processing Stripe Events

Once the signature is verified, you can safely process the Stripe event. The $event object contains information about what happened in Stripe. You’ll typically want to switch on the type property of the event to handle different Stripe events (e.g., payment_intent.succeeded, charge.refunded, customer.subscription.created).

Example Event Processing Function

function my_stripe_plugin_process_stripe_event( $event ) {
    // Log the event type for debugging
    error_log( "Stripe webhook received event type: " . $event->type );

    switch ( $event->type ) {
        case 'payment_intent.succeeded':
            $paymentIntent = $event->data->object; // Contains the PaymentIntent
            // Handle successful payment intent
            // e.g., update order status, send confirmation email
            my_stripe_plugin_handle_payment_intent_succeeded( $paymentIntent );
            break;

        case 'charge.refunded':
            $charge = $event->data->object; // Contains the Charge object
            // Handle refunded charge
            // e.g., update order status, notify customer
            my_stripe_plugin_handle_charge_refunded( $charge );
            break;

        case 'customer.subscription.created':
            $subscription = $event->data->object; // Contains the Subscription object
            // Handle new subscription creation
            // e.g., grant access to premium content
            my_stripe_plugin_handle_subscription_created( $subscription );
            break;

        // ... handle other event types as needed
        default:
            // Unexpected event type
            error_log( "Stripe webhook: Unhandled event type: " . $event->type );
            break;
    }
}

// Placeholder functions for event handlers
function my_stripe_plugin_handle_payment_intent_succeeded( $paymentIntent ) {
    // Example: Find order by Stripe PaymentIntent ID and update status
    $order_id = get_order_id_from_payment_intent( $paymentIntent->id ); // You'll need to implement this function
    if ( $order_id ) {
        // Update order status in your custom order system or WooCommerce
        update_order_status( $order_id, 'completed' ); // Implement this
        error_log( "Processed payment_intent.succeeded for order ID: " . $order_id );
    } else {
        error_log( "Could not find order for PaymentIntent ID: " . $paymentIntent->id );
    }
}

function my_stripe_plugin_handle_charge_refunded( $charge ) {
    // Example: Find order by Stripe Charge ID and update status
    $order_id = get_order_id_from_charge( $charge->id ); // Implement this
    if ( $order_id ) {
        update_order_status( $order_id, 'refunded' ); // Implement this
        error_log( "Processed charge.refunded for order ID: " . $order_id );
    } else {
        error_log( "Could not find order for Charge ID: " . $charge->id );
    }
}

function my_stripe_plugin_handle_subscription_created( $subscription ) {
    // Example: Link subscription to a user and grant access
    $user_id = get_user_id_from_stripe_customer( $subscription->customer ); // Implement this
    if ( $user_id ) {
        grant_premium_access( $user_id, $subscription->id ); // Implement this
        error_log( "Processed subscription.created for user ID: " . $user_id . " Subscription ID: " . $subscription->id );
    } else {
        error_log( "Could not find user for Stripe Customer ID: " . $subscription->customer );
    }
}

// Dummy implementations for helper functions (replace with your actual logic)
function get_order_id_from_payment_intent( $payment_intent_id ) { return false; }
function get_order_id_from_charge( $charge_id ) { return false; }
function get_user_id_from_stripe_customer( $customer_id ) { return false; }
function update_order_status( $order_id, $status ) { /* ... */ }
function grant_premium_access( $user_id, $subscription_id ) { /* ... */ }

The placeholder functions like get_order_id_from_payment_intent and update_order_status are crucial. You will need to implement these based on how you store and manage your orders or user data within WordPress. This might involve custom database tables, post meta, or integration with e-commerce plugins like WooCommerce.

Configuring Stripe Webhooks

After implementing your webhook endpoint in WordPress, you need to configure Stripe to send events to it. This is done in the Stripe dashboard.

Steps in Stripe Dashboard:

  • Navigate to Developers > Webhooks.
  • Click “Add endpoint”.
  • Endpoint URL: Enter the full URL to your WordPress webhook endpoint. For example: https://your-wordpress-site.com/wp-json/my-stripe-plugin/v1/webhook
  • Events to send: Select the specific events you want to listen for (e.g., payment_intent.succeeded, charge.refunded). It’s good practice to only subscribe to the events your application actually needs.
  • Click “Add endpoint”.

Stripe will then display your signing secret. Ensure this matches the secret you’ve configured in your wp-config.php file.

Testing Your Webhook Endpoint

Thorough testing is essential. Stripe provides tools to help with this.

Using the Stripe CLI

The Stripe Command Line Interface (CLI) is invaluable for testing webhooks locally. It allows you to forward events from Stripe to your local development environment.

First, install the Stripe CLI if you haven’t already. Then, configure it with your Stripe API key:

stripe login
# Follow the prompts to link your Stripe account

Next, forward events to your local server. If your local WordPress site is running on http://localhost:8000 and your webhook endpoint is /wp-json/my-stripe-plugin/v1/webhook, you would run:

stripe listen --forward-to localhost:8000/wp-json/my-stripe-plugin/v1/webhook

The CLI will output a webhook signing secret. Use this secret for your local development environment (e.g., in a local wp-config.php or environment variable) for signature verification. Now, when events occur in your Stripe test mode, they will be forwarded to your local endpoint.

Testing with Stripe Dashboard

You can also manually trigger events from the Stripe dashboard:

  • Go to Developers > Webhooks.
  • Select your endpoint.
  • Click “Send test event”.
  • Choose an event type and click “Send test event”.

Check your WordPress site’s error logs (and any logging you’ve implemented in your webhook handler) to confirm the event was received and processed correctly.

Security Best Practices and Considerations

  • HTTPS: Always use HTTPS for your WordPress site. This encrypts data in transit between Stripe and your server.
  • Signing Secret Security: Never commit your webhook signing secret to version control. Store it securely, ideally in wp-config.php or a secure environment variable.
  • Idempotency: Design your webhook handlers 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.
  • Rate Limiting: While Stripe handles its own rate limiting, be mindful of your server’s capacity. If you expect a very high volume of webhooks, consider asynchronous processing (e.g., queuing events for background processing) to avoid overwhelming your WordPress application.
  • Error Handling: Implement comprehensive error handling and logging. A failed webhook can lead to inconsistencies in your application’s state. Stripe will retry failed webhooks for a period, so ensure your endpoint is reliable.
  • Event Filtering: Only subscribe to the events you absolutely need in the Stripe dashboard. This reduces unnecessary traffic and potential attack surface.
  • Sanitize and Validate: Always sanitize and validate any data received from Stripe before using it in your application, even after signature verification.

Conclusion

By using WordPress’s REST API controllers and diligently verifying Stripe signatures, you can build a secure and reliable webhook integration for your custom plugins. This approach leverages WordPress’s core functionalities while ensuring the integrity and security of your payment processing workflows.

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