WebhookKit 1.0.0

dotnet add package WebhookKit --version 1.0.0
                    
NuGet\Install-Package WebhookKit -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WebhookKit" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WebhookKit" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="WebhookKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add WebhookKit --version 1.0.0
                    
#r "nuget: WebhookKit, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package WebhookKit@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=WebhookKit&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=WebhookKit&version=1.0.0
                    
Install as a Cake Tool

WebhookKit

Send and receive signed webhooks with retry logic, delivery logging, and signature verification for .NET.

Inspired by spatie/laravel-webhook-server and spatie/laravel-webhook-client.


Table of Contents


Packages

NuGet Package Target Frameworks Description
WebhookKit net8.0, net9.0, net10.0 Core — signing, dispatcher, delivery entry, options
WebhookKit.AspNetCore net8.0, net10.0 Middleware and minimal API for receiving webhooks
WebhookKit.EntityFrameworkCore net8.0, net9.0, net10.0 EF Core delivery log entity configuration and store
WebhookKit.Dapper net8.0, net9.0, net10.0 Dapper delivery log queries and store

Requirements

  • .NET 8, .NET 9, or .NET 10
  • For EF Core storage: any EF Core 9+ relational provider (PostgreSQL, SQL Server, SQLite, etc.)
  • For Dapper storage: any IDbConnection implementation

Installation

Install the packages you need via the .NET CLI:

# Core + ASP.NET Core receiver
dotnet add package WebhookKit
dotnet add package WebhookKit.AspNetCore

# EF Core storage
dotnet add package WebhookKit.EntityFrameworkCore

# OR Dapper storage
dotnet add package WebhookKit.Dapper

Quick Start

// Program.cs
builder.Services
    .AddWebhookKit(options =>
    {
        options.Server.MaxRetries = 5;
        options.Server.RetryIntervals =
        [
            TimeSpan.FromSeconds(30),
            TimeSpan.FromMinutes(2),
            TimeSpan.FromMinutes(15),
            TimeSpan.FromHours(1),
            TimeSpan.FromHours(6),
        ];
    })
    .UseEntityFrameworkCore<AppDbContext>()
    .AddHandler<OrderCreatedWebhookHandler>()
    .UseRetryWorker();

// In AppDbContext.OnModelCreating:
modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration());

Sending Webhooks (Server)

Registration

builder.Services.AddWebhookKit(options =>
{
    options.Server.MaxRetries = 5;
    options.Server.HttpTimeout = TimeSpan.FromSeconds(30);
    options.Server.RetryPollingInterval = TimeSpan.FromSeconds(30);
    options.Server.RetryBatchSize = 50;
})
.UseEntityFrameworkCore<AppDbContext>();

Dispatching

Inject IWebhookDispatcher and call DispatchAsync:

public class OrderService(IWebhookDispatcher dispatcher)
{
    public async Task CreateOrderAsync(Order order)
    {
        // ... create order ...

        await dispatcher.DispatchAsync(
            eventType: "order.created",
            payload: new { order.Id, order.Total, order.Status },
            endpointUrl: "https://partner.example.com/webhooks",
            secret: "endpoint-specific-secret",
            scopeId: order.TenantId   // optional, for multi-tenant apps
        );
    }
}

DispatchAsync returns the delivery entry ID so you can track the delivery.

The dispatcher automatically:

  1. Creates a WebhookDeliveryEntry with Status = "pending"
  2. Signs the payload with HMAC-SHA256
  3. Sends an HTTP POST with the signature headers
  4. Updates the entry to "delivered" on HTTP 2xx, or "failed" with a scheduled NextRetryAt on failure

Retry Logic

On delivery failure (non-2xx or network error), the entry is updated to Status = "failed" with NextRetryAt set according to the retry schedule:

Attempt Default Delay
1 → 2 30 seconds
2 → 3 2 minutes
3 → 4 15 minutes
4 → 5 1 hour
5 → 6 6 hours

After MaxRetries failures the entry moves to Status = "exhausted" with no further retries.

Retry Worker and Secret Resolver

To automatically retry failed deliveries, enable the background worker and provide a secret resolver:

builder.Services
    .AddWebhookKit(...)
    .UseEntityFrameworkCore<AppDbContext>()
    .AddSecretResolver<MySecretResolver>()
    .UseRetryWorker();

Implement IWebhookEndpointSecretResolver to return the signing secret for a given endpoint URL:

public class MySecretResolver(IEndpointRepository repo) : IWebhookEndpointSecretResolver
{
    public async Task<string?> ResolveAsync(string endpointUrl, string? scopeId, CancellationToken ct)
    {
        var endpoint = await repo.FindByUrlAsync(endpointUrl, ct);
        return endpoint?.Secret;
    }
}

Receiving Webhooks (Client)

Registering Handlers

Implement IWebhookHandler for each event type you want to handle:

public class OrderCreatedWebhookHandler : IWebhookHandler
{
    public string EventType => "order.created";

    public async Task HandleAsync(WebhookPayload payload, CancellationToken ct)
    {
        var order = payload.Deserialize<OrderDto>();
        // process the order...
    }
}

// To handle all event types:
public class CatchAllHandler : IWebhookHandler
{
    public string EventType => "*";

    public Task HandleAsync(WebhookPayload payload, CancellationToken ct)
    {
        Console.WriteLine($"Received: {payload.EventType} / {payload.DeliveryId}");
        return Task.CompletedTask;
    }
}

Register via the builder:

builder.Services
    .AddWebhookKit(...)
    .AddHandler<OrderCreatedWebhookHandler>()
    .AddHandler<CatchAllHandler>();

Minimal API Endpoint

Add a typed webhook receiver endpoint using MapWebhookReceiver:

// Program.cs
app.MapWebhookReceiver("/webhooks/receive", secret: "your-endpoint-secret");

This endpoint will:

  1. Verify the HMAC-SHA256 signature
  2. Check idempotency via X-Webhook-Delivery-Id
  3. Dispatch to registered IWebhookHandler implementations

Middleware Approach

For more control, use the middleware:

app.UseWebhookReceiver("/webhooks");

Note: the middleware approach skips automatic signature verification — add your own verification logic before calling IWebhookHandler implementations if needed.

Signature Verification

Inject ISignatureVerifier to manually verify a signature:

public class WebhookController(ISignatureVerifier verifier) : ControllerBase
{
    [HttpPost("webhooks")]
    public async Task<IActionResult> Receive()
    {
        var body = await new StreamReader(Request.Body).ReadToEndAsync();
        var signature = Request.Headers["X-Webhook-Signature"].FirstOrDefault() ?? string.Empty;
        var timestamp = Request.Headers["X-Webhook-Timestamp"].FirstOrDefault() ?? string.Empty;

        if (!verifier.Verify(body, "your-secret", signature, timestamp))
            return Unauthorized();

        // process...
        return Ok();
    }
}

Idempotency

The minimal API endpoint and middleware automatically deduplicate incoming webhooks using the X-Webhook-Delivery-Id header. If a delivery ID has already been processed (found in IWebhookDeliveryStore), the request returns 200 OK immediately without re-processing.

WebhookPayload

WebhookPayload provides access to the raw body and parsed headers:

public Task HandleAsync(WebhookPayload payload, CancellationToken ct)
{
    // Typed deserialization
    var dto = payload.Deserialize<MyEventDto>();

    // Raw access
    var body = payload.RawBody;
    var deliveryId = payload.DeliveryId;
    var eventType = payload.EventType;

    // Case-insensitive header access
    var customHeader = payload.Headers["X-My-Header"];

    return Task.CompletedTask;
}

Storage Backends

Entity Framework Core

Add WebhookKit.EntityFrameworkCore and call UseEntityFrameworkCore<TContext>:

builder.Services
    .AddWebhookKit(...)
    .UseEntityFrameworkCore<AppDbContext>();

Apply the entity configuration in your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration());
    // or with a custom table name:
    modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration("outbox_webhooks"));
}

Generate migrations:

dotnet ef migrations add AddWebhookDeliveries
dotnet ef database update

Dapper

Add WebhookKit.Dapper, register an IDbConnection, and call UseDapper:

builder.Services.AddScoped<IDbConnection>(_ =>
    new NpgsqlConnection(connectionString));

builder.Services
    .AddWebhookKit(...)
    .UseDapper(options => options.TableName = "webhook_deliveries");

Create the table manually (example for PostgreSQL):

CREATE TABLE webhook_deliveries (
    id              VARCHAR(64)   NOT NULL PRIMARY KEY,
    endpoint_url    VARCHAR(2048) NOT NULL,
    event_type      VARCHAR(200)  NOT NULL,
    payload         TEXT          NOT NULL,
    attempt_number  INTEGER       NOT NULL DEFAULT 1,
    http_status     INTEGER,
    response_body   VARCHAR(1024),
    error_message   TEXT,
    status          VARCHAR(20)   NOT NULL DEFAULT 'pending',
    next_retry_at   TIMESTAMPTZ,
    delivered_at    TIMESTAMPTZ,
    created_at      TIMESTAMPTZ   NOT NULL,
    scope_id        VARCHAR(64)
);

CREATE INDEX ix_webhook_deliveries_status ON webhook_deliveries (status);
CREATE INDEX ix_webhook_deliveries_status_retry ON webhook_deliveries (status, next_retry_at);
CREATE INDEX ix_webhook_deliveries_scope_id ON webhook_deliveries (scope_id);
CREATE INDEX ix_webhook_deliveries_created_at ON webhook_deliveries (created_at DESC);

Delivery Log Entity

WebhookDeliveryEntry tracks every delivery attempt:

public class WebhookDeliveryEntry
{
    public string Id { get; set; }              // Guid("N") — also used as X-Webhook-Delivery-Id
    public string EndpointUrl { get; set; }
    public string EventType { get; set; }
    public string Payload { get; set; }         // JSON string
    public int AttemptNumber { get; set; }      // starts at 1
    public int? HttpStatus { get; set; }
    public string? ResponseBody { get; set; }   // truncated to 1 KB
    public string? ErrorMessage { get; set; }
    public string Status { get; set; }          // pending | delivered | failed | exhausted
    public DateTimeOffset? NextRetryAt { get; set; }
    public DateTimeOffset? DeliveredAt { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    public string? ScopeId { get; set; }
}

Status values are available as constants on WebhookDeliveryStatus:

WebhookDeliveryStatus.Pending    // "pending"
WebhookDeliveryStatus.Delivered  // "delivered"
WebhookDeliveryStatus.Failed     // "failed"
WebhookDeliveryStatus.Exhausted  // "exhausted"

Signing Utility

WebhookSigner is a static utility for HMAC-SHA256 signing and verification:

// Sign a payload
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var signature = WebhookSigner.Sign(payload, secret, timestamp);

// Verify an incoming signature (default: 5 minute replay window)
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp);

// Verify with a custom replay window
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp, maxAge: TimeSpan.FromMinutes(10));

// Disable replay protection entirely
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp, maxAge: null);

The signed string format is {timestamp}.{payload}, matching the pattern used by Stripe and similar webhook providers.

HTTP Headers

Outbound webhooks include:

Header Value
X-Webhook-Signature HMAC-SHA256 hex signature
X-Webhook-Timestamp Unix timestamp (seconds)
X-Webhook-Delivery-Id Unique delivery ID (Guid "N")
X-Webhook-Event Event type string

Configuration Reference

builder.Services.AddWebhookKit(options =>
{
    // Server (outbound) options
    options.Server.MaxRetries = 5;
    options.Server.RetryIntervals =
    [
        TimeSpan.FromSeconds(30),
        TimeSpan.FromMinutes(2),
        TimeSpan.FromMinutes(15),
        TimeSpan.FromHours(1),
        TimeSpan.FromHours(6),
    ];
    options.Server.HttpTimeout = TimeSpan.FromSeconds(30);
    options.Server.RetryPollingInterval = TimeSpan.FromSeconds(30);
    options.Server.RetryBatchSize = 50;
    options.Server.HttpClientName = "WebhookKit";  // named HttpClient

    // Client (inbound) options
    options.Client.MaxRequestAge = TimeSpan.FromMinutes(5);  // null = disable replay protection
});

Multi-Tenancy (ScopeId)

Pass a scopeId to associate a delivery with a tenant or scope:

await dispatcher.DispatchAsync(
    eventType: "order.created",
    payload: orderDto,
    endpointUrl: tenant.WebhookUrl,
    secret: tenant.WebhookSecret,
    scopeId: tenant.Id
);

The ScopeId is stored on WebhookDeliveryEntry and passed to IWebhookEndpointSecretResolver.ResolveAsync for multi-tenant secret resolution.


License

MIT — see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on WebhookKit:

Package Downloads
WebhookKit.AspNetCore

ASP.NET Core integration for WebhookKit — middleware and minimal API endpoint for receiving and processing signed webhooks.

WebhookKit.EntityFrameworkCore

Entity Framework Core integration for WebhookKit — delivery log entity configuration and IWebhookDeliveryStore implementation.

WebhookKit.Dapper

Dapper integration for WebhookKit — raw SQL delivery log queries and IWebhookDeliveryStore implementation.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 152 3/26/2026