Stripe.Extensions.AspNetCore
0.6.4
dotnet add package Stripe.Extensions.AspNetCore --version 0.6.4
NuGet\Install-Package Stripe.Extensions.AspNetCore -Version 0.6.4
<PackageReference Include="Stripe.Extensions.AspNetCore" Version="0.6.4" />
<PackageVersion Include="Stripe.Extensions.AspNetCore" Version="0.6.4" />
<PackageReference Include="Stripe.Extensions.AspNetCore" />
paket add Stripe.Extensions.AspNetCore --version 0.6.4
#r "nuget: Stripe.Extensions.AspNetCore, 0.6.4"
#:package Stripe.Extensions.AspNetCore@0.6.4
#addin nuget:?package=Stripe.Extensions.AspNetCore&version=0.6.4
#tool nuget:?package=Stripe.Extensions.AspNetCore&version=0.6.4
Stripe .NET Extensions
The Stripe .NET Extension packages provide a collection of convenient features to help improve the experience integrating Stripe in .NET applications.
- Stripe.Extensions.DependencyInjection — configuration and dependency injection support for the Stripe .NET SDK.
- Stripe.Extensions.AspNetCore — webhook handling helpers for Stripe events in ASP.NET Core applications.
- Stripe.Hosting.Aspire — Aspire hosting integration for the Stripe CLI, enabling local webhook forwarding during development.
Install
dotnet add package Stripe.Extensions.DependencyInjection
dotnet add package Stripe.Extensions.AspNetCore
# For Aspire AppHost projects
dotnet add package Stripe.Hosting.Aspire
Building Locally
This project uses Just for build automation.
Prerequisites
- .NET 10.0 SDK or later
- Just (install via
brew install juston macOS/Linux, or see just.systems for other platforms)
Available Commands
# List all available build recipes
just
# Build the solution (Release configuration)
just build
# Run all tests
just test
# Run a specific test
just test-filter "FullyQualifiedName~YourTest"
# Create NuGet packages
just pack
# Clean all build artifacts
just clean
# Full CI pipeline (clean → build → test → pack)
just ci
You can also use dotnet commands directly if you prefer:
dotnet build
dotnet test
dotnet pack
Dependency Injection & Configuration
Using Stripe.Extensions.DependencyInjection you can register named and unnamed versions of StripeClient using AddStripe().
The StripeClient service is registered as scoped.
builder.Services.AddStripe();
The AddStripe() extension also supports registering named StripeClient instances, which uses keyed DI registrations.
builder.Services.AddStripe(); // default client
builder.Services.AddStripe("client1"); // named client1
builder.Services.AddStripe("client2"); // named client2
Configuration
The Stripe API keys need to be configured in your application before calls can be made using the SDK.
The extension packages will look for a Stripe configuration section when calling AddStripe(). Configuring multiple clients is also supported by
using the client name as the key in the configuration section. When configuring the default client without a client name, the key should be Default.
To configure the default client when using AddStripe():
{
"Stripe": {
"Default" : {
"ApiKey": "<secret key>",
"WebhookSecret": "<webhook secret>"
}
}
}
To configure a client named client1 when using AddStripe("client1")::
{
"Stripe": {
"client1": {
"ApiKey": "<secret key>",
"WebhookSecret": "<webhook secret>"
}
}
}
Configuration can also be attached to each registered client by passing a configuration delegate.
// default registration
builder.Services.AddStripe(configureOptions: opts =>
{
opts.ApiKey = "<secret key>";
opts.WebhookSecret = "<webhook secret>";
});
// name registration
builder.Services.AddStripe("client1", opts =>
{
opts.ApiKey = "<secret key>";
opts.WebhookSecret = "<webhook secret>";
});
See StripeOptions for all the available options.
Aspire event forwarding
Stripe.Hosting.Aspire includes convenience APIs for wiring the Stripe CLI to Aspire resources during local development.
var stripe = builder.AddStripeCli("stripe");
stripe.WithWebhookForwardTo(api);
stripe.WithWebhookConnectForwardTo(api);
// Multiple targets are also supported
stripe.WithWebhookForwardTo("/webhooks/stripe", api, worker);
stripe.WithWebhookConnectForwardTo("/webhooks/stripe-connect", api, worker);
// v2 thin events forward to their own endpoint
stripe.WithThinEventForwardTo(notifications, thinEventPath: "/stripe/thin-events");
Snapshot (v1) and thin (v2) events must go to separate endpoints, but one stripe listen session covers both with a single signing secret.
The Stripe CLI's
--thin-eventsflag defaults tonone, so forwarding without it silently delivers nothing.WithThinEventForwardTotherefore always emits it, defaulting to*. Narrow it when you want to:stripe.WithThinEventForwardTo(notifications, thinEvents: ["v2.core.account.created"]);
WithReference(stripe) injects the Stripe API key, publishable key, and webhook secret into dependent resources. The webhook secret is resolved when the CLI starts, so dependent resources can use WaitFor(stripe) before starting.
For Stripe v1 events, use MapStripeWebhookHandler<T>() / StripeWebhookHandler<T>.
For Stripe v2 thin events, use MapStripeEventNotifications() with event subscribers.
Retrieving the default client registered with AddStripe():
public class HomeController : Controller
{
private readonly StripeClient _stripeClient;
public HomeController(StripeClient stripeClient)
{
_stripeClient = stripeClient;
}
public async Task<IActionResult> Index()
{
var customer = await _stripeClient.V1.Customers.GetAsync("cus_NffrFeUfNV2Hib");
...
return View();
}
}
Retrieving a client registered with AddStripe("client1"):
public class HomeController : Controller
{
private readonly StripeClient _stripeClient;
public HomeController([FromKeyedServices("client1")] StripeClient stripeClient)
{
_stripeClient = stripeClient;
}
}
Webhook handling
The Stripe.Extensions.AspNetCore package simplifies Webhook handling by automating the event parsing, signature validation and logging.
All that's needed is to override the appropriate events of the handler class.
Snapshot Events (v1)
Create a handler class that inherits from StripeWebhookHandler, which provides virtual methods for all known webhook events.
To handle an event override the corresponding On*Async method.
public class MyWebhookHandler: StripeWebhookHandler<MyWebhookHandler>();
{
public MyWebhookHandler(StripeWebhookContext context) : base(context) {}
public override Task OnCustomerCreatedAsync(Event e)
{
// handle customer.create event
var customer = (e.Data.Object as Customer);
}
}
Each handler has a single constructor that accepts an instance of StripeWebhookContext, which provides
access to StripeClient, the configured StripeOptions and an instance of ILogger.
The last step is to register the webhook handler with ASP.NET Core routing by calling MapStripeWebhookHandler.
app.MapStripeWebhookHandler<MyWebhookHandler>();
Thin Events (v2)
Stripe v2 APIs generate thin events — lightweight notifications carrying only the event type and the related object's id. These are strongly typed in the Stripe SDK.
Handle them with event subscribers: a small DI-registered class per concern, resolved from the request scope. Each subscriber declares the one notification type it handles, so adding an event means adding a class rather than editing a shared one.
- Thin events use
EventNotificationtypes from theStripe.Events.*namespace - Each notification provides
FetchEventAsync()to get the full event with additional data - Each notification provides
FetchRelatedObjectAsync()to fetch the latest version of the related resource. It throws when the event carries no related object, so checknotification.RelatedObject is not nullfirst - Event types this Stripe.net version cannot type arrive as
UnknownEventNotification
Implement IStripeEventSubscriber<TNotification> once per event you care about:
using Stripe.Events; // notification types
using Stripe.Extensions.AspNetCore;
public sealed class AccountProvisioningSubscriber(IProvisioningService provisioning)
: IStripeEventSubscriber<V2CoreAccountCreatedEventNotification>
{
public async ValueTask HandleAsync(
V2CoreAccountCreatedEventNotification notification,
StripeEventNotificationContext context,
CancellationToken cancellationToken)
{
var account = await notification.FetchRelatedObjectAsync();
await provisioning.CreateWorkspaceAsync(account.Id, cancellationToken);
}
}
Register each subscriber and map one endpoint:
builder.Services.AddStripeEventSubscriber<AccountProvisioningSubscriber>();
builder.Services.AddStripeEventSubscriber<AccountAnalyticsSubscriber>();
app.MapStripeEventNotifications("/stripe/thin-events");
- Fan-out — register several subscribers for the same event and all of them run. One failing does not prevent the others, and every failure is reported.
- Multiple events per class — implement the interface more than once on a single class.
- Constructor injection works normally; subscribers are resolved per request.
- Registering a subscriber for an event type this Stripe.net version does not know fails at startup, not on the first delivery.
Events nobody subscribed to
IStripeUnhandledEventSubscriber receives every notification that no typed subscriber claimed.
That set shrinks as you add subscribers, so it means "the events this app has not accounted for",
not "all events".
That is the difference from a catch-all. A catch-all would be
IStripeEventSubscriber<EventNotification> — subscribing to the base type to receive everything,
including events a typed subscriber already handles. That is rejected at startup, because it would
make delivery to a given subscriber depend on what other subscribers happen to be registered:
'MySubscriber' subscribes to 'EventNotification', which is not a specific event type.
Subscribers are dispatched per event type, so this would never be invoked.
Implement 'IStripeUnhandledEventSubscriber' to handle notifications that no typed subscriber claims.
The two are separate interfaces with different context types, so the distinction is enforced by the compiler rather than by convention:
public sealed class UnhandledEventAuditSubscriber(ILogger<UnhandledEventAuditSubscriber> log)
: IStripeUnhandledEventSubscriber
{
public ValueTask HandleAsync(
StripeUnhandledEventNotificationContext context,
CancellationToken cancellationToken)
{
// IsKnownEventType is false only when this Stripe.net version cannot type the event
// at all - a precise "the SDK is behind the API" signal.
log.LogWarning("{Type} unhandled (known: {Known})",
context.Notification.Type, context.Details.IsKnownEventType);
return ValueTask.CompletedTask;
}
}
If no IStripeUnhandledEventSubscriber is registered at all, the library logs a warning for each
such notification instead, so unclaimed events are never silently dropped. Registering one replaces
that warning with your own handling.
Skipping duplicate deliveries
Stripe retries, so the same notification id can arrive twice. ShouldDispatchAsync runs after
parsing and signature verification but before any subscriber. Return false to skip dispatch; the
endpoint still answers 202 so Stripe stops retrying:
app.MapStripeEventNotifications("/stripe/thin-events", options =>
{
options.ShouldDispatchAsync = async (context, cancellationToken) =>
await store.TryMarkSeenAsync(context.Notification.Id, cancellationToken);
});
Observing the outcome
There is no "post-handle" callback. The endpoint returns an IEndpointConventionBuilder, so ASP.NET Core
filters already wrap it - they compose, resolve services, and can rewrite the response. The one
thing a filter cannot work out for itself is what the endpoint decided, so the endpoint publishes
a StripeEventNotificationResult on HttpContext.Features:
using Microsoft.AspNetCore.Http; // AddEndpointFilter lives here
app.MapStripeEventNotifications("/stripe/thin-events")
.AddEndpointFilter(async (context, next) =>
{
var response = await next(context);
var result = context.HttpContext.Features.Get<StripeEventNotificationResult>();
metrics.Record(result?.EventType, result?.Outcome);
return response;
});
Outcome is Rejected (400), Skipped (202, the gate declined), Dispatched (202) or Failed
(500). The feature is set before the body is read, so it is present even when the request is
rejected - EventType is simply null because nothing parsed. On Failed, Exception is an
AggregateException holding every subscriber failure, not just the first.
A complete worked example lives in samples/SampleEventNotifications.
Dependency Injection in StripeWebhookHandler
The StripeWebhookHandler also supports constructor dependency injection, so Stripe or other services can be injected by defining them as constructor parameters.
public class MyWebhookHandler: StripeWebhookHandler<MyWebhookHandler>
{
private readonly IMyService _myService;
public MyWebhookHandler(IMyService myService, StripeWebhookContext context) : base(context) {}
{
_myService = myService;
}
public override async Task OnCustomerCreatedAsync(Event e)
{
Customer customer = (Customer)e.Data.Object;
await Context.Client.V1.Customers.UpdateAsync(customer.Id, new CustomerUpdateOptions()
{
Description = "New customer"
});
}
}
Unit testing
The StripeWebhookHandler also simplifies unit testing of webhook handling logic.
For example, here is how a unit-test might be written to test the logic of the handler from the previous section:
[Fact]
public async Task UpdatesCustomerOnCreation()
{
var serviceMock = new Mock<CustomerService>();
var handler = new MyWebhookHandler(serviceMock.Object);
// Prepare the event
var e = new Event()
{
Data = new EventData()
{
Object = new Customer()
{
Id = "cus_123"
}
}
};
// Invoke the handler
await handler.OnCustomerCreatedAsync(e);
// Verify that the customer was updated with a new description
serviceMock.Verify(s => s.UpdateAsync(
"cus_123",
It.Is<CustomerUpdateOptions>(o => o.Description == "New customer"),
It.IsAny<RequestOptions>(),
It.IsAny<CancellationToken>()));
}
Aspire Integration
Stripe.Hosting.Aspire adds the Stripe CLI to your Aspire AppHost so it automatically forwards webhook events to your local services during development. It supports two modes: a locally installed Stripe CLI or the official Docker image.
Prerequisites
Local CLI mode: Install the Stripe CLI and run stripe login once.
Docker container mode: Docker must be running. No local Stripe CLI installation required.
Install
In your AppHost project:
dotnet add package Stripe.Hosting.Aspire
Quick start
Store your Stripe API keys as user secrets in the AppHost project:
aspire secret set "Parameters:stripe-api-key" "sk_test_..."
aspire secret set "Parameters:stripe-publishable-key" "pk_test_..."
Then wire up the Stripe CLI in your AppHost:
var builder = DistributedApplication.CreateBuilder(args);
var stripeApiKey = builder.AddParameter("stripe-api-key", secret: true);
var stripePublishableKey = builder.AddParameter("stripe-publishable-key", secret: false);
var api = builder.AddProject<Projects.MyApi>("api");
// Docker container mode (no local Stripe CLI required)
var stripeCli = builder.AddStripeCliContainer("stripe-cli",
apiKey: stripeApiKey,
publishableKey: stripePublishableKey)
.WithWebhookForwardTo(api, webhookPath: "/webhooks/stripe");
// WaitFor ensures the api starts only after the signing secret is captured
api.WithReference(stripeCli)
.WaitFor(stripeCli);
builder.Build().Run();
Switch to the locally installed CLI by replacing AddStripeCliContainer with AddStripeCli:
var stripeCli = builder.AddStripeCli("stripe-cli",
apiKey: stripeApiKey,
publishableKey: stripePublishableKey)
.WithWebhookForwardTo(api, webhookPath: "/webhooks/stripe");
Injected environment variables
WithReference(stripeCli) injects the following environment variables into the dependent service, covering both standalone usage and zero-config services.AddStripe():
| Environment variable | Config path (Stripe:Default:*) |
Value |
|---|---|---|
STRIPE_SECRET_KEY |
Stripe__Default__ApiKey |
Secret API key |
STRIPE_PUBLISHABLE_KEY |
Stripe__Default__PublicKey |
Publishable key |
STRIPE_WEBHOOK_SECRET |
Stripe__Default__WebhookSecret |
Signing secret from CLI output |
Because the Stripe__Default__* variables map directly to the Stripe:Default configuration section, calling services.AddStripe() in the dependent service requires no additional configuration — all values are supplied automatically at startup.
To target a named client (e.g. services.AddStripe("payments")), pass the client name:
api.WithReference(stripeCli, clientName: "payments");
// injects Stripe__payments__ApiKey, Stripe__payments__WebhookSecret, etc.
Forwarding to multiple services
var stripeCli = builder.AddStripeCliContainer("stripe-cli", apiKey: stripeApiKey)
.WithWebhookForwardTo("/webhooks/stripe", api, paymentsService, notificationsService);
Stripe Connect webhooks
var stripeCli = builder.AddStripeCliContainer("stripe-cli", apiKey: stripeApiKey)
.WithWebhookForwardTo(api, webhookPath: "/webhooks/stripe")
.WithWebhookConnectForwardTo(api, webhookPath: "/webhooks/stripe-connect");
Filtering events
var stripeCli = builder.AddStripeCliContainer("stripe-cli", apiKey: stripeApiKey)
.WithWebhookForwardTo(api, webhookPath: "/webhooks/stripe",
events: ["payment_intent.succeeded", "customer.created"]);
How it works
- The Stripe CLI starts with
stripe listen --forward-to <url>(local mode) or as a Docker container (container mode). - The integration watches the CLI stdout for the
whsec_...signing secret printed at startup. - Once captured, a health check on the
stripe-cliresource transitions to Healthy. WaitFor(stripeCli)holds the dependent service until the health check passes, guaranteeingSTRIPE_WEBHOOK_SECRETis populated before the service starts.- On macOS/Windows (Docker Desktop),
localhostin--forward-toURLs is automatically rewritten tohost.docker.internal. On Linux,--add-host=host.docker.internal:host-gatewayis injected into the container runtime args.
Publish mode
The Stripe CLI is a local development tool, so it is excluded from published artifacts (aspire publish). Two things follow from that:
- The webhook secret becomes a deployment parameter. Because the CLI never runs during publish, there is no secret to capture. The environment variables are still emitted, but as an unresolved placeholder for you to supply at deploy time — for example, the Docker Compose publisher writes
STRIPE_WEBHOOK_SECRET: "${STRIPE_CLI_WEBHOOKSIGNINGSECRET}"and adds a matching blank entry to.env. In production, supply the signing secret of a real webhook endpoint created in the Stripe Dashboard rather than one from the CLI. WaitFor(stripeCli)is dropped automatically. Waiting on a resource that was excluded from the manifest would emit a dependency on a service that does not exist in the output (with the Docker Compose publisher,docker compose configrejects the project outright). The integration removes those wait relationships during publish, so you can keep usingWaitForunconditionally in your AppHost. It remains fully active in run mode, where it is what guarantees the secret is populated before your service starts.
No credential is ever written into published artifacts.
Additional Information
Useful links
To keep track of major Stripe API updates and versions, reference the API upgrades page in the Stripe documentation. For a detailed list of API changes, please refer to the API Changelog.
Contributing
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup instructions
- Build and test commands
- Code style guidelines
- Pull request process
License
This project is licensed under the MIT License. See LICENSE.md for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Stripe.Extensions.DependencyInjection (>= 0.6.4)
-
net8.0
- Stripe.Extensions.DependencyInjection (>= 0.6.4)
-
net9.0
- Stripe.Extensions.DependencyInjection (>= 0.6.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.4 | 138 | 8/31/2026 |
| 0.6.1 | 102 | 8/29/2026 |
| 0.3.8 | 522 | 3/26/2026 |
| 0.3.6 | 116 | 3/21/2026 |
| 0.3.4 | 169 | 3/3/2026 |
| 0.2.0 | 590 | 4/10/2025 |
| 0.1.1-preview.1.6 | 168 | 4/1/2025 |
| 0.1.1-preview.1.5 | 175 | 4/1/2025 |
| 0.1.0 | 217 | 4/1/2025 |