Managing Webhook Events for Connected Accounts

Managing Webhook Events for Connected Accounts

Preface

In this series of blog posts, we’ll explore some key highlights for a modest implementation of a Stripe integration that brings together the capabilities of Stripe Connect and Stripe Billing for a fictitious business, Oasis Hubs.

The business provides customers with access to unique private and commercial workspaces that can be conveniently booked by the hour. Customers will be able to choose between different subscription tiers that will give them access to various workspace listings (hubs) which have been provided by service vendors (hosts). Hosts will sign up to the platform to provide details of their available workspaces, and get paid monthly based on the number of hours booked for their workspace.

Beyond APIs

In the previous article in this series, we examined how to integrate customer registration and onboarding for Stripe Connect accounts into an ASP.NET Core Razor Page application. To do this, we leveraged the Stripe .NET NuGet package which provides a convenient wrapper around the Stripe HTTP APIs. While Stripe does provide an abundance of functionality through API endpoints, there will be situations that require more than a typical request-response interaction.

If we consider some of the typical workflows that happen in the world of business, we understand that there are some processes that can resolve themselves immediately while others might take more time to complete. The financial workflows supported by Stripe also operate in a very similar fashion. Invoking commands to create new customers or products within a Stripe account have the expectation of executing and returning a result almost immediately. On the other hand, carrying out tasks like identity verification or processing delayed payment methods might take minutes, hours or even days. Take a moment and consider the onboarding process of Oasis Hubs in the real world. Verifying those business and banking details for Connect accounts can take some time to complete. So how can we handle these types of interactions? How do we get notified and react to updates that happen to the processes within our Stripe account? This is where we have to go beyond APIs and start thinking about events.

Understanding Stripe Events

As updates happen to the state of the activities running within your Stripe account, various events get generated to inform you that something has changed. This can be extremely beneficial for Stripe integrations as they can subscribe to the events they’re interested in and run the associated workflows. For example, imagine subscribing to an event that lets you know another order was placed for the custom t-shirts your company makes. At this point, you might want to start your fulfillment process of packing and shipping the new order to the customer. The pricing model of Oasis Hubs is based on monthly subscription. It will be important to listen for created and canceled subscription events to know access to listings should be updated for a given user.

When events are triggered in a Stripe account, a new Event object will be created that contains contextual information about what caused the event. The API reference documentation contains an exhaustive list of event types that it supports. While events vary, there are a few key properties on each one that should be paid attention to.

{
  "id": "evt_1NG8Du2eZvKYlo2CUI79vXWy",
  "object": "event",
  "api_version": "2019-02-19",
  "created": 1686089970,
  "data": {   },
  "livemode": false,
  "pending_webhooks": 0,
  "request": {
    "id": null,
    "idempotency_key": null
  },
  "type": "customer.subscription.created"
}

The image above shows a condensed version of what an event object might look like. Probably the most important property of them all is “type”. You will need to inspect this value to determine which event has been triggered; which also has implications for the type of data it’s carrying. Another important property to observe, as you might have guessed, is “data”. This contains the Stripe object that is most relevant to the event that just fired. If you’ve subscribed to and received a customer.updated event, it would be fair to expect that the data property of the event could contain a customer object. The “api_version” property of the event object indicates the version of the Stripe API that the payload is associated with. This can be important when debugging your integration as different versions return payloads with different properties which could cause issues if your installed SDK or parser is expecting a particular shape.

To learn more about setting an API version in your SDK and on your Stripe account, take a look at the following link stripe.com/docs/libraries/set-version

Implementing Webhooks

The way an application can subscribe to event activity firing on a Stripe account is through registering a webhook. This is a common pattern used by service platforms to notify customers via an exposed HTTP endpoint. For the security minded folks, this really should be a HTTPS endpoint and that is a requirement for live mode environments in Stripe. I like to think of webhooks as an implementation of the Hollywood Principle, "Don't Call Us, We'll Call You".

Registering a webhook endpoint with Stripe can be done through the dashboard, but what we will be doing instead is seeing how to run webhook handlers locally using the Stripe CLI. In either case, you have the option to choose the events your integration should subscribe to. There are many different events that can be triggered and receiving them all can easily overwhelm your system.

To create a webhook handler in ASP.NET Core, we will need to add a method to the Oasis Hubs project that can respond to HTTP POST requests and deserialize the Stripe event object contained in the request body. The code sample below shows an action method for a Controller but feel free to use minimal APIs or whatever your preference might be for handling HTTP requests.

[HttpPost("stripe/platform")]
public async Task<IActionResult> PlatformHandler() {
  var payload = await new StreamReader(Request.Body).ReadToEndAsync();
  try {
    var stripeEvent = EventUtility.ConstructEvent(
        payload, Request.Headers["Stripe-Signature"],
        this._stripeConfiguration.GetValue<string>("WebhookSecret"));

    switch (stripeEvent.Type) {
      case Events.InvoicePaid: {
        var invoice = (stripeEvent.Data.Object as Invoice)!;

        this._logger.LogDebug(
            "Initiating funds transfer for paid invoice ({InvoiceId})",
            invoice.Id);

        break;
      }
      default:
        _logger.LogInformation("Unhandled event type: {StripeEvent}",
                               stripeEvent.Type);
        break;
    }

    return Ok();
  } catch (StripeException ex) {
    _logger.LogError(ex, "There was an issue processing this webhook request");
    return BadRequest();
  }
}

Regardless of the language or framework used, most webhook handlers will follow in a similar sequence. First, attach a method to the route at which you’ll want to receive event notifications. Retrieve the event object from the request body and parse the JSON. Since these routes are usually publicly accessible, you will want to validate the request to make sure that it’s actually coming from Stripe. The Stripe.net library comes with a helpful utility that does just that. The EventUtility.ConstructEvent method will validate and deserialize the Event object of the webhook request if you pass it the JSON payload, the value of the Stripe-Signature request header, and the webhook secret associated with your Stripe account.

In addition to request validation, production implementations might also want to consider adding rate limiting or an allow/block list.

Now that you have a strongly typed Event object, you can inspect the Type property, look for any of the event types your integration is concerned with and run any applicable workflows. Notice how the Data.Object property can be cast to the Stripe object associated with the event type.

When creating your webhook handler, there are some behavioral constraints that you should be aware of. For instance, handlers should quickly return a successful (HTTP 200) response once an event is received. Executing long running tasks within the body of your handler is not recommended as that may cause timeouts and Stripe will attempt to resend the event. This retry behavior involves an exponential backoff but does vary between live and test modes. Because of this, it is a good idea for you to implement some logic for deduplicating events or making your event processing idempotent.

A common solution for scaling webhook event processing is to make use of an external queuing system like Azure Service Bus or RabbitMQ. When events are received and validated, the webhook handler will publish a message to a queue on the message broker, and another process listening to that queue can start processing the messages. This allows the webhook handler to do very little work and return quickly. This also enables some scalability as additional listener processes can be added if queued messages need to be processed more quickly. In the .NET space, there are many options to choose from when it comes to message processing frameworks. MassTransit, NServiceBus, and Wolverine are some interesting examples. In the codebase for Oasis Hubs, we’re using an OSS messaging framework called Brighter along with RabbitMQ as the message broker.

When working with subscription based payments, there are many events that get triggered for a business to pay attention to. A few of them include:

  • customer.subscription.created - triggered when a new subscription gets created
  • customer.subscription.deleted - triggered when a subscription ends
  • invoice.created - triggered when a new invoice gets created for a payment period
  • invoice.paid - triggered when the invoice is successfully paid
  • invoice.payment_failed - triggered when a payment attempt for an invoice fails

This is far from a complete list of the events that are involved in a subscription, but also keep in mind that a typical integration might only need to address a handful of these. For Oasis Hubs, we specifically focus on the customer.subscription.created, customer.subscription.updated and invoice.paid events.

switch (stripeEvent.Type) {
  case Events.InvoicePaid: {
    var invoice = (stripeEvent.Data.Object as Invoice)!;

    await this._commandProcessor.PostAsync(
        new InitiateFundsTransferCommand(invoice));
    break;
  }
  case Events.CustomerSubscriptionCreated: {
    var newSubscription = (stripeEvent.Data.Object as Subscription)!;

    await this._commandProcessor.PostAsync(
        new ActivateCustomerSubscriptionCommand(newSubscription));

    break;
  }
  case Events.CustomerSubscriptionUpdated: {
    var updatedSubscription = (stripeEvent.Data.Object as Subscription)!;

    await this._commandProcessor.PostAsync(
        new ActivateCustomerSubscriptionCommand(updatedSubscription));

    break;
  }
  default:
    _logger.LogInformation("Unhandled event type: {StripeEvent}",
                           stripeEvent.Type);
    break;
}

The sample above shows a slimmed down version of the actual implementation, but the core pattern is the same. Check the event type, cast the event object to its actual type, and then send a message to the queue. Instead of having to work directly with strings, the Stripe .NET library provides an Events class that contains constant string representations for all the event types. It’s definitely saved me time from having to debug typos in my webhook handlers.

To locally test your webhook integration, the Stripe CLI has two commands that are particularly useful. When running a project on localhost, you’ll want to be able to capture Stripe events in your local instance so you can inspect things with your debugging tools. In the command line, you can run “stripe listen --forward-to ”, supplying it with the local URL to forward the test mode events to. Here’s an example using the CLI command with the action method shown earlier.

stripe listen --forward-to http://localhost:5000/api/webhooks/platform

Now that you have a listener, you’ll want to trigger some test events. You can do this by interacting with objects through Stripe Dashboard or by triggering them manually using the CLI trigger command.

stripe trigger invoice.paid

Connected Events

Working with Stripe Connect accounts adds an interesting dynamic to handling events. If the main Stripe account has multiple connected accounts attached to it, each with their own activities occurring, how can we know who the trigger event belongs to? Luckily, we already have 90% of the setup already done. When registering your webhooks in the Stripe Dashboard, set the listen to option to “Events on Connected accounts”.

Configure webhooks

With the Stripe CLI, you can add the “--forward-connect-to” switch to the listen command.

stripe listen --forward-to http://localhost:5000/api/webhooks/platform --forward-connect-to  http://localhost:5000/api/webhooks/connect

A recommended approach, and how Oasis Hubs has been integrated, is to have separate webhook endpoints for regular account events and Connect events. This greatly helps with simplifying the logic of your handlers and makes the code more readable. In Oasis Hubs, for example, we look out for the account.updated events to get notified when a new connect account successfully completes onboarding. You can also look out for the account.application.deauthorized as well to know when a Connect account disconnects from your platform.

When handling Connect events, it is expected that there will be multiple Connect accounts associated with your application so it is important to know which account triggered the event. Conveniently, each event generated by a Connect account also contains a top-level “account” property that contains the id for the source account. Now, any actions run in response to Connect events will be able to operate in the correct context.

References

Stay connected with Stripe

You can also stay up-to-date with Stripe developer updates on the following platforms:

📣 Follow @StripeDev on Twitter. 📺 Subscribe to our YouTube channel. 💬 Join the official Discord server. 📧 Sign up for the Developer Digest.