Open Closed

Handling Stripe Events #9734


User avatar
0
jasenfici@gmail.com created

Trying to handle callback webhooks from Stripe.

I assume (even though there is literally ZERO documentation on any of this) that in my webhook controller, I need to call paymentRequestService.HandleWebhookCallbackAsync.

I also assume that callback is where all the distributed events are kicked off, or the tenant's subscription is migrated to a specific edition?

I don't really know since none of that is documented (very frustrated, sorry).

However, one of the problems I am having is that whenever I call that method from within the controller, it throws an exception on certain stripe events:

var results = await _paymentRequestAppService.HandleWebhookAsync( "stripe", json, Request.Headers.ToDictionary(k=>k.Key, v=>v.Value.ToString()));

Works fine if the stripe event is customer.subscription.created

Otherwise it throws exceptions for the other events we are supposed to hook into, such as:

customer.subscription.updated checkout.session.completed

``Exception thrown: 'System.MissingMethodException' in System.Private.CoreLib.dll
[16:37:49 ERR] Error processing Stripe webhook
System.MissingMethodException: Method not found: 'System.DateTime Stripe.Subscription.get_CurrentPeriodEnd()'.
   at Volo.Payment.Stripe.StripePaymentGateway.HandleCustomerSubscriptionUpdatedAsync(Event stripeEvent)
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
   at Volo.Payment.Stripe.StripePaymentGateway.HandleCustomerSubscriptionUpdatedAsync(Event stripeEvent)
   at Volo.Payment.Stripe.StripePaymentGateway.HandleWebhookAsync(String payload, Dictionary`2 headers)
   at Volo.Payment.Requests.PaymentRequestAppService.HandleWebhookAsync(String paymentGateway, String payload, Dictionary`2 headers)
   at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
   at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
   at Volo.Abp.GlobalFeatures.GlobalFeatureInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
   at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
   at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
   at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
   at Volo.Abp.Validation.ValidationInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
   at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
   at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
   at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
   at Volo.Abp.Auditing.AuditingInterceptor.ProceedByLoggingAsync(IAbpMethodInvocation invocation, AbpAuditingOptions options, IAuditingHelper auditingHelper, IAuditLogScope auditLogScope)
   at Volo.Abp.Auditing.AuditingInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
   at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
   at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo)
   at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue`1.ProceedAsync()
   at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation)
   at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter`1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
   at SeoGpa.Web.Controllers.PaymentWebhookController.StripeWebhook() in C:\src\SeoGpa\src\SeoGpa.Web\Controllers\PaymentWebhookController.cs:line 95

I've already had to manually update the Stripe.Net library to the most recent because the default version that is packaged with abp.io 9.2.3 is so outdated that the webhook signature API version is no longer supported. Then I've basically had to spend hours searching support tickets to get any of this webhook system to work since there is no active demo, and basically no documentation regarding much of this process (scaffolding the startup doesn't have any documentation on setting up the URL's, I had to get lucky and found some of that in a ticket as well).

Anyway, point 1: As a paid module, I feel this needs a lot more support and documentation, it's really frustrating when I have to spend more time researching how to get something to work than I would have been just writing much of it

Point 2: Should we only be calling _paymentRequestAppService.HandleWebhookAsync only on certain Stripe events? If so, can you please tell us which ones?

thanks! jasen


10 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • The exception you are encountering (System.MissingMethodException: Method not found: 'System.DateTime Stripe.Subscription.get_CurrentPeriodEnd()') is caused by a version mismatch between the Stripe.Net library used by the ABP Payment module and the version you have manually updated to. The ABP Payment module is tightly coupled to a specific Stripe.Net version, and updating Stripe.Net independently can break internal method calls, as you have experienced.

    • The ABP Payment module's webhook handler (_paymentRequestAppService.HandleWebhookAsync) is designed to process Stripe webhook events and update payment/subscription state accordingly. It expects the Stripe.Net types and methods it was built against. If you update Stripe.Net, ABP's code may call methods that no longer exist or have changed signatures.

    • You should only call HandleWebhookAsync for Stripe events that are relevant to payment and subscription state changes, such as:

      • checkout.session.completed
      • customer.subscription.created
      • customer.subscription.updated
      • invoice.paid
      • invoice.payment_failed
      • (and other payment-related events)

      However, the ABP Payment module is designed to internally route and process these events as needed. You do not need to filter events yourself; you should forward all Stripe webhook events to the ABP endpoint, and the module will handle the ones it supports.

    • If you must upgrade Stripe.Net due to deprecated API versions, you will need to ensure that all ABP Payment module code and dependencies are compatible with the new Stripe.Net version. Otherwise, you will encounter runtime errors like the one above.

    • For Stripe webhook endpoint configuration, ensure you are using the correct URL as per the latest ABP documentation:

    yourdomain.com/api/payment/stripe/webhook
    

    (See migration guide for details.)

    • If you encounter unsupported events or missing method exceptions, you should revert to the Stripe.Net version that matches your ABP Payment module, or contact ABP support for an updated module compatible with the latest Stripe.Net.

    • For more information on Stripe integration and webhook configuration in ABP, see the official documentation:

    Sources:

    • https://docs.abp.io/en/commercial/latest/modules/payment#stripeoptions
    • https://abp.io/docs/latest/release-info/migration-guides/abp-5-2

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • User Avatar
    0
    jasenfici@gmail.com created

    If I am forced to use the old version of Stripe.NET then how can I validate the signature that comes through because the only one available in the Stripe dashboard is for 2025-07-30.basil but the Stripe package abp.io uses is not that (its the previous one, i dont remember it's name)

    Also I'm not 100% sure your version mismatch suggestion is the reason, since customer.subscription.created works fine.

    Which Stripe event should be updating the Edition for the tenant? I'm becoming even a little more confused the deeper I dig because I don't see how the webhook is tying the subscription back to a specific tenant and setting the plan. The PaymentRequest objects don't have any tenant information in them that I can see. So this call:

    ``var paymentRequest = await _paymentRequestAppService.CreateAsync(
        new PaymentRequestCreateDto
        {
            Currency = "USD",
            Products = new List<PaymentRequestProductCreateDto>
            {
                new PaymentRequestProductCreateDto
                {
                    PaymentType = PaymentType.Subscription,
                    Name = "Paid Listing Subscription",
                    Code = "PAID_LISTING",
                    Count = 1,
                    UnitPrice = 9.95f,
                    TotalPrice = 9.95f,
                    PlanId = planId,
                    ExtraProperties = null // Get the plan ID for ListingOnly edition
                }
            }
        }
    );
    
    

    Which works, and creates a Stripe subscription, sends back a bunch of webhooks, but there is nothing in those webhooks that would allow us to update the proper tenant edition, or that would successfully kick off all the proper disttributed events. This is on top of the problem of the exception I am getting when processing any event other than subscription_created.

    btw, subscription created works fine, and i can see the stripe subscription Id in PayPaymentRequests table, but that's where it ends. No events kicked off, and the tenant is not updated with the new edition. (and yes, I have the plan associated with an edition, the issue is linking paymentrequests and tenants. How is that done?)

  • User Avatar
    0
    jasenfici@gmail.com created

    Update: Struggling through this just through trial and error.

    On a hunch, I had to guess that the webhooks expect requests to come in on the tenants domain and that is what is used to resolve tenantId. Since I am localhost for now (and using Cookies, not domains for product) this wouldnt work. So, I am now:

    1. manually putting TenantId into the PayPaymentRequest (why cant the module just do it this way by default?)
    2. overriding the default StripePaymentGateway with this:
    ``public override async Task<bool> HandleWebhookAsync(string payload, Dictionary<string, string> headers)
    {
        try
        {
            Guid? tenantId = null;
            string? paymentRequestId = null;
    
            try
            {
                using var jsonDoc = JsonDocument.Parse(payload);
                var root = jsonDoc.RootElement;
                if (root.TryGetProperty("data", out var data) &&
                    data.TryGetProperty("object", out var obj) &&
                    obj.TryGetProperty("metadata", out var metadata) &&
                    metadata.TryGetProperty("PaymentRequestId", out var paymentRequestIdElement))
                    paymentRequestId = paymentRequestIdElement.GetString();
            }
            catch { }
    
            if (!string.IsNullOrEmpty(paymentRequestId) && Guid.TryParse(paymentRequestId, out var requestId))
            {
                try
                {
                    var paymentRequest = await _paymentRequestRepository.GetAsync(requestId);
                    if (paymentRequest.ExtraProperties.TryGetValue("TenantId", out var tenantIdObj) &&
                        Guid.TryParse(tenantIdObj?.ToString(), out var extractedTenantId))
                        tenantId = extractedTenantId;
                }
                catch { }
            }
    
            if (tenantId.HasValue)
            {
                using (_currentTenant.Change(tenantId.Value))
                    await base.HandleWebhookAsync(payload, headers);
            }
            else
            {
                await base.HandleWebhookAsync(payload, headers);
            }
            return true;
        }
        catch
        {
            return true;
        }
    }
    

    Ok, all good so far. Things seem to be moving along.

    However, now I'm stuck with this problem:

    Even though I am 100% sure the method is getting to the end and returning true; I am getting this exception in the output window, and Stripe is telling me its getting a 500 instead of a 200. So this exception is still causing the webhook to error out:

    [20:47:07 DBG] Distributed Event Received - Type: DistributedEventSent, Namespace: Volo.Abp.EventBus.Distributed, Timestamp: "2025-08-06T00:47:07.9927228Z", Data: {"source":0,"eventName":"Volo.Payment.SubscriptionCreated","eventData":{"paymentRequestId":"fa952f98-2f4a-6027-1d5f-3a1b8ecb9415","state":1,"currency":"usd","gateway":"stripe","externalSubscriptionId":"sub_1RsuXmF3WmCAxSyXLnbZdLYA","periodEndDate":"2026-08-05T23:45:22Z","extraProperties":{"TenantId":"c1f5d334-f237-95f3-0e10-3a1b5450e7a6"},"properties":{}}}
    [20:47:07 DBG] Distributed Event Received - Type: DistributedEventReceived, Namespace: Volo.Abp.EventBus.Distributed, Timestamp: "2025-08-06T00:47:07.9968540Z", Data: {"source":0,"eventName":"Volo.Payment.SubscriptionCreated","eventData":{"paymentRequestId":"fa952f98-2f4a-6027-1d5f-3a1b8ecb9415","state":1,"currency":"usd","gateway":"stripe","externalSubscriptionId":"sub_1RsuXmF3WmCAxSyXLnbZdLYA","periodEndDate":"2026-08-05T23:45:22Z","extraProperties":{"TenantId":"c1f5d334-f237-95f3-0e10-3a1b5450e7a6"},"properties":{}}}
    [20:47:08 ERR] Value cannot be null. (Parameter 'serviceType')
    System.ArgumentNullException: Value cannot be null. (Parameter 'serviceType')
       at System.ThrowHelper.Throw(String paramName)
       at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
       at Volo.Abp.EventBus.Distributed.DistributedEventBusBase.AddToInboxAsync(String messageId, String eventName, Type eventType, Object eventData, String correlationId)
       at Volo.Abp.EventBus.Distributed.LocalDistributedEventBus.PublishToEventBusAsync(Type eventType, Object eventData)
       at Volo.Abp.EventBus.Distributed.LocalDistributedEventBus.PublishAsync(Type eventType, Object eventData, Boolean onUnitOfWorkComplete, Boolean useOutbox)
       at Volo.Abp.EventBus.UnitOfWorkEventPublisher.PublishDistributedEventsAsync(IEnumerable`1 distributedEvents)
       at Volo.Abp.Uow.UnitOfWork.CompleteAsync(CancellationToken cancellationToken)
       at Volo.Abp.AspNetCore.Uow.AbpUnitOfWorkMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
       at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.InterfaceMiddlewareBinder.&lt;&gt;c__DisplayClass2_0.&lt;&lt;CreateMiddleware&gt;b__0>d.MoveNext()
    --- End of stack trace from previous location ---
       at Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingMiddleware.InvokeAsync(HttpContext context, RequestDelegate next)
    

    You can see I am logging incoming distributed events for tracing purposes and it looks the Subscrption Created event is successfully being put into the pipeline (twice for reason) but then the fatal error happens.

    Note, this is happening on a checkout.session.completed Stripe event.

    Thanks!

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    [20:47:08 ERR] Value cannot be null. (Parameter 'serviceType')
    System.ArgumentNullException: Value cannot be null. (Parameter 'serviceType')
       at System.ThrowHelper.Throw(String paramName)
    

    Do you have such code in your module?

    Configure<AbpDistributedEventBusOptions>(options =>
    {
        options.Outboxes.Configure(config =>
        {
            config.UseDbContext<YourDbContext>();
        });
        
        options.Inboxes.Configure(config =>
        {
            config.UseDbContext<YourDbContext>();
        });
    });
    

    Thanks.

  • User Avatar
    0
    jasenfici@gmail.com created

    Ok well enabling the full DistributedEvent module (data tables and all) eliminated that message. Ive never used or had to use that module but apparently that set up is required for stripe. that should really really be documented. I wasted hours on that.

    Now however, im getting a new error. When I receive a webhook from Stripe that looks like this:

    [emailed support the json]

    I am getting this error thrown by the StripePaymentGateway:

    [23:49:53 ERR] Volo.Payment.Stripe.StripePaymentGateway Error in Stripe webhook processing System.ArgumentNullException: Value cannot be null. (Parameter 'input')  at System.ArgumentNullException.Throw(String paramName)  at System.Guid.Parse(String input)  at Volo.Payment.Stripe.StripePaymentGateway.HandleCustomerSubscriptionUpdatedAsync(Event stripeEvent)  at Volo.Payment.Stripe.StripePaymentGateway.HandleWebhookAsync(String payload, Dictionary`2 headers)

    I do get sent properly to the PaymentSuccess page, so I could try..catch calling HandleWebhookAsync the problem is the Tenant edition is still not being set, which i'm guessing is because of this error?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    All the modules can work without an inbox and an outbox. You can enable/disable them in your app code. See https://abp.io/docs/latest/framework/infrastructure/event-bus/distributed#outbox-inbox-for-transactional-events

    I have shared the StripePaymentGateway.cs with you. Can you check and try to override its methods? eg HandleCustomerSubscriptionUpdatedAsync

    Thanks.

  • User Avatar
    0
    jasenfici@gmail.com created

    Thank you, this was very helpful.

    It turns out the native code is not sending PaymentRequestId to the subscriptionData, so once I do this...

    var options = new SessionCreateOptions
     {
         Locale = purchaseParameters.Locale,
         PaymentMethodTypes = purchaseParameters.PaymentMethodTypes,
         LineItems = lineItems,
         Mode = modeMapping[paymentRequest.Products.First().PaymentType],
         SuccessUrl = input.ReturnUrl,
         CancelUrl = input.CancelUrl,
         Metadata = new Dictionary<string, string>
         {
             { StripeConsts.ParameterNames.PaymentRequestId, paymentRequest.Id.ToString() }
         },
        SubscriptionData = new SessionSubscriptionDataOptions()
         {
             Metadata = new Dictionary<string, string> { { StripeConsts.ParameterNames.PaymentRequestId, paymentRequest.Id.ToString() }  }
         }
    
     };
    

    ...events started flowing with no errors. However, now I am getting a follow-up error that is problematic:

    [09:24:07 DBG] SeoGpa.Web.EventHandlers.UniversalDistributedEventHandler Distributed Event Received - Type: SubscriptionCreatedEto, Namespace: Volo.Payment.Subscription, Timestamp: 08/06/2025 13:24:07, Data: {"paymentRequestId":"d7a3fd9d-23f1-2fff-12c9-3a1b91ba3d6c","state":0,"currency":"USD","gateway":"stripe","externalSubscriptionId":"sub_1Rt7JyF3WmCAxSyXcXPSoHff","periodEndDate":"2026-08-06T13:23:58Z","extraProperties":{"TenantId":"c1f5d334-f237-95f3-0e10-3a1b5450e7a6"},"properties":{}}
    [09:24:07 ERR] Volo.Abp.Threading.AbpAsyncTimer The given key 'EditionId' was not present in the dictionary.
    System.Collections.Generic.KeyNotFoundException: The given key 'EditionId' was not present in the dictionary.
       at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
       at Volo.Payment.Subscription.SubscriptionCreatedHandler.HandleEventAsync(SubscriptionCreatedEto eventData)
       at Volo.Abp.EventBus.EventHandlerInvoker.InvokeAsync(IEventHandler eventHandler, Object eventData, Type eventType)
       at Volo.Abp.EventBus.EventBusBase.TriggerHandlerAsync(IEventHandlerFactory asyncHandlerFactory, Type eventType, Object eventData, List`1 exceptions, InboxConfig inboxConfig)
       at System.AbpExceptionExtensions.ReThrow(Exception exception)
       at Volo.Abp.EventBus.EventBusBase.ThrowOriginalExceptions(Type eventType, List`1 exceptions)
       at Volo.Abp.EventBus.Distributed.LocalDistributedEventBus.ProcessFromInboxAsync(IncomingEventInfo incomingEvent, InboxConfig inboxConfig)
       at Volo.Abp.EventBus.Distributed.InboxProcessor.RunAsync()
       at Volo.Abp.EventBus.Distributed.InboxProcessor.RunAsync()
       at Volo.Abp.EventBus.Distributed.InboxProcessor.TimerOnElapsed(AbpAsyncTimer arg)
       at Volo.Abp.Threading.AbpAsyncTimer.Timer_Elapsed()
    

    It looks like the receiver for this event that is used to update the tenant edition is not getting the data it needs. In none of the events that are fired off in StripePaymentGateway can I see EditionId being set.

    I can also confirm that the products are all set up and associated properly:

    Unfortunately you can see after all the processing has taken place, the tenant C1F5D334-F237-95F3-0E10-3A1B5450E7A6 is still associated with the wrong Edition. I'm assuming its because this distributed event message is failed because EditionId is missing somewhere?

    Thanks again for your assistance.

  • User Avatar
    0
    jasenfici@gmail.com created

    If I got lookup EditionId via planid and pass it all along in the ExtraProperties things start working completely normal.

    I am forwarding you a copy of my modified StripePaymentGateway.cs file. It's a little ugly with some extraneous logging and some copy and paste from the original, but its pretty basic to see whats going on. It now fully supports:

    1. tenants that dont use domain to determine tenantId
    2. embed required PaymentRequestInfo in the subscription session (allowing create and update subscription to work, and preventing the webhook timing issue that happens often when stripe sends multiple webhooks at once)
    3. embed EditionId into the distributed event message to be pickedup by the Subscription manager, allowing tenant editions to successfully change.

    I feel like these three things are all kind of required for the StripePaymentGateway to actually work, so feel free to integrate them into the Pro code.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Thanks. I will check your code.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you help remove the extra code? I'll compare and update it in the module, but I'm not sure which code is essential right now.

    Thanks.

Learn More, Pay Less
33% OFF
All Trainings!
Get Your Deal
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.0.0-preview. Updated on September 12, 2025, 10:20