What are Webhooks?
Webhooks are HTTP callbacks that notify your application when events occur in external services. They enable real-time integrations without polling.
Creating a Webhook Endpoint
[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
private readonly ILogger<WebhookController> _logger;
public WebhookController(ILogger<WebhookController> logger)
{
_logger = logger;
}
[HttpPost("payment")]
public async Task<IActionResult> HandlePaymentWebhook()
{
// Read the raw body
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
_logger.LogInformation("Received webhook: {Body}", body);
// Parse and process
var payload = JsonSerializer.Deserialize<PaymentWebhook>(body);
// Process the webhook
await ProcessPaymentAsync(payload);
// Return 200 OK to acknowledge receipt
return Ok();
}
}
Validating Webhook Signatures
Always validate webhook signatures to ensure authenticity:
[HttpPost("stripe")]
public async Task<IActionResult> HandleStripeWebhook()
{
var json = await new StreamReader(Request.Body).ReadToEndAsync();
var signature = Request.Headers["Stripe-Signature"];
var webhookSecret = _configuration["Stripe:WebhookSecret"];
try
{
var stripeEvent = EventUtility.ConstructEvent(
json, signature, webhookSecret);
// Handle the event
switch (stripeEvent.Type)
{
case "payment_intent.succeeded":
var paymentIntent = stripeEvent.Data.Object as PaymentIntent;
await HandlePaymentSuccessAsync(paymentIntent);
break;
// Handle other events...
}
return Ok();
}
catch (StripeException e)
{
_logger.LogError(e, "Webhook signature validation failed");
return BadRequest();
}
}
HMAC Signature Validation (Generic)
private bool ValidateSignature(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var computedSignature = Convert.ToBase64String(hash);
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(computedSignature));
}
Webhook Best Practices
- Return 200 quickly - Process asynchronously if needed
- Implement idempotency - Handle duplicate deliveries
- Validate signatures - Never trust unvalidated webhooks
- Log everything - For debugging failed webhooks
- Use HTTPS - Always use secure endpoints
Async Processing with Background Service
// Queue webhook for background processing
[HttpPost("order")]
public async Task<IActionResult> HandleOrderWebhook([FromBody] OrderWebhook payload)
{
// Validate
if (!ValidateSignature(payload))
return Unauthorized();
// Queue for processing
await _webhookQueue.EnqueueAsync(payload);
return Ok();
}
// Background service processes the queue
public class WebhookProcessorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var webhook = await _queue.DequeueAsync(stoppingToken);
await ProcessWebhookAsync(webhook);
}
}
}
Testing Webhooks Locally
Use tools like ngrok to expose your local server:
ngrok http 5000
Then use the ngrok URL as your webhook endpoint during development.