Getting timely updates about payments is crucial for maintaining smooth operational flows. Especially for businesses that rely heavily on e-commerce or offer subscription-based services. Stripe provides a robust solution for this through the use of webhooks, allowing for real-time notifications on various payment events. This blog post aims to shed light on how to automate and instantly reflect payment status changes. By understanding and implementing webhooks, developers and business owners can ensure their systems are more responsive in handling financial transactions online.

This guide covers everything from the basics of webhooks, their configuration, to practical examples and best practices.

This article is part of a comprehensive series. Be sure to check out the previous post, where you learn how to implement your own payment system with Stripe.

Disclaimer: I am not affiliated with Stripe. All insights shared in this article are based on my personal experience and opinions.

Table of Contents

Understanding Webhooks

A webhook is an HTTP endpoint that receives events from Stripe.

Stripe docs

Webhooks are essential for real-time updates and are used widely in modern web applications for a variety of tasks. They are highly efficient as they eliminate the need for constant polling for changes. Instead, they bring the information to your doorstep the moment it happens, which is why they are preferable for payment operations where timely updates are crucial for both the business and the customer.

A webhook is an endpoint you register in Stripe in order to receive notifications about payments and other events. Image source: Stripe

Webhooks function through a system of subscribers and publishers. When you set up a webhook, you are subscribing to events you’re interested in. Here’s how it works:

  1. A new event occurs: Let’s say a customer makes a payment on your online store via Stripe.
  2. Stripe collects event details: Stripe recognizes this event and creates a webhook payload. In our example, the payload informs about the payment that just happened.
  3. Stripe sends a request to the registered endpoint (webhook): Stripe sends this payload by making a request to the webhook URL you’ve set up. This URL leads to a part of your server configured to listen for webhook requests.
  4. Your system receives the request: Your server receives the payload at the endpoint. The URL endpoint works like a phone receiving a text message. It gets the message and can respond accordingly. It is essential to confirm a receipt of the request by generating a successful response at the endpoint. A delayed or unexpected response makes Stripe try to deliver the message at a later point in time, which is wasteful and defies the purpose of timely reaction in the first place.
  5. Act on the event: Your server then processes the information sent by Stripe and takes necessary actions, like updating the payment status in your database. This usually happens asynchronously, after having confirmed a successful receipt.

Types of Events You Can Track with Stripe Webhooks

Stripe’s webhooks are incredibly versatile, allowing you to track a wide range of events related to payments and account management. These include, but are not limited to:

  • Successful payments: Notifications when payments are successfully processed, which is crucial for both fulfilling orders and acknowledging customer payments immediately.
  • Failed payments: Alerts for failed payment attempts can help you prompt customers to try a different payment method without delay, improving retention and reducing churn.
  • Disputed payments: Receiving information on charge disputes or chargebacks lets you manage customer concerns and address any potential fraud quickly.
  • Subscription changes: Notifications regarding subscription lifecycle events, such as creation, update, cancelation, or expiration, enable better management of recurring billing.
  • Balance updates: Being informed of balance availability changes in your Stripe account can aid in better financial planning and funds allocation.

Webhooks practically serve as a bridge between your Stripe account’s activities and your operational systems. They help your business logic align seamlessly with transactions and account changes in real time.

Setting Up Your Stripe Webhook

First, you’ll need to set up an endpoint on your server that Stripe can call when events occur. In Java, you might use a framework like Spring to set up a RESTful API endpoint.

Here’s a simple example of what the Java code to create a webhook endpoint could look like:

import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/stripe/webhook")
public class StripeWebhookController {
    
    @Value("${stripe.webhook-secret}")
    private String webhookSecret;
    
    @PostMapping
    public String handleWebhookEvent(String payload, String sigHeader) {
        // Verify the signature and construct the event
        Event event = null;
        
        try {
            event = Webhook.constructEvent(
              payload, sigHeader, webhookSecret
            );    
        } catch (SignatureVerificationException e) {
            return "Webhook signature verification failed";
        }
        
        // Handle the event
        // This should be done asynchronously!
        switch (event.getType()) {
            case "payment_intent.succeeded":
                // Handle successful payment
                break;
            case "payment_intent.payment_failed":
                // Handle failed payment
                break;
            // Add more event types as needed
            default:
                // Unexpected event type
                return "Unexpected event type";
        }
        
        return "Success";
    }
}

In application.properties, remember to replace your_webhook_secret with your actual endpoint secret from Stripe (see the next section):

spring.application.name=stripe-payments-tutorial
stripe.secret-key=your_secret_key
stripe.webhook-secret=your_webhook_secret

Next, you’ll need to configure the webhook in the Stripe Dashboard.

Configuring the Webhook in the Stripe Dashboard

Stripe will provide you with a signing secret. Keep this secret safe, as you’ll need it to verify the webhook signatures. Here’s how to configure the webhook in Stripe:

  1. Log in to your Stripe Dasboard.
  2. Go to the Developers section in the sidebar menu.
  3. Click on Webhooks and then Add an Endpoint.
  4. Enter the URL of your webhook endpoint that you set up. For example, https://yourdomain.com/stripe/webhook.
  5. Select the events you want to listen to (e.g. payment_intent.succeeded, payment_intent.failed etc.).
  6. Finally, tap on Add endpoint to save the new webhook.

Signature Verification for Security

Every time Stripe sends a webhook, it includes a signature in the Stripe-Signature header. This signature is created using your endpoint’s secret, which allows you to verify that the requests are genuinely from Stripe. Here’s how you perform signature verification in Java:

Event event = null;
        
try {
  event = Webhook.constructEvent(
    payload, sigHeader, webhookSecret
  );    
} catch (SignatureVerificationException e) {
  return "Webhook signature verification failed";
}

This code attempts to construct an event with the provided payload and signature header. If the signature doesn’t match, a SignatureVerificationException will be thrown, indicating that the request is not to be trusted.

Be sure to never expose your webhook secret. If someone acquires this secret, they could forge events to your endpoint. So handle it securely, much like you would handle any sensitive credentials.

Handling Different Types of Webhook Events

Stripe sends various types of events encapsulated in webhook requests, reflecting changes in payment status, customer data, etc. Your webhook endpoint needs to distinguish between these events effectively. Below is an expansion of the switch case to handle different event types:

// Inside your @PostMapping annotated method
try {
    event = Webhook.constructEvent(payload, sigHeader, ENDPOINT_SECRET);
} catch (Exception e) {
    return "Signature verification failed";
}

// Handle the event
switch (event.getType()) {
    case "payment_intent.succeeded":
        handlePaymentIntentSucceeded(event);
        break;
    case "payment_intent.payment_failed":
        handlePaymentIntentFailed(event);
        break;
    case "charge.refunded":
        handleChargeRefunded(event);
        break;
    default:
        return "Unhandled event type";
}

return "Success";

// Example handler methods
private void handlePaymentIntentSucceeded(Event event) {
    // Extract payment intent object and update order status in your system
}

private void handlePaymentIntentFailed(Event event) {
    // Notify the user or retry payment process
}

private void handleChargeRefunded(Event event) {
    // Handle refund in your application
}

This implementation, however, suffers from a substantial flaw. The processing will block the request which can lead to timeouts and duplicated events due to retries. Let’s fix it.

Asynchronous Event Processing with Spring

To ensure that your webhook handler processes events asynchronously, preventing timeouts and potential duplicated delivery attempts from Stripe, you need to delegate the processing of the event to a separate thread. In Spring, you can easily achieve this by using @Async annotation and configuring task execution.

Step 1: Enable Async Processing

First, in your Spring application’s main configuration or startup class, enable asynchronous processing with the @EnableAsync annotation:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}

Step 2: Create an Async Event Processing Service

Next, create a service that will process the events asynchronously. You can use the @Async annotation on methods in this service to tell Spring to execute them in a separate thread:

import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.Event;
import com.stripe.model.PaymentIntent;
import com.stripe.param.PaymentIntentConfirmParams;
import com.stripe.param.PaymentIntentCreateParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class StripeService {

    private final Logger logger = LoggerFactory.getLogger(StripeService.class);

    public StripeService(
            @Value("stripe.secret-key")
            String secretKey
    ) {
        Stripe.apiKey = secretKey;
    }

    @Async
    public void processEventAsync(Event event) {
        switch (event.getType()) {
            case "payment_intent.succeeded":
                handlePaymentIntentSucceeded(event);
                break;
            case "payment_intent.failed":
                handlePaymentIntentFailed(event);
                break;
            // Add other cases as needed
            default:
                logger.warn("Unhandled event type: {}", event.getType());
        }
    }
    private void handlePaymentIntentSucceeded(Event event) {
        // Process payment intent succeeded event
        logger.info("Handled payment_intent.succeeded asynchronously.");
    }

    private void handlePaymentIntentFailed(Event event) {
        // Process payment intent failed event
        logger.info("Handled payment_intent.failed asynchronously.");
    }
}

Step 3: Modify Your Controller to Use the Async Service

Finally, inject this service into your controller and delegate event processing to it. Your modified StripeWebhookController should look something like this:

import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.net.Webhook;
import io.connektn.stripe.tutorial.StripeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/stripe/webhook")
public class StripeWebhookController {

    @Value("${stripe.webhook-secret}")
    private String webhookSecret;

    private final StripeService stripeService;

    public StripeWebhookController(StripeService stripeService) {
        this.stripeService = stripeService;
    }

    @PostMapping
    public String handleWebhookEvent(String payload, String sigHeader) {
        // Verify the signature and construct the event
        Event event = null;

        try {
            event = Webhook.constructEvent(
              payload, sigHeader, webhookSecret
            );
        } catch (SignatureVerificationException e) {
            return "Webhook signature verification failed";
        }

        // Handle the event asynchronously
        stripeService.processEventAsync(event);

        return "Success";
    }
}

By following these steps, your application now offloads the processing of Stripe webhook events to a separate thread, allowing the main thread to quickly respond to the incoming requests. This approach greatly reduces the risk of timeouts, ensuring that Stripe does not unnecessarily retry sending the same event.

Example: Updating Payment Status

Event validation has been addressed previously with signature verification to ensure that the event is genuinely from Stripe. Once verified, you can confidently process the event. For instance, when handling a payment_intent.succeeded event, you might retrieve the payment intent ID and update the corresponding order or invoice status in your database to Paid:

import com.stripe.model.PaymentIntent;

private void handlePaymentIntentSucceeded(Event event) {
  // Process payment intent succeeded event
  event.getDataObjectDeserializer().getObject()
    .ifPresent(paymentIntent -> {
       String paymentIntentId = ((PaymentIntent) paymentIntent).getId();
       // Update your order status in the database using paymentIntentId
       // Example: updateOrderStatus(paymentIntentId, "Paid");
       logger.info("Handled payment_intent.succeeded asynchronously.");
    });
}

Error Handling and Troubleshooting

Error handling is critical for maintaining the integrity of your transactional logic. When errors occur (e.g., database update failures, network issues), it’s important to log these errors for later analysis and, if possible, retry the transaction.

The following practices will save you time and effort when troubleshooting common failures:

  • Logging. Ensure that all exceptions are logged. Consider including the event ID and type in your logs to facilitate troubleshooting.
  • Retry logic. Stripe does not automatically retry webhook deliveries if your server responds with an error (other than a 5xx status code). Implement retry logic within your application if certain operations fail.
  • Strictly asynchronous processing. Please do not intentionally let your endpoint respond with a 5xx status code! Remember, your business logic should run asynchronously (see the previous section). Your objective should be to respond with a success 2xx code ASAP in order to avoid timeouts and unnecessary retries of webhook delivery from Stripe. Instead, introduce an internal event queue where you’re able to replay failed events at your own terms.
  • Idempotency. Some webhooks, particularly around payment, may trigger actions that potentially lead to data duplicates or inconsistencies (e.g., updating an order status). Ensure that your handlers are idempotent to avoid side effects from potential duplicate delivery, or your own retry logic.

Testing Your Webhook

Thorough testing of your webhook is critical to ensure your system reacts correctly to events sent by Stripe. This part covers how to leverage Stripe’s built-in testing tools and how to simulate events for comprehensive testing.

Using Stripe’s Built-In Testing Tools

Stripe Dashboard provides a testing environment where you can simulate events without needing to make real API calls or transactions. This allows you to verify that your webhook endpoint is correctly receiving and handling events. Here’s how to use it:

Toggle view to test data: Log in to your Stripe Dashboard and toggle the view to Test mode. This ensures that your actions won’t affect live transactions.

It’s always a good idea to experiment in a sandbox environment.

Generate and explore test events: Navigate to the Developers section. Stripe provides a CLI embedded directly in your web console:

Stripe sports and embedded terminal with contextual help. Not as convenient as pressing a button, but it’s an elegant and versatile tool that lets you experiment as you please.

Simulating Events with Stripe CLI

Stripe CLI is a powerful tool for testing and developing with Stripe. It allows you to simulate events that your application can react to. Here’s how to use it:

  1. Installation: If you haven’t already, install Stripe CLI following the official documentation.
  2. Linking to your Stripe account: Run stripe login and follow the instructions. This links the CLI with your Stripe account.
  3. Listening for webhooks locally: To forward events to your local server, use:
stripe listen --forward-to localhost:8080/stripe/webhook

Replace localhost:8080/stripe/webhook with your local webhook handling URL. The CLI will display a webhook secret; use this in your application for verifying Stripe signatures when testing.

  1. Triggering test events: To manually trigger an event to your local endpoint, run a command like this one:
stripe trigger payment_intent.succeeded

Replace payment_intent.succeeded with any event you want to test. This simulates the event as if it occurred naturally within the Stripe environment, allowing you to observe and debug your application’s response.

Addressing Common Issues

  • Event not received: Ensure your server is running and accessible via the URL you’ve provided to stripe listen. Also, check your application’s logs and firewall settings.
  • Signature verification failed: Double-check the endpoint secret used in your code matches the one provided by stripe listen. Also bear in mind that the validity of each request is limited. Generate a fresh request instead of recycling an old one.
  • Unexpected behaviour in business logic: Insert detailed logs at various points in your webhook handler to trace how the event is processed. This can help identify where the logic diverges from expectations.

By utilizing Stripe’s testing tools and being thorough with your tests, you can confidently ensure your webhook integrations perform as intended under various conditions.

Summary

Integrating webhooks into your Stripe payment processing system provides a robust, automated way to handle events and transactions in real-time. By leveraging webhooks, you can greatly improve the efficiency and reliability of your payment system. Whether you are just starting with Stripe or looking to optimize an existing setup, consider webhooks an essential component of your e-commerce infrastructure.

Thank you for reading. The source code is available on GitHub. In our next discussion, we’ll explore how to enhance the resilience of your payment system through proper handling of exceptions and network issues.


Tomas Zezula

Hello! I'm a technology enthusiast with a knack for solving problems and a passion for making complex concepts accessible. My journey spans across software development, project management, and technical writing. I specialise in transforming rough sketches of ideas to fully launched products, all the while breaking down complex processes into understandable language. I believe a well-designed software development process is key to driving business growth. My focus as a leader and technical writer aims to bridge the tech-business divide, ensuring that intricate concepts are available and understandable to all. As a consultant, I'm eager to bring my versatile skills and extensive experience to help businesses navigate their software integration needs. Whether you're seeking bespoke software solutions, well-coordinated product launches, or easily digestible tech content, I'm here to make it happen. Ready to turn your vision into reality? Let's connect and explore the possibilities together.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *