In this post, I create a Stripe webhooks receiver for ASP.NET Core and Blazor. This is the first post of 4 where I show the full implementation.
What is a webhook?
WebHooks is a lightweight HTTP pattern providing a simple pub/sub model for wiring together Web APIs and SaaS services. When an event happens in a service, a notification is sent in the form of an HTTP POST request to registered subscribers. The POST request contains information about the event which makes it possible for the receiver to act accordingly.
So, Stripe is one of the provider that offers a webhook for its system.
Configure Stripe
First, we have to configure a new account in the Stripe dashboard after your registration. For this example, I’m going to create a new Account Name with name PSC Test
and the country is United Kingdom
.
After that, you should be in the same environment as in the following screenshot. The new account is in Developers
status and Test mode
is activated.
Now, on the left side you see the Developers menu. Under API Keys, you find the keys to use in the project later. To test the webhook, you have to download the Stripe CLI from GitHub for your operating system. The CLI will help us to test the application. Download the file (it is just an exe
for Windows) and save it in the project folder.
Configure your ASP.NET Core project
Now, the next step is to configure your ASP.NET Core project in order to receive the call to the webhook from Stripe. In my case, I created a solution for a Blazor application hosted in ASP.NET Core website. Therefore, I have to add the Stripe.net
as a NuGet package to the solution.
There are a number of global configuration settings for Stripe.net
. The one you will need to set for this tutorial is your Stripe API Key. Remember, you should never store secrets in your project’s source code. Using ASP.NET’s Secret Manager, add your API key. Also, I added my webhook endpoint signing key.
{
"Stripe": {
"ApiKey": "sk_test_xxxx",
"WebhookSigningKey": "whsec_xxxx"
}
}
Add dependency for StripeOptions
At this point, we have to save an read the configuration for Stripe. For this reason, I create a model with 2 properties: ApiKey
and WebhookSigningKey
.
namespace BlazorStripe.Shared.Models
{
public class StripeOptions
{
public string? ApiKey { get; set; }
public string? WebhookSigningKey { get; set; }
}
}
Now, I have to read those values in the Program.cs
and add the dependency for the project and set the ApiKey
to Stripe.
builder.Services.AddTransient(_ =>
{
return builder.Configuration.GetSection("Stripe").Get<StripeOptions>();
});
string? stripeKey = builder.Configuration["Stripe:ApiKey"];
StripeConfiguration.ApiKey = stripeKey;
Add the controller for the webhook receiver
So, your webhook listener simply be a controller with a single HttpPost
endpoint. Stripe will send POST requests to this endpoint containing details related to a Stripe event. Your controller will determine which type of event it has received and take the appropriate action based on the event.
Now, create a Controllers folder in your project. Within that folder, create a class called StripeWebhook. You will make this a controller class that will be able to respond to any event received from Stripe’s webhooks.
Now, inject any services you will need in the controller using C# dependency injection. Also inject the StripeOptions
interface so you will be able to access your unique webhook signing key.
private readonly string _webhookSecret;
public StripeWebhook(StripeOptions options)
{
_webhookSecret = options?.WebhookSigningKey;
}
In the constructor above, I have extracted the WebhookSigningKey
value from the Options interface and assigned it to a private variable _webhookSecret
.
Next, add a single HttpPost
endpoint for your controller.
This is where you will respond to events sent by Stripe. When you are designing your webhook receiver, please remember that Stripe expects your application to send an Http success response code every time an event is received.
Stripe will notify your webhook receiver for any numbers of events. For example, your application might respond when a customer updates their payment information, when a customer’s payment method will soon expire, or when a charge fails. Your controller should determine which type of event it has received and then take the appropriate action.
[HttpPost]
public async Task<IActionResult> Index()
{
string json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
try
{
var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _webhookSecret);
switch (stripeEvent.Type)
{
case Events.CustomerSourceUpdated:
//make sure payment info is valid
break;
case Events.CustomerSourceExpiring:
//send reminder email to update payment method
break;
case Events.ChargeFailed:
//do something
break;
}
return Ok();
}
catch (StripeException e)
{
return BadRequest();
}
}
The controller action above starts by parsing the body of the request received and saving it as a string json
. Then, the controller verifies that the event was sent by Stripe by comparing the value of the Stripe-Signature with our unique webhook secret.
Finally, we determine the type of event and perform the appropriate action based on the event. In this case, we might have some code that will verify that a customer’s payment method is still valid and update our database records accordingly when a customer.source.updated
event is received.
Similarly, we might send a friendly reminder email in response to the customer.source.expiring
event when a customer’s payment method is set to expire.
Add Swagger
Now, I like to add Swagger to the project to see the APIs and, if it is the case, test them. So, in the server project add the NuGet package Swashbuckle.AspNetCore.SwaggerUI and Swashbuckle.AspNetCore, if not automatically added.
Then, in the Program.cs add
// ...
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ASP.NET Blazor with Stripe Webhooks",
Version = "v1"
});
});
var app = builder.Build();
an then this code
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseSwagger();
app.UseSwaggerUI(c => {
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Blazor with Stripe Webhooks");
});
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
This configuration should be enough to see the Swagger documentation using the URL
https://localhost:7110/swagger
The result of this is the following screenshot.
The new Program.cs
using BlazorStripe.Shared.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.OpenApi.Models;
using Stripe;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
#region Read configuration
var configuration = builder.Configuration;
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env}.json", true, true);
StripeOptions settings = new StripeOptions();
builder.Configuration.Bind(settings);
#endregion Read configuration
#region Dependecy injection
builder.Services.AddTransient(_ =>
{
return builder.Configuration.GetSection("Stripe").Get<StripeOptions>();
});
#endregion
string? stripeKey = builder.Configuration["Stripe:ApiKey"];
StripeConfiguration.ApiKey = stripeKey;
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ASP.NET Blazor with Stripe Webhooks",
Version = "v1"
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
app.UseSwagger();
app.UseSwaggerUI(c => {
c.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Blazor with Stripe Webhooks");
});
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");
app.Run();
Test your controller
So, it is time to test your StripeWebhook
controller. With the Stripe CLI
that we downloaded before, we will test all our code. Now, we have to pair the Stripe CLI
with our account on Stripe. Open the PowerShell (or another prompt) and execute the following line
.\stripe.exe login
At this point, the CLI asks to press Enter
to open a browser and pairs itself with the account in the Stripe Dashboard. After the Enter
, we should see a screen like the following
Click on Allow access. Then, your screen changes and the access is granted.
Then, in your PowerShell, you see the Stripe CLI is configured for your application.
Now, you have to configure the Stripe CLI to receive the call from Stripe and forward the call to the controller StripeWebhook
that we have just created. For that, in the PowerShell type
.\stripe.exe listen --forward-to https://localhost:7110/api/StripeWebhook
As you can see, this step gives you the webhook signing secret. Copy this key and paste in the configuration in the property WebhookSigningKey
. Leave this PowerShell open. Now, run your server project. When the project is up and running, we are in the position to receive a trigger from Stripe. I want to try a successful payment. So, in a new PowerShell, type
.\stripe trigger payment_intent.succeeded
If you follow all steps, you receive a Trigger succeeded
. You can add breakpoints in your code to check what your controller receives. In the Stripe CLI, you read the successful request.
Now, in the open PowerShell, you have all details about the webhook call. You see that the event payment_intent.succeeded
has 3 steps: charge.succeeded
, payment_intent.succeded
and payment_intent.created
. You can write in the controller for each step the appropriate code to respond to your need.
Also, all the events are available in the Stripe Dashboard. Under the option Events
, you can see all the calls to the webhook.
So, if you want to see more details about each event, just click on it. For example, I want to see all the details of the payment (first line in the above screenshot). I click on it and the result is in the following screnshot.