Skip to content

Add EventNotificationHandler#3250

Merged
xavdid-stripe merged 14 commits intobetafrom
DEVSDK-2822
Dec 16, 2025
Merged

Add EventNotificationHandler#3250
xavdid-stripe merged 14 commits intobetafrom
DEVSDK-2822

Conversation

@xavdid-stripe
Copy link
Copy Markdown
Contributor

@xavdid-stripe xavdid-stripe commented Nov 11, 2025

Why?

We've been designing a streamlined approach to handling incoming events that is easy to get right and hard to get wrong. This PR has the initial implementation of this new system.

The only other pending item is to add a method to allow handling a webhook without verifying the signature. This is good for testing and for Event Bridge, which doesn't use the signature-based verification. Otherwise, this is ready for review.

What?

  • add EventHandler class
  • add event handler constructor on StripeClient
  • add tests
  • add method for changing the stripe context on a client

Example usage

[Route("api/[controller]")]
[ApiController]
public class EventNotificationWebhookHandler : ControllerBase
{
    private readonly StripeClient client;
    private readonly StripeEventNotificationHandler handler;

    public EventNotificationWebhookHandler()
    {
        client = new StripeClient(Environment.GetEnvironmentVariable("STRIPE_API_KEY"));
        handler = client.NotificationHandler(Environment.GetEnvironmentVariable("WEBHOOK_SECRET") ?? string.Empty, FallbackCallback);

        // register handlers
        handler.V1BillingMeterErrorReportTriggered += HandleBillingMeterErrorReportTriggeredEventNotification;
    }

    private void HandleBillingMeterErrorReportTriggeredEventNotification(object sender, Stripe.StripeEventNotificationEventArgs<Stripe.Events.V1BillingMeterErrorReportTriggeredEventNotification> e)
    {
        var meter = e.EventNotification.FetchRelatedObject();
        Console.WriteLine($"Meter {meter.DisplayName} had an error");
    }

    private void FallbackCallback(object sender, Stripe.StripeUnhandledEventNotificationEventArgs e)
    {
        Console.WriteLine($"Received unhandled event notification type: {e.EventNotification.Type}");
    }

    [HttpPost]
    public async Task<IActionResult> Index()
    {
        var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
        handler.Handle(json, Request.Headers["Stripe-Signature"]);
        return null;
    }
}

See Also

@xavdid-stripe xavdid-stripe changed the base branch from master to beta November 15, 2025 18:12
Comment thread src/Stripe.net/Infrastructure/Public/StripeEventRouter.cs Outdated
Comment thread justfile Outdated
@xavdid-stripe xavdid-stripe changed the title add "inverted" event handler add event handler class Nov 20, 2025
@xavdid-stripe xavdid-stripe marked this pull request as ready for review November 20, 2025 00:07
@xavdid-stripe xavdid-stripe requested a review from a team as a code owner November 20, 2025 00:07
@jar-stripe
Copy link
Copy Markdown
Contributor

Not sure if the comment should go here or in your doc, but HandleUnhandledError seems like the wrong name in your example. That's just HandleUnhandledEventNotification, right?

/// </summary>
/// <param name="eventNotification">The event notification instance.</param>
/// <param name="client">The StripeClient instance.</param>
public StripeEventNotificationEventArgs(TEventNotification eventNotification, StripeClient client)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this ever get constructed outside of our SDK code? If not, I would consider changing this constructor to be internal

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't users need it for tests? Like, if they write their function and want to test it in isolation, that function takes these args and they'd nee to be able to construct it?

Copy link
Copy Markdown
Contributor

@jar-stripe jar-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a pass thru, it looks largely good (the names are pretty long and I left a comment in your naming doc), but I have concerns about StripeClient/ApiRequestor reuse when you swap in and out the StripeContext

Comment thread src/Stripe.net/Infrastructure/Public/StripeEventNotificationEventArgs.cs Outdated
Comment thread src/Stripe.net/Infrastructure/Public/StripeEventRouter.cs Outdated
Comment thread justfile Outdated
Comment thread src/Stripe.net/Infrastructure/Public/StripeEventRouter.cs Outdated
@jar-stripe
Copy link
Copy Markdown
Contributor

Also, we should consider adding the example from the PR description as an actual example in the project, so folks can see how to use this from code they can easily find.

@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented Dec 2, 2025

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@xavdid-stripe
Copy link
Copy Markdown
Contributor Author

ok @jar-stripe this is ready for re-review!

My biggest open question is about supporting async - in Node, all of the callbacks return promises for easier integration with making stripe client calls. In python we haven't added support for async (yet) since i'm not positive how we'll do it. For dotnet, I know users can call async functions syncronously. So, is it worth having Handle be async and all of the callbacks be too?

Also re: naming. Dotnet having a built-in event thing called a "handler" (that other languages call a "callback") muddles our naming a bit. Is it worth diverging from the other languages here for consistency? Basically, the fill-in-the-blank exercise we did for the other languages is:

A user instantiates a _____, registers 1+ ______, and then invokes that [1st blank] from their (framework-specific) webhook endpoint.

Today, it's

A user instantiates an event (notification) handler, registers 1+ callback functions, and then invokes that handler from their (framework-specific) webhook endpoint.

But should dotnet be different?

@xavdid-stripe xavdid-stripe changed the title add event handler class Add EventNotificationHandler Dec 16, 2025
@xavdid-stripe
Copy link
Copy Markdown
Contributor Author

ignoring CLA check (??) and merging

@xavdid-stripe xavdid-stripe merged commit b1c3bac into beta Dec 16, 2025
7 of 8 checks passed
@xavdid-stripe xavdid-stripe deleted the DEVSDK-2822 branch December 16, 2025 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants