C# – Webhooks

By | 18/02/2026

In this post, we will see what Webhooks are and how we can use them in our .NET projects.
Based on the post I created for SignalR, it is important to distinguish between different types of real-time communication. While SignalR handles complex, persistent connections, Webhooks solve a different problem entirely.
A Webhook is not a library or a framework; it is an architectural pattern.
Conceptually, it is often described as a “user-defined HTTP callback.” In a standard API interaction, we (the client) ask the server for data. In the Webhook pattern, we invert this relationship. We provide a URL to an external system, and that system makes an HTTP POST request to us when something happens.

When to use Webhooks?
We should reach for a webhook when we need “service to service” notifications and we don’t want (or can’t have) a long-lived connection.
Typical scenarios are integrations with third-party platforms (Stripe, GitHub, Shopify, etc.), internal microservices that need to react to domain events, CI/CD triggers, notifications to downstream systems, “fire and forget” fan out where the sender just calls our endpoint and moves on.
A strong signal that a webhook is the right tool is when the sender and receiver are not tightly coupled. The sender doesn’t care if we are online “right now”, it just delivers an HTTP request and expects us to handle retries/idempotency on our side.

Differences with SignalR
SignalR and webhooks are both “push”, but they solve different problems.
SignalR is built for “low latency, bidirectional communication” between a server and connected clients (often browsers). It keeps a persistent connection and negotiates transports like WebSockets (with fallbacks like long polling).
A webhook is “one way”: the sender calls our HTTP endpoint when an event happens. There’s no persistent connection to keep alive, no session, no real-time stream to the UI. It’s simply HTTP request/response, usually from backend to backend.
A practical way to choose is this: if our requirement is “update many connected users instantly and keep interacting”, think SignalR. If our requirement is “tell another system that something happened and we don’t control their connectivity/runtime”, think webhooks.

Security and Webhooks
Since a Webhook is just a public HTTP endpoint, anyone can send a POST request to it.
How do we ensure the request actually came from our Payment Provider and not a hacker trying to fake a payment?
The industry standard is HMAC (Hash-based Message Authentication Code).

  • We share a Secret Key with the provider.
  • The provider hashes the payload using this key and sends the hash in a header (e.g., X-Signature).
  • We “re-hash” the received payload with the same key.
  • If the hashes match, the request is authentic.


Let’s build a secure Webhook receiver.
We will create a helper class to validate the signature and a Minimal API endpoint.

[WEBHOOKVERIFIER.CS]

using System.Security.Cryptography;
using System.Text;

namespace Webhoock;

public static class WebhookVerifier
{
    // In production, load this from IConfiguration/KeyVault
    private const string Secret = "test_key_123";

    public static async Task<bool> IsValid(HttpRequest request)
    {
        // Extract signature from header (sent by webhook provider)
        if (!request.Headers.TryGetValue("X-Signature", out var signature))
            return false;

        // Read request body (EnableBuffering allows reading the stream multiple times)
        request.EnableBuffering();
        using var reader = new StreamReader(request.Body, leaveOpen: true);
        var body = await reader.ReadToEndAsync();
        request.Body.Position = 0; // Reset so the endpoint can read it again

        // Compute HMAC SHA256 hash of the body using our secret
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body));
        var computedSignature = Convert.ToHexStringLower(hash);

        // Compare signatures using constant-time comparison (prevents timing attacks)
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(computedSignature),
            Encoding.UTF8.GetBytes(signature!));
    }
}

[PAYMENTEVENT.CS]

namespace Webhoock;

public record PaymentEvent(string TransactionId, decimal Amount, string Status, DateTime Timestamp);

[PROGRAM.CS]

using Webhoock;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/webhooks/payment", async (HttpContext context) =>
{
    // 1. Security Check
    if (!await WebhookVerifier.IsValid(context.Request))
    {
        Console.WriteLine("[Security Alert] Invalid Signature!");
        return Results.Unauthorized();
    }

    // 2. Deserialize the payloadı
    var payload = await context.Request.ReadFromJsonAsync<PaymentEvent>();
    
    // 3. Process Business Logic
    Console.WriteLine($"[Verified] Payment {payload?.TransactionId} received.");

    return Results.Ok();
});

app.Run();


Now, in order to test it, we’ll create a console application to send a request to the Webhook:

using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

var client = new HttpClient();
const string secret = "test_key_123"; // Shared secret (same one used by the receiving endpoint)

// Create the webhook payload (the data we want to send)
var payload = new { TransactionId = "TX-100", Amount = 50 };
var json = JsonSerializer.Serialize(payload);

// Generate HMAC SHA256 signature of the payload
// This proves we know the secret and the data hasn't been tampered with
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(json));
var signature = Convert.ToHexStringLower(hashBytes); // Convert to lowercase hex string

// Prepare HTTP request with the JSON body
var content = new StringContent(json, Encoding.UTF8, "application/json");
content.Headers.Add("X-Signature", signature); // Add signature to header

// Send the webhook POST request
await client.PostAsync("http://localhost:5201/webhooks/payment", content);


If we run the applications, and the X-Signature is correct, this will be the result:

Instead, if the X-Signature is not correct, we will have this result:


Webhooks are one of the simplest and most effective ways to integrate systems.
Even if the implementation looks like a normal HTTP endpoint, the real difference is in the mindset: a webhook is not something we call manually, but something that is triggered automatically by another system when an event occurs.
Compared to SignalR, webhooks are much lighter because they don’t require persistent connections, client management, or real-time sessions. They are simply an HTTP notification mechanism and for this reason they are widely used in modern architectures, especially in microservices and third-party integrations.
If our goal is to build real-time communication with connected users, SignalR is the right choice.
But if we just need your application to react to external events in a clean and scalable way, webhooks are often the easiest and most reliable solution.



Leave a Reply

Your email address will not be published. Required fields are marked *