Skip to main content
Wolfix.Server integrates with Stripe for payment processing in the Order module. This guide covers configuration, implementation, and testing of Stripe payments.

Overview

The Order module uses Stripe for:
  • Payment Intents - Secure payment processing
  • Webhooks - Real-time payment notifications
  • Customer Management - Stripe customer profiles
  • Refunds - Payment refund processing

Configuration

Environment Variables

Configure Stripe credentials in your .env file:
.env
# Test keys (development)
STRIPE_PUBLISHABLE_KEY=pk_test_51...
STRIPE_SECRET_KEY=sk_test_51...
STRIPE_WEBHOOK_KEY=whsec_...

# Live keys (production)
STRIPE_PUBLISHABLE_KEY=pk_live_51...
STRIPE_SECRET_KEY=sk_live_51...
STRIPE_WEBHOOK_KEY=whsec_...
Never commit Stripe keys to version control. Use environment variables or secrets management.

Module Registration

Stripe is configured when registering the Order module:
Wolfix.API/Extensions/WebApplicationBuilderExtension.cs
private static WebApplicationBuilder AddOrderModule(
    this WebApplicationBuilder builder, 
    string connectionString)
{
    string publishableKey = builder.Configuration.GetOrThrow("STRIPE_PUBLISHABLE_KEY");
    string secretKey = builder.Configuration.GetOrThrow("STRIPE_SECRET_KEY");
    string webhookKey = builder.Configuration.GetOrThrow("STRIPE_WEBHOOK_KEY");
    
    builder.Services.AddOrderModule(
        connectionString, 
        publishableKey, 
        secretKey, 
        webhookKey
    );
    
    return builder;
}

Service Registration

The Order module registers Stripe services:
Order.Infrastructure/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddOrderModule(
    this IServiceCollection services,
    string connectionString,
    string publishableKey,
    string secretKey,
    string webhookKey)
{
    // Configure Stripe options
    services.Configure<StripeOptions>(options =>
    {
        options.PublishableKey = publishableKey;
        options.SecretKey = secretKey;
        options.WebhookKey = webhookKey;
    });
    
    // Register Stripe payment service
    services.AddScoped<IPaymentService<StripePaymentResponse>, StripePaymentService>();
    
    // Other registrations...
    
    return services;
}

Implementation

Payment Service

The StripePaymentService handles payment processing:
Order.Infrastructure/Services/StripePaymentService.cs
using Microsoft.Extensions.Options;
using Order.Application.Contracts;
using Order.Application.Models;
using Order.Infrastructure.Options;
using Shared.Domain.Models;
using Stripe;

namespace Order.Infrastructure.Services;

internal sealed class StripePaymentService : IPaymentService<StripePaymentResponse>
{
    private readonly StripeClient _stripeClient;
    
    public StripePaymentService(IOptions<StripeOptions> stripeOptions)
    {
        _stripeClient = new StripeClient(stripeOptions.Value.SecretKey);
    }
    
    public async Task<Result<StripePaymentResponse>> PayAsync(
        decimal amount, 
        string currency, 
        string customerEmail, 
        CancellationToken ct)
    {
        try
        {
            var options = new PaymentIntentCreateOptions
            {
                Amount = (long)(amount * 100), // Convert to cents
                Currency = currency,
                ReceiptEmail = customerEmail,
                PaymentMethodTypes = ["card"]
            };

            PaymentIntentService service = new(_stripeClient);
            PaymentIntent intent = await service.CreateAsync(options, cancellationToken: ct);

            return Result<StripePaymentResponse>.Success(new StripePaymentResponse
            {
                PaymentIntentId = intent.Id,
                ClientSecret = intent.ClientSecret
            });
        }
        catch (StripeException ex)
        {
            return Result<StripePaymentResponse>.Failure(
                $"Stripe error: {ex.Message}",
                HttpStatusCode.InternalServerError
            );
        }
    }
}

Options Model

Order.Infrastructure/Options/StripeOptions.cs
public class StripeOptions
{
    public string PublishableKey { get; set; } = string.Empty;
    public string SecretKey { get; set; } = string.Empty;
    public string WebhookKey { get; set; } = string.Empty;
}

Payment Response Model

Order.Application/Models/StripePaymentResponse.cs
public class StripePaymentResponse
{
    public string PaymentIntentId { get; init; } = string.Empty;
    public string ClientSecret { get; init; } = string.Empty;
}

Payment Flow

1

Customer Creates Order

Customer adds items to cart and initiates checkout:
Order.Application/Services/OrderService.cs
public async Task<Result<CreateOrderResponse>> CreateOrderAsync(
    CreateOrderDto dto, 
    Guid customerId, 
    CancellationToken ct)
{
    // 1. Create order
    Result<Order> createResult = Order.Create(
        customerId,
        dto.Items,
        dto.ShippingAddress
    );
    
    if (createResult.IsFailure)
        return Result<CreateOrderResponse>.Failure(createResult);
    
    Order order = createResult.Value!;
    
    // 2. Save order
    await _orderRepository.AddAsync(order, ct);
    await _orderRepository.SaveChangesAsync(ct);
    
    // 3. Create payment intent
    Result<StripePaymentResponse> paymentResult = await _paymentService.PayAsync(
        order.TotalAmount,
        "usd",
        dto.CustomerEmail,
        ct
    );
    
    if (paymentResult.IsFailure)
        return Result<CreateOrderResponse>.Failure(paymentResult);
    
    // 4. Return response with client secret
    return Result<CreateOrderResponse>.Success(new CreateOrderResponse
    {
        OrderId = order.Id,
        ClientSecret = paymentResult.Value!.ClientSecret,
        TotalAmount = order.TotalAmount
    });
}
2

Frontend Collects Payment

Frontend uses Stripe.js to collect payment details:
// Initialize Stripe
const stripe = Stripe('pk_test_...');

// Create order and get client secret
const response = await fetch('/api/orders', {
  method: 'POST',
  body: JSON.stringify(orderData)
});
const { clientSecret, orderId } = await response.json();

// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(
  clientSecret,
  {
    payment_method: {
      card: cardElement,
      billing_details: {
        email: '[email protected]'
      }
    }
  }
);

if (error) {
  console.error(error.message);
} else if (paymentIntent.status === 'succeeded') {
  console.log('Payment successful!');
  window.location.href = `/orders/${orderId}/confirmation`;
}
3

Stripe Sends Webhook

When payment succeeds, Stripe sends a webhook to your server:
Order.Endpoints/Endpoints/WebhookEndpoints.cs
public static IEndpointRouteBuilder MapWebhookEndpoints(
    this IEndpointRouteBuilder app)
{
    app.MapPost("/api/webhooks/stripe", HandleStripeWebhook)
        .WithTags("Webhooks")
        .AllowAnonymous();
    
    return app;
}

private static async Task<IResult> HandleStripeWebhook(
    HttpContext context,
    [FromServices] IOptions<StripeOptions> stripeOptions,
    [FromServices] OrderService orderService,
    CancellationToken ct)
{
    var json = await new StreamReader(context.Request.Body).ReadToEndAsync(ct);
    
    try
    {
        var stripeEvent = EventUtility.ConstructEvent(
            json,
            context.Request.Headers["Stripe-Signature"],
            stripeOptions.Value.WebhookKey
        );
        
        if (stripeEvent.Type == Events.PaymentIntentSucceeded)
        {
            var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
            await orderService.MarkOrderAsPaidAsync(
                paymentIntent!.Id, 
                ct
            );
        }
        
        return Results.Ok();
    }
    catch (StripeException)
    {
        return Results.BadRequest();
    }
}
4

Order Status Updated

The order service marks the order as paid:
Order.Application/Services/OrderService.cs
public async Task<VoidResult> MarkOrderAsPaidAsync(
    string paymentIntentId, 
    CancellationToken ct)
{
    // Find order by payment intent
    Order? order = await _orderRepository
        .GetByPaymentIntentIdAsync(paymentIntentId, ct);
    
    if (order == null)
        return VoidResult.Failure("Order not found", HttpStatusCode.NotFound);
    
    // Mark as paid
    VoidResult markPaidResult = order.MarkAsPaid();
    if (markPaidResult.IsFailure)
        return markPaidResult;
    
    // Save changes
    await _orderRepository.UpdateAsync(order, ct);
    await _orderRepository.SaveChangesAsync(ct);
    
    return VoidResult.Success();
}

Testing Payments

Test Card Numbers

Stripe provides test card numbers:
Card NumberBehavior
4242 4242 4242 4242Succeeds
4000 0000 0000 9995Declines (insufficient funds)
4000 0000 0000 9987Requires authentication (3D Secure)
4000 0025 0000 3155Succeeds with 3D Secure
Use any future expiry date, any CVC, and any postal code.

Testing Webhooks Locally

Use Stripe CLI to forward webhooks:
1

Install Stripe CLI

# macOS
brew install stripe/stripe-cli/stripe

# Windows
scoop install stripe

# Linux
wget https://github.com/stripe/stripe-cli/releases/download/v1.17.0/stripe_1.17.0_linux_x86_64.tar.gz
tar -xvf stripe_1.17.0_linux_x86_64.tar.gz
2

Login to Stripe

stripe login
3

Forward Webhooks

stripe listen --forward-to localhost:5000/api/webhooks/stripe
Copy the webhook signing secret:
Ready! Your webhook signing secret is whsec_... (^C to quit)
4

Update .env File

STRIPE_WEBHOOK_KEY=whsec_...
5

Trigger Test Events

stripe trigger payment_intent.succeeded

Refunds

Implement refund processing:
Order.Infrastructure/Services/StripePaymentService.cs
public async Task<VoidResult> RefundAsync(
    string paymentIntentId, 
    decimal amount, 
    CancellationToken ct)
{
    try
    {
        var options = new RefundCreateOptions
        {
            PaymentIntent = paymentIntentId,
            Amount = (long)(amount * 100)
        };
        
        RefundService service = new(_stripeClient);
        await service.CreateAsync(options, cancellationToken: ct);
        
        return VoidResult.Success();
    }
    catch (StripeException ex)
    {
        return VoidResult.Failure(
            $"Refund failed: {ex.Message}",
            HttpStatusCode.InternalServerError
        );
    }
}

Production Checklist

1

Switch to Live Keys

Update .env with live Stripe keys:
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
2

Configure Production Webhooks

  1. Go to Stripe Dashboard > Developers > Webhooks
  2. Add endpoint: https://yourdomain.com/api/webhooks/stripe
  3. Select events: payment_intent.succeeded, payment_intent.payment_failed
  4. Copy webhook signing secret
  5. Update STRIPE_WEBHOOK_KEY in production environment
3

Enable HTTPS

Stripe requires HTTPS for webhooks in production.
4

Test in Production

  1. Use real card numbers
  2. Monitor Stripe Dashboard for payments
  3. Check webhook delivery logs
5

Set Up Monitoring

Monitor failed payments and webhook errors:
catch (StripeException ex)
{
    _logger.LogError(ex, "Stripe payment failed");
    // Send alert
}

Best Practices

1

Always Use PaymentIntents

PaymentIntents are the recommended way to handle payments:
  • Support 3D Secure authentication
  • Handle multiple payment attempts
  • Better error handling
2

Verify Webhooks

Always verify webhook signatures:
var stripeEvent = EventUtility.ConstructEvent(
    json,
    signature,
    webhookSecret
);
3

Handle Idempotency

Stripe webhooks may be delivered multiple times. Make handlers idempotent:
public async Task<VoidResult> MarkOrderAsPaidAsync(string paymentIntentId)
{
    Order? order = await _repository.GetByPaymentIntentIdAsync(paymentIntentId);
    
    // Idempotent check
    if (order.Status == OrderStatus.Paid)
        return VoidResult.Success(); // Already processed
    
    order.MarkAsPaid();
    await _repository.SaveChangesAsync();
    return VoidResult.Success();
}
4

Log Everything

Log all payment operations for debugging:
_logger.LogInformation(
    "Payment intent created: {PaymentIntentId} for order {OrderId}",
    paymentIntent.Id,
    order.Id
);

Troubleshooting

Payment Intent Creation Fails

Issue: “Invalid API Key provided” Solution: Verify STRIPE_SECRET_KEY is set correctly and matches your environment (test/live).

Webhook Not Received

Issue: Payment succeeds but order not marked as paid. Solution:
  1. Check webhook endpoint is publicly accessible
  2. Verify webhook signature matches
  3. Check Stripe Dashboard > Developers > Webhooks for delivery status

3D Secure Not Working

Issue: Payment requires authentication but fails. Solution: Ensure frontend handles requires_action status:
if (paymentIntent.status === 'requires_action') {
  const { error } = await stripe.handleCardAction(clientSecret);
  // Handle error
}

Next Steps

Azure Storage

Integrate Azure Blob Storage

Google OAuth

Add Google authentication

Build docs developers (and LLMs) love