The Billing service processes Stripe webhooks and manages billing-related events for subscriptions, payments, and invoices in the Bitwarden ecosystem.
Overview
The Billing service provides:
Stripe Webhooks : Process payment and subscription events from Stripe
Event Handlers : Specialized handlers for different Stripe event types
Provider Support : Managed Service Provider (MSP) billing operations
Invoice Processing : Handle invoices, payments, and refunds
Subscription Management : Track subscription lifecycle events
Background Jobs : Scheduled billing operations and maintenance
Architecture
Configuration
From src/Billing/Startup.cs:32:
public void ConfigureServices ( IServiceCollection services )
{
// Settings
var globalSettings = services . AddGlobalSettingsServices ( Configuration , Environment );
services . Configure < BillingSettings >( Configuration . GetSection ( "BillingSettings" ));
var billingSettings = Configuration . GetSection ( "BillingSettings" ). Get < BillingSettings >();
// Stripe Configuration
StripeConfiguration . ApiKey = globalSettings . Stripe . ApiKey ;
StripeConfiguration . MaxNetworkRetries = globalSettings . Stripe . MaxNetworkRetries ;
// Data Protection
services . AddCustomDataProtectionServices ( Environment , globalSettings );
// Repositories
services . AddDatabaseRepositories ( globalSettings );
// PayPal IPN Client
services . AddHttpClient < IPayPalIPNClient , PayPalIPNClient >();
// Context
services . AddScoped < ICurrentContext , CurrentContext >();
// Stripe Event Handlers
services . AddScoped < IStripeEventUtilityService , StripeEventUtilityService >();
services . AddScoped < ISubscriptionDeletedHandler , SubscriptionDeletedHandler >();
services . AddScoped < ISubscriptionUpdatedHandler , SubscriptionUpdatedHandler >();
services . AddScoped < IUpcomingInvoiceHandler , UpcomingInvoiceHandler >();
services . AddScoped < IChargeSucceededHandler , ChargeSucceededHandler >();
services . AddScoped < IChargeRefundedHandler , ChargeRefundedHandler >();
services . AddScoped < ICustomerUpdatedHandler , CustomerUpdatedHandler >();
services . AddScoped < IInvoiceCreatedHandler , InvoiceCreatedHandler >();
services . AddScoped < IPaymentFailedHandler , PaymentFailedHandler >();
services . AddScoped < IPaymentMethodAttachedHandler , PaymentMethodAttachedHandler >();
services . AddScoped < IPaymentSucceededHandler , PaymentSucceededHandler >();
services . AddScoped < IInvoiceFinalizedHandler , InvoiceFinalizedHandler >();
services . AddScoped < ISetupIntentSucceededHandler , SetupIntentSucceededHandler >();
services . AddScoped < ICouponDeletedHandler , CouponDeletedHandler >();
services . AddScoped < IStripeEventProcessor , StripeEventProcessor >();
// Services
services . AddScoped < IStripeFacade , StripeFacade >();
services . AddScoped < IStripeEventService , StripeEventService >();
services . AddScoped < IProviderEventService , ProviderEventService >();
services . AddScoped < IPushNotificationAdapter , PushNotificationAdapter >();
// Quartz Job Scheduler
services . AddQuartz ( q =>
{
q . UseMicrosoftDependencyInjectionJobFactory ();
});
services . AddQuartzHostedService ();
// Background Jobs
Jobs . JobsHostedService . AddJobsServices ( services );
services . AddHostedService < Jobs . JobsHostedService >();
}
Stripe Webhook Endpoint
From src/Billing/Controllers/StripeController.cs:38:
[ HttpPost ( "webhook" )]
public async Task < IActionResult > PostWebhook ([ FromQuery ] string key )
{
// Verify webhook key
if ( ! CoreHelpers . FixedTimeEquals ( key , _billingSettings . StripeWebhookKey ))
{
_logger . LogError ( "Stripe webhook key does not match configured webhook key" );
return new BadRequestResult ();
}
// Parse and validate event
var parsedEvent = await TryParseEventFromRequestBodyAsync ();
if ( parsedEvent is null )
{
return Ok ( new
{
Processed = false ,
Message = "Could not find a configured webhook secret to process this event with"
});
}
// Verify API version compatibility
if ( StripeConfiguration . ApiVersion != parsedEvent . ApiVersion )
{
_logger . LogWarning (
"Stripe webhook's API version ({WebhookAPIVersion}) does not match SDK ({SDKAPIVersion})" ,
parsedEvent . ApiVersion ,
StripeConfiguration . ApiVersion );
return Ok ( new { Processed = false , Message = "API version mismatch" });
}
// Validate cloud region
if ( ! await _stripeEventService . ValidateCloudRegion ( parsedEvent ))
{
return Ok ( new { Processed = false , Message = "Event is not for this cloud region" });
}
// Process the event
await _stripeEventProcessor . ProcessEventAsync ( parsedEvent );
return Ok ( new { Processed = true , Message = "Processed" });
}
Endpoint : POST /stripe/webhook?key={webhook_key}
Authentication : Webhook key verification
Content-Type : application/json
Event Handlers
The Billing service implements specialized handlers for different Stripe event types:
Subscription Events
customer.subscription.deleted Handle subscription cancellation and cleanup
customer.subscription.updated Process subscription plan changes and upgrades
invoice.upcoming Preview upcoming invoice and notify user
invoice.finalized Finalize invoice before payment attempt
Payment Events
charge.succeeded Process successful payment
charge.refunded Handle payment refunds
payment_intent.succeeded Confirm payment intent completion
payment_intent.payment_failed Handle failed payment attempts
Customer Events
customer.updated Sync customer data changes
payment_method.attached Track new payment method additions
setup_intent.succeeded Confirm payment method setup
coupon.deleted Handle promotional code deletion
Event Processing Flow
public interface IStripeEventProcessor
{
Task ProcessEventAsync ( Event parsedEvent );
}
public class StripeEventProcessor : IStripeEventProcessor
{
public async Task ProcessEventAsync ( Event parsedEvent )
{
switch ( parsedEvent . Type )
{
case "customer.subscription.deleted" :
await _subscriptionDeletedHandler . HandleAsync ( parsedEvent );
break ;
case "customer.subscription.updated" :
await _subscriptionUpdatedHandler . HandleAsync ( parsedEvent );
break ;
case "invoice.payment_succeeded" :
await _paymentSucceededHandler . HandleAsync ( parsedEvent );
break ;
case "invoice.payment_failed" :
await _paymentFailedHandler . HandleAsync ( parsedEvent );
break ;
case "charge.succeeded" :
await _chargeSucceededHandler . HandleAsync ( parsedEvent );
break ;
case "charge.refunded" :
await _chargeRefundedHandler . HandleAsync ( parsedEvent );
break ;
// ... additional event types
}
}
}
Subscription Lifecycle
Subscription Created
Handled through API service during organization upgrade.
Subscription Updated
Receive Event
Stripe sends customer.subscription.updated webhook
Extract Changes
Parse subscription changes (plan, quantity, status)
Update Database
Sync changes to organization subscription record
Notify User
Send email notification if plan changed
Subscription Canceled
From subscription deleted handler:
public async Task HandleAsync ( Event parsedEvent )
{
var subscription = await GetSubscriptionAsync ( parsedEvent );
var organization = await GetOrganizationAsync ( subscription );
if ( organization != null )
{
// Disable organization
organization . Enabled = false ;
organization . ExpirationDate = DateTime . UtcNow ;
await _organizationRepository . ReplaceAsync ( organization );
// Send cancellation email
await _mailService . SendOrganizationCanceledEmailAsync ( organization );
// Log event
await _eventService . LogOrganizationEventAsync (
organization ,
EventType . Organization_Disabled );
}
}
Invoice Processing
Payment Succeeded
When an invoice is paid successfully:
Payment Succeeded Handler
public async Task HandleAsync ( Event parsedEvent )
{
var invoice = await GetInvoiceAsync ( parsedEvent );
var organization = await GetOrganizationAsync ( invoice );
if ( organization != null )
{
// Create transaction record
var transaction = new Transaction
{
Amount = invoice . AmountPaid / 100M ,
Gateway = GatewayType . Stripe ,
GatewayId = invoice . Id ,
Type = TransactionType . Charge ,
CreationDate = DateTime . UtcNow ,
OrganizationId = organization . Id
};
await _transactionRepository . CreateAsync ( transaction );
// Send receipt email
await _mailService . SendInvoiceUpcomingAsync (
organization . BillingEmailAddress (),
invoice . AmountDue / 100M ,
invoice . DueDate );
}
}
Payment Failed
When payment fails:
Receive Webhook
invoice.payment_failed event received
Update Organization
Mark organization as at-risk or disabled based on retry count
Notify User
Send payment failed email with retry information
Log Event
Record payment failure in event log
Provider Billing
The service supports Managed Service Provider (MSP) billing:
public interface IProviderEventService
{
Task ProcessSubscriptionUpdatedAsync ( Provider provider );
Task ProcessPaymentSucceededAsync ( Provider provider , Invoice invoice );
Task ProcessPaymentFailedAsync ( Provider provider , Invoice invoice );
}
Provider features:
Consolidated billing for multiple client organizations
Per-seat pricing
Automatic client organization synchronization
Volume-based discounts
Background Jobs
From src/Billing/Startup.cs:107:
services . AddQuartz ( q =>
{
q . UseMicrosoftDependencyInjectionJobFactory ();
});
services . AddQuartzHostedService ();
Jobs . JobsHostedService . AddJobsServices ( services );
services . AddHostedService < Jobs . JobsHostedService >();
Scheduled jobs:
Invoice Finalization : Prepare invoices before payment
Usage Tracking : Sync seat usage with Stripe
Failed Payment Retry : Monitor and retry failed payments
Subscription Cleanup : Remove expired subscriptions
PayPal Integration
The service includes PayPal IPN (Instant Payment Notification) support:
services . AddHttpClient < IPayPalIPNClient , PayPalIPNClient >();
PayPal webhook endpoint:
PayPal integration is deprecated in favor of Stripe for new subscriptions.
Middleware Pipeline
From src/Billing/Startup.cs:123:
public void Configure ( IApplicationBuilder app , IWebHostEnvironment env )
{
// Security headers
app . UseMiddleware < SecurityHeadersMiddleware >();
// Development tools
if ( env . IsDevelopment ())
{
app . UseDeveloperExceptionPage ();
app . UseSwagger ();
app . UseSwaggerUI ( c =>
{
c . SwaggerEndpoint ( "/swagger/v1/swagger.json" , "Billing API V1" );
});
}
// Static files
app . UseStaticFiles ();
// Routing
app . UseRouting ();
// Authentication & Authorization
app . UseAuthentication ();
app . UseAuthorization ();
// Controllers
app . UseEndpoints ( endpoints => endpoints . MapDefaultControllerRoute ());
}
Stripe Facade
The service uses a facade pattern for Stripe API interactions:
public interface IStripeFacade
{
Task < Customer > GetCustomerAsync ( string customerId );
Task < Subscription > GetSubscriptionAsync ( string subscriptionId );
Task < Invoice > GetInvoiceAsync ( string invoiceId );
Task < Charge > GetChargeAsync ( string chargeId );
Task UpdateSubscriptionAsync ( string subscriptionId , SubscriptionUpdateOptions options );
Task CancelSubscriptionAsync ( string subscriptionId );
}
Benefits:
Centralized error handling
Retry logic for transient failures
Logging and instrumentation
Testability through mocking
Security
Webhook Signature Verification
Always verify webhook signatures to prevent spoofing attacks.
private async Task < Event > TryParseEventFromRequestBodyAsync ()
{
var json = await new StreamReader ( HttpContext . Request . Body ). ReadToEndAsync ();
var signature = HttpContext . Request . Headers [ "Stripe-Signature" ];
try
{
return EventUtility . ConstructEvent (
json ,
signature ,
_billingSettings . StripeWebhookSecret ,
throwOnApiVersionMismatch : false );
}
catch ( StripeException ex )
{
_logger . LogError ( ex , "Failed to parse Stripe webhook" );
return null ;
}
}
API Version Validation
From src/Billing/Controllers/StripeController.cs:57:
if ( StripeConfiguration . ApiVersion != parsedEvent . ApiVersion )
{
_logger . LogWarning (
"Stripe webhook API version mismatch: webhook={0}, SDK={1}" ,
parsedEvent . ApiVersion ,
StripeConfiguration . ApiVersion );
}
Cloud Region Validation
For multi-region deployments:
public async Task < bool > ValidateCloudRegion ( Event stripeEvent )
{
var customer = await _stripeFacade . GetCustomerAsync ( stripeEvent . CustomerId );
var region = customer . Metadata . GetValueOrDefault ( "region" );
return region == _globalSettings . CloudRegion ;
}
Deployment
Environment Variables
GLOBALSETTINGS__SELFHOSTED = true
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING =< connection >
GLOBALSETTINGS__STRIPE__APIKEY =< stripe_secret_key >
BILLINGSETTINGS__STRIPEWEBHOOKKEY =< webhook_key >
BILLINGSETTINGS__STRIPEWEBHOOKSECRET =< webhook_secret >
Docker
docker run -d \
--name bitwarden-billing \
-p 5007:5000 \
-e GLOBALSETTINGS__Stripe__ApiKey="<stripe_key>" \
-e BILLINGSETTINGS__StripeWebhookKey="<webhook_key>" \
bitwarden/billing:latest
Stripe Webhook Configuration
Create Webhook
In Stripe Dashboard, create webhook endpoint
Set URL
https://billing.bitwarden.com/stripe/webhook?key={webhook_key}
Select Events
Subscribe to relevant event types
Copy Secret
Save webhook signing secret to configuration
Test Webhook
Send test event to verify configuration
Monitoring
Health Checks
curl http://billing:5000/alive
Webhook Monitoring
Monitor webhook processing:
Event processing success rate
Average processing time
Failed event count
Retry queue depth
Stripe Dashboard
Use Stripe Dashboard to:
View webhook delivery status
Retry failed webhooks
Monitor API usage
Track subscription metrics
Troubleshooting
Common Issues
Issue Solution Webhook signature invalid Verify webhook secret matches Stripe API version mismatch Update Stripe SDK to match webhook version Duplicate events Implement idempotency checks Missing customer Verify customer exists before processing Region mismatch Check customer metadata region tag
Debug Logging
{
"Logging" : {
"LogLevel" : {
"Bit.Billing" : "Debug" ,
"Stripe" : "Debug"
}
}
}
Best Practices
Idempotency : Handle duplicate webhook events gracefully
Async Processing : Process webhooks asynchronously when possible
Error Handling : Return 200 OK even for handled errors
Timeout : Process webhooks within 30 seconds to avoid retries
Logging : Log all webhook events for audit trail
Testing : Use Stripe CLI for local webhook testing