Create a POST action in your PaymentController that reads the raw request body as a string, computes an HMAC SHA512 hash using your Paystack secret key, and compares it to the x-paystack-signature request header. Return HTTP 200 immediately and process the event asynchronously. Enable request body buffering to allow reading the body in ASP.NET Core.
// Controllers/WebhookController.cs
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
[IgnoreAntiforgeryToken] // Paystack cannot send CSRF tokens
public class WebhookController : ControllerBase
{
private readonly IConfiguration _config;
private readonly ILogger<WebhookController> _logger;
public WebhookController(IConfiguration config, ILogger<WebhookController> logger)
{
_config = config;
_logger = logger;
}
[HttpPost("paystack")]
public async Task<IActionResult> Paystack()
{
// Read raw body
Request.EnableBuffering();
string rawBody;
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true))
{
rawBody = await reader.ReadToEndAsync();
}
// Verify signature
var signature = Request.Headers["x-paystack-signature"].FirstOrDefault();
if (string.IsNullOrEmpty(signature))
return BadRequest("Missing signature");
var secretKey = _config["Paystack:SecretKey"]!;
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
var bodyBytes = Encoding.UTF8.GetBytes(rawBody);
using var hmac = new HMACSHA512(keyBytes);
var hashBytes = hmac.ComputeHash(bodyBytes);
var hash = Convert.ToHexString(hashBytes).ToLower();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(hash),
Encoding.UTF8.GetBytes(signature)))
{
_logger.LogWarning("Invalid Paystack webhook signature");
return BadRequest("Invalid signature");
}
var webhookEvent = JsonSerializer.Deserialize<JsonElement>(rawBody);
var eventType = webhookEvent.GetProperty("event").GetString();
var eventData = webhookEvent.GetProperty("data");
_logger.LogInformation("Paystack webhook received: {EventType}", eventType);
// Return 200 immediately
_ = Task.Run(() => ProcessEventAsync(eventType!, eventData));
return Ok(new { received = true });
}
private async Task ProcessEventAsync(string eventType, JsonElement data)
{
try
{
switch (eventType)
{
case "charge.success":
await HandleChargeSuccess(data);
break;
case "transfer.success":
await HandleTransferSuccess(data);
break;
case "transfer.failed":
_logger.LogWarning("Transfer failed: {Reference}", data.GetProperty("reference").GetString());
break;
default:
_logger.LogInformation("Unhandled event: {EventType}", eventType);
break;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing Paystack event {EventType}", eventType);
}
}
private Task HandleChargeSuccess(JsonElement data)
{
var reference = data.GetProperty("reference").GetString();
var amount = data.GetProperty("amount").GetInt64();
var currency = data.GetProperty("currency").GetString();
_logger.LogInformation("Payment confirmed: {Reference} {Amount} {Currency}",
reference, amount / 100m, currency);
// Update order in database
return Task.CompletedTask;
}
private Task HandleTransferSuccess(JsonElement data)
{
var reference = data.GetProperty("reference").GetString();
_logger.LogInformation("Transfer successful: {Reference}", reference);
return Task.CompletedTask;
}
}
ASP.NET Core reads the request body once. To read it as raw text for signature verification, call Request.EnableBuffering() before reading. This rewinds the stream so model binding can still work if needed.
For Minimal APIs, configure the endpoint to allow reading the body:
// Program.cs (Minimal API approach)
app.MapPost("/api/webhooks/paystack", async (HttpContext context, IConfiguration config) =>
{
context.Request.EnableBuffering();
string rawBody;
using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true))
{
rawBody = await reader.ReadToEndAsync();
}
var signature = context.Request.Headers["x-paystack-signature"].FirstOrDefault();
var secretKey = config["Paystack:SecretKey"]!;
using var hmac = new HMACSHA512(Encoding.UTF8.GetBytes(secretKey));
var hash = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody))).ToLower();
if (signature != hash)
return Results.BadRequest("Invalid signature");
var evt = JsonSerializer.Deserialize<JsonElement>(rawBody);
// Process event...
return Results.Ok(new { received = true });
}).DisableAntiforgery();
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
This guide is part of the Paystack integration guides by language and framework series.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course