Webhook Handlers
Handler registration
Standalone mode (PolarSharp.Webhooks only)
When you install only PolarSharp.Webhooks without the PolarSharp core package:
// Program.cs
builder.Services
.AddPolarWebhooks()
.AddWebhookHandler<OrderCreatedEvent, OrderCreatedHandler>()
.AddWebhookHandler<OrderPaidEvent, OrderPaidHandler>()
.AddWebhookHandler<SubscriptionActiveEvent, SubscriptionActiveHandler>()
.AddWebhookHandler<SubscriptionCanceledEvent, SubscriptionCanceledHandler>()
.AddWebhookHandler<CustomerCreatedEvent, CustomerCreatedHandler>()
.AddWebhookHandler<RefundCreatedEvent, RefundHandler>();
// ...
app.MapPolarWebhooks(); // maps POST /hooks/polar directly
Full-stack mode (PolarSharp + PolarSharp.Webhooks)
When you install both packages, webhooks integrate into the shared infrastructure builder:
builder.Services
.AddPolarInfrastructure(builder.Configuration)
.AddPolarWebhooks()
.AddWebhookHandler<OrderCreatedEvent, OrderCreatedHandler>()
.AddWebhookHandler<OrderPaidEvent, OrderPaidHandler>()
.AddWebhookHandler<SubscriptionActiveEvent, SubscriptionActiveHandler>()
.AddWebhookHandler<SubscriptionCanceledEvent, SubscriptionCanceledHandler>()
.AddWebhookHandler<CustomerCreatedEvent, CustomerCreatedHandler>()
.AddWebhookHandler<RefundCreatedEvent, RefundHandler>();
// ...
app.UsePolarInfrastructure(); // maps POST /hooks/polar automatically
Handlers are registered as Scoped DI services — they participate in the incoming HTTP request's DI scope, so you can inject DbContext, ICurrentUser, and other scoped services normally.
All 28 known event types
| Event type string | .NET record type |
|---|---|
order.created |
OrderCreatedEvent |
order.updated |
OrderUpdatedEvent |
order.paid |
OrderPaidEvent |
order.refunded |
OrderRefundedEvent |
subscription.created |
SubscriptionCreatedEvent |
subscription.active |
SubscriptionActiveEvent |
subscription.updated |
SubscriptionUpdatedEvent |
subscription.canceled |
SubscriptionCanceledEvent |
subscription.uncanceled |
SubscriptionUncanceledEvent |
subscription.past_due |
SubscriptionPastDueEvent |
subscription.revoked |
SubscriptionRevokedEvent |
checkout.created |
CheckoutCreatedEvent |
checkout.updated |
CheckoutUpdatedEvent |
checkout.expired |
CheckoutExpiredEvent |
customer.created |
CustomerCreatedEvent |
customer.updated |
CustomerUpdatedEvent |
customer.state_changed |
CustomerStateChangedEvent |
customer.deleted |
CustomerDeletedEvent |
product.created |
ProductCreatedEvent |
product.updated |
ProductUpdatedEvent |
benefit.created |
BenefitCreatedEvent |
benefit.updated |
BenefitUpdatedEvent |
benefit.grant.created |
BenefitGrantCreatedEvent |
benefit.grant.updated |
BenefitGrantUpdatedEvent |
benefit.grant.cycled |
BenefitGrantCycledEvent |
benefit.grant.revoked |
BenefitGrantRevokedEvent |
refund.created |
RefundCreatedEvent |
refund.updated |
RefundUpdatedEvent |
See Webhook Event Reference for full payload schemas for each event type.
Handler base class
PolarWebhookHandlerBase<TEvent> seals all infrastructure orchestration. Implement only HandleCoreAsync:
public sealed class OrderPaidHandler : PolarWebhookHandlerBase<OrderPaidEvent>
{
private readonly IEntitlementService _entitlements;
private readonly IEmailSender _email;
public OrderPaidHandler(
IEntitlementService entitlements,
IEmailSender email,
ILogger<OrderPaidHandler> logger) : base(logger)
{
_entitlements = entitlements;
_email = email;
}
protected override async Task HandleCoreAsync(OrderPaidEvent @event, CancellationToken ct)
{
// Use @event.WebhookId for idempotency — Polar delivers at-least-once
var orderId = OrderId.From(@event.Data.Id);
await _entitlements.GrantAsync(orderId, ct);
await _email.SendReceiptAsync(@event.Data.Customer.Email, @event.Data, ct);
}
}
The base class handles:
- Logging
Informationon event received / handled - Cancellation token scoping (HTTP disconnect does NOT abort an in-flight handler)
- Metrics via
polar.webhooks.received
Logging-only handlers
For event types you want to acknowledge but not process, use a simple logging handler:
public sealed class OrderUpdatedHandler : PolarWebhookHandlerBase<OrderUpdatedEvent>
{
public OrderUpdatedHandler(ILogger<OrderUpdatedHandler> logger) : base(logger) { }
protected override Task HandleCoreAsync(OrderUpdatedEvent @event, CancellationToken ct)
{
Logger.LogInformation("Order {OrderId} updated (webhook {WebhookId})",
@event.Data.Id, @event.WebhookId);
return Task.CompletedTask;
}
}
This pattern is used by PolarWebhooksTestApp to register all 28 handlers without writing any business logic.
Completeness check
PolarWebhookStartupValidator runs before the app accepts traffic and warns about every unhandled event type:
[WRN] PolarSharp Webhooks: No handler registered for event type 'product.updated'
(ProductUpdatedEvent). If Polar delivers this event, it will be silently discarded.
Register: .AddWebhookHandler<ProductUpdatedEvent, YourHandler>()
Set FailOnMissingHandlers: true in appsettings.json to fail startup instead:
{
"PolarSharp": {
"Webhooks": {
"FailOnMissingHandlers": true
}
}
}
dotnet new polar-handler scaffold
Install the template pack once and scaffold any handler in one command:
dotnet new install PolarSharp.Templates
dotnet new polar-handler --event OrderCreatedEvent --name OrderCreatedHandler --output Handlers/
The generated file is compilable, XML-documented, and includes all available @event.Data property references for the chosen event type.