McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Handle Paystack Webhooks in ASP.NET Core
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Handle Paystack Webhooks in ASP.NET Core

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.

In this article

  1. 1. Webhook Controller
  2. 2. Enable Raw Body Reading
  3. 3. Learn More

Webhook Controller

// 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;
    }
}

Enable Raw Body Reading

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.

Learn More

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Read the raw request body with StreamReader before any model binding. Model binding will read and discard the stream.
  • ✓Compute HMAC SHA512 using System.Security.Cryptography.HMACSHA512 with your secret key as UTF8 bytes.
  • ✓The x-paystack-signature header contains the hex-encoded HMAC. Compare strings in constant time to prevent timing attacks.
  • ✓Return HTTP 200 immediately. Use a background task or queue for event processing to avoid timeout.
  • ✓Disable anti-forgery validation on the webhook action — Paystack cannot send CSRF tokens.
  • ✓Store processed event references in your database to prevent duplicate processing on retries.

Frequently Asked Questions

Why does my Paystack webhook signature verification fail in ASP.NET Core?
The most common cause is reading the request body after model binding has already consumed it. Use Request.EnableBuffering() and read with StreamReader before any other body access. The second common cause is using a different encoding for the body bytes. Always use UTF8.
Do I need to disable CSRF protection for the Paystack webhook endpoint?
Yes. Add [IgnoreAntiforgeryToken] to your controller or action. Paystack sends a raw POST request without CSRF tokens. For Minimal APIs, call .DisableAntiforgery() on the endpoint.
Should I process webhook events synchronously or asynchronously?
Return 200 immediately and process asynchronously. Paystack has a timeout after which it will retry if it does not receive a 200. Heavy database operations should be queued (BullMQ equivalent: use IBackgroundTaskQueue or Hangfire in .NET).
How do I prevent double-processing of Paystack webhook events?
Store the transaction reference in a processed_webhooks table when you first handle an event. Before processing, check if the reference already exists. If it does, return 200 without processing again.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in ASP.NET Core

Wire Paystack payments into an ASP.NET Core web application. This guide covers configuring HttpClient for the Paystack API, creating payment controllers, initializing transactions, handling redirect callbacks, and verifying payments server-side.

Read more

Verify Paystack Payments in ASP.NET Core

Verify Paystack payments in ASP.NET Core by calling the transaction verify endpoint with your secret key. This guide covers the verify action, parsing the response, validating the amount, and building a reusable PaystackService.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Idempotent Paystack Webhook Handlers: The Complete Pattern

A complete guide to building idempotent Paystack webhook handlers. Covers why Paystack sends duplicate events, using transaction references as idempotency keys, database-level deduplication with a processed_events table, and the full production-ready pattern in Node.js.

Read more

Why You Must Return 200 Immediately from a Paystack Webhook

The number one Paystack webhook reliability rule: return 200 before doing anything else. This guide explains why Paystack has a timeout, what happens when you break the rule, and how to implement the respond-first-then-queue pattern in Node.js.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.