RepletoryLib.Webhook.Abstractions
Webhook abstractions and interfaces for inbound receiving, outbound delivery, HMAC signature validation, and subscription management.
Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

Overview
RepletoryLib.Webhook.Abstractions defines the core contracts, models, enums, and options for both outbound webhook delivery and inbound webhook reception. It provides IWebhookSender for delivering payloads to subscriber endpoints, IWebhookSubscriptionManager for managing subscriptions, IWebhookReceiver for processing incoming webhooks, IWebhookEventHandler<TPayload> for handling specific event types, and IWebhookSignatureValidator for HMAC signature generation and validation.
This package contains no implementations -- it is the abstraction layer that concrete providers implement. Reference this package in your application code to program against interfaces, then swap implementations without changing business logic.
Key Features
IWebhookSender -- Outbound webhook delivery: single send, bulk send, and retry operations
IWebhookSubscriptionManager -- Full subscription lifecycle: create, remove, retrieve by ID or event type, list all, and activate/deactivate
IWebhookReceiver -- Inbound webhook processing: signature validation and event dispatching
IWebhookEventHandler<TPayload> -- Strongly-typed event handler contract for processing specific webhook event types
IWebhookSignatureValidator -- Signature generation and validation for payload integrity and authenticity
WebhookPayload / WebhookDeliveryRequest -- Outbound models with event type, data, metadata, secrets, and custom headers
WebhookDeliveryResult / BulkDeliveryResult -- Delivery outcome models with per-delivery success/failure tracking
WebhookSubscription -- Subscription model with URL, event type, secret, active state, and metadata
IncomingWebhook / WebhookValidationResult -- Inbound models with body, signature, headers, source IP, and validation outcomes
WebhookDeliveryStatus enum -- Delivery state tracking: Pending, Delivered, Failed, Retrying
WebhookEventType constants -- Well-known event types including wildcard (*) and ping
WebhookOptions -- Shared configuration for secrets, headers, retry counts, delays, and timeouts
Installation
dotnet add package RepletoryLib.Webhook.Abstractions
Or add to your .csproj:
<PackageReference Include="RepletoryLib.Webhook.Abstractions" Version="1.0.0" />
Note: RepletoryLib packages are published to a local BaGet feed.
See the main repository README for feed configuration.
Dependencies
| Package |
Type |
RepletoryLib.Common |
RepletoryLib |
Quick Start
1. Reference the abstractions in your service layer
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class OrderWebhookService
{
private readonly IWebhookSender _sender;
private readonly IWebhookSubscriptionManager _subscriptions;
public OrderWebhookService(IWebhookSender sender, IWebhookSubscriptionManager subscriptions)
{
_sender = sender;
_subscriptions = subscriptions;
}
public async Task NotifyOrderCreatedAsync(string orderId, string orderJson)
{
var subscribers = await _subscriptions.GetByEventTypeAsync("order.created");
foreach (var sub in subscribers.Where(s => s.IsActive))
{
var request = new WebhookDeliveryRequest
{
Url = sub.Url,
Secret = sub.Secret,
Payload = new WebhookPayload
{
EventType = "order.created",
Data = orderJson
}
};
await _sender.SendAsync(request);
}
}
}
2. Register a concrete provider in Program.cs
using RepletoryLib.Webhook.Core;
builder.Services.AddRepletoryWebhook(builder.Configuration);
Configuration
WebhookOptions
| Property |
Type |
Default |
Description |
SecretKey |
string |
"" |
Secret key used to sign and validate webhook payloads |
SignatureHeaderName |
string |
"X-Webhook-Signature" |
HTTP header name for transmitting the webhook signature |
EventTypeHeaderName |
string |
"X-Webhook-Event" |
HTTP header name for transmitting the webhook event type |
RetryCount |
int |
3 |
Maximum number of retry attempts for failed deliveries |
RetryDelaySeconds |
int |
5 |
Delay in seconds between retry attempts |
TimeoutSeconds |
int |
30 |
Timeout in seconds for each webhook delivery HTTP request |
Section name: "Webhook"
{
"Webhook": {
"SecretKey": "your-shared-secret-key",
"SignatureHeaderName": "X-Webhook-Signature",
"EventTypeHeaderName": "X-Webhook-Event",
"RetryCount": 3,
"RetryDelaySeconds": 5,
"TimeoutSeconds": 30
}
}
Usage Examples
Sending Outbound Webhooks
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class PaymentWebhookService
{
private readonly IWebhookSender _sender;
public PaymentWebhookService(IWebhookSender sender) => _sender = sender;
public async Task<WebhookDeliveryResult> NotifyPaymentReceivedAsync(
string endpointUrl, string secret, string paymentJson)
{
var request = new WebhookDeliveryRequest
{
Url = endpointUrl,
Secret = secret,
Payload = new WebhookPayload
{
EventType = "payment.received",
Data = paymentJson,
Metadata = new Dictionary<string, string>
{
["source"] = "payment-service"
}
},
Headers = new Dictionary<string, string>
{
["X-Correlation-Id"] = Guid.NewGuid().ToString()
}
};
return await _sender.SendAsync(request);
}
}
Bulk Delivery
public async Task<BulkDeliveryResult> BroadcastEventAsync(
string eventType,
string data,
IEnumerable<WebhookSubscription> subscribers)
{
var requests = subscribers.Where(s => s.IsActive).Select(sub => new WebhookDeliveryRequest
{
Url = sub.Url,
Secret = sub.Secret,
Payload = new WebhookPayload
{
EventType = eventType,
Data = data
}
});
var result = await _sender.SendBulkAsync(requests);
// Partial success -- check individual results
foreach (var r in result.Results.Where(r => !r.Success))
{
_logger.LogWarning("Delivery to {Url} failed: {Error}", r.Url, r.Error);
}
_logger.LogInformation("Bulk delivery: {Success}/{Total} succeeded",
result.SuccessCount, result.Results.Count);
return result;
}
Retrying Failed Deliveries
public async Task<WebhookDeliveryResult> RetryFailedDeliveryAsync(
WebhookDeliveryRequest originalRequest)
{
originalRequest.AttemptNumber++;
return await _sender.RetryAsync(originalRequest);
}
Managing Subscriptions
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class SubscriptionService
{
private readonly IWebhookSubscriptionManager _manager;
public SubscriptionService(IWebhookSubscriptionManager manager) => _manager = manager;
// Create a subscription
public async Task<WebhookSubscription> RegisterAsync(string url, string eventType, string secret)
{
return await _manager.SubscribeAsync(new WebhookSubscription
{
Url = url,
EventType = eventType,
Secret = secret,
Metadata = new Dictionary<string, string>
{
["registered-by"] = "subscription-api"
}
});
}
// Subscribe to all events using wildcard
public async Task<WebhookSubscription> RegisterForAllEventsAsync(string url, string secret)
{
return await _manager.SubscribeAsync(new WebhookSubscription
{
Url = url,
EventType = "*",
Secret = secret
});
}
// Get all subscribers for a specific event type
public async Task<IReadOnlyList<WebhookSubscription>> GetSubscribersAsync(string eventType)
{
return await _manager.GetByEventTypeAsync(eventType);
}
// Deactivate a subscription without deleting it
public async Task DeactivateAsync(Guid subscriptionId)
{
await _manager.SetActiveAsync(subscriptionId, isActive: false);
}
// Remove a subscription
public async Task RemoveAsync(Guid subscriptionId)
{
await _manager.UnsubscribeAsync(subscriptionId);
}
}
Receiving Inbound Webhooks
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Inbound;
public class ExternalWebhookService
{
private readonly IWebhookReceiver _receiver;
public ExternalWebhookService(IWebhookReceiver receiver) => _receiver = receiver;
public async Task<WebhookValidationResult> HandleIncomingAsync(
string body, string? signature, string eventType, Dictionary<string, string> headers)
{
var webhook = new IncomingWebhook
{
Body = body,
Signature = signature,
EventType = eventType,
Headers = headers
};
var result = await _receiver.ProcessAsync(webhook);
if (!result.Success)
{
if (!result.SignatureValid)
Console.WriteLine("Rejected webhook: invalid signature");
else
Console.WriteLine($"Webhook processing failed: {result.Error}");
}
return result;
}
}
Implementing a Custom Event Handler
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Inbound;
public class OrderCreatedHandler : IWebhookEventHandler<string>
{
public string EventType => "order.created";
public async Task HandleAsync(string payload, IncomingWebhook webhook, CancellationToken ct = default)
{
// Deserialize and process the payload
var order = JsonSerializer.Deserialize<OrderDto>(payload);
Console.WriteLine($"Order {order?.Id} received at {webhook.ReceivedAt} from {webhook.SourceIp}");
await Task.CompletedTask;
}
}
// Register in DI
builder.Services.AddScoped<IWebhookEventHandler<string>, OrderCreatedHandler>();
Implementing a Wildcard Event Handler
public class AuditLogHandler : IWebhookEventHandler<string>
{
public string EventType => "*";
public async Task HandleAsync(string payload, IncomingWebhook webhook, CancellationToken ct = default)
{
Console.WriteLine(
"Webhook received: EventType={0}, SourceIp={1}, ReceivedAt={2}",
webhook.EventType, webhook.SourceIp, webhook.ReceivedAt);
await _auditRepository.LogAsync(webhook.EventType, payload, webhook.ReceivedAt);
}
}
Generating and Validating Signatures
using RepletoryLib.Webhook.Abstractions.Interfaces;
public class SignatureService
{
private readonly IWebhookSignatureValidator _validator;
public SignatureService(IWebhookSignatureValidator validator) => _validator = validator;
public string SignPayload(string payload, string secret)
{
return _validator.GenerateSignature(payload, secret);
}
public bool VerifyPayload(string payload, string signature, string secret)
{
return _validator.ValidateSignature(payload, signature, secret);
}
}
Working with WebhookDeliveryResult
using RepletoryLib.Webhook.Abstractions.Enums;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
// Factory methods
var success = WebhookDeliveryResult.Succeeded(
url: "https://example.com/webhooks",
webhookId: Guid.NewGuid(),
httpStatusCode: 200,
attemptNumber: 1);
var failure = WebhookDeliveryResult.Failed(
url: "https://example.com/webhooks",
webhookId: Guid.NewGuid(),
error: "Connection refused",
attemptNumber: 3,
httpStatusCode: null);
// Inspect results
if (success.Success)
Console.WriteLine($"Delivered to {success.Url} on attempt {success.AttemptNumber}");
if (!failure.Success)
Console.WriteLine($"Failed ({failure.Status}): {failure.Error}");
// BulkDeliveryResult aggregation
var bulk = new BulkDeliveryResult { Results = [success, failure] };
Console.WriteLine($"Total: {bulk.Results.Count}, OK: {bulk.SuccessCount}, Failed: {bulk.FailureCount}");
Working with WebhookValidationResult
using RepletoryLib.Webhook.Abstractions.Models.Inbound;
// Factory methods
var valid = WebhookValidationResult.Succeeded("order.created");
var invalid = WebhookValidationResult.Failed("Invalid signature", signatureValid: false);
var handlerError = WebhookValidationResult.Failed("Handler threw exception", signatureValid: true);
// Inspect results
if (valid.Success)
Console.WriteLine($"Processed event: {valid.EventType}");
if (!invalid.Success && !invalid.SignatureValid)
Console.WriteLine($"Signature rejected: {invalid.Error}");
API Reference
Interfaces
IWebhookSender
| Method |
Returns |
Description |
SendAsync(request, ct) |
Task<WebhookDeliveryResult> |
Sends a single webhook delivery request to the target endpoint |
SendBulkAsync(requests, ct) |
Task<BulkDeliveryResult> |
Sends multiple webhook delivery requests in bulk |
RetryAsync(request, ct) |
Task<WebhookDeliveryResult> |
Retries a previously failed webhook delivery request |
IWebhookSubscriptionManager
| Method |
Returns |
Description |
SubscribeAsync(subscription, ct) |
Task<WebhookSubscription> |
Creates or registers a new webhook subscription |
UnsubscribeAsync(subscriptionId, ct) |
Task |
Removes an existing webhook subscription |
GetByIdAsync(subscriptionId, ct) |
Task<WebhookSubscription?> |
Retrieves a subscription by its unique identifier |
GetByEventTypeAsync(eventType, ct) |
Task<IReadOnlyList<WebhookSubscription>> |
Retrieves all subscriptions matching the specified event type |
GetAllAsync(ct) |
Task<IReadOnlyList<WebhookSubscription>> |
Retrieves all registered webhook subscriptions |
SetActiveAsync(subscriptionId, isActive, ct) |
Task |
Sets the active state of a webhook subscription |
IWebhookReceiver
| Method |
Returns |
Description |
ProcessAsync(webhook, ct) |
Task<WebhookValidationResult> |
Processes an incoming webhook by validating its signature and dispatching the event |
IWebhookEventHandler<TPayload>
| Member |
Type |
Description |
EventType |
string (property) |
The event type this handler processes |
HandleAsync(payload, webhook, ct) |
Task |
Handles the deserialized webhook payload |
IWebhookSignatureValidator
| Method |
Returns |
Description |
GenerateSignature(payload, secret) |
string |
Generates a cryptographic signature for the payload |
ValidateSignature(payload, signature, secret) |
bool |
Validates a signature against the expected value |
Outbound Models
WebhookPayload
| Property |
Type |
Default |
Description |
Id |
Guid |
Guid.NewGuid() |
Unique identifier for this webhook payload |
EventType |
string |
(required) |
The type of event this payload represents |
Data |
string |
(required) |
The serialized event data |
Timestamp |
DateTime |
DateTime.UtcNow |
UTC timestamp when the payload was created |
Metadata |
Dictionary<string, string>? |
null |
Optional metadata key-value pairs |
WebhookDeliveryRequest
| Property |
Type |
Default |
Description |
Url |
string |
(required) |
Target URL for webhook delivery |
Payload |
WebhookPayload |
(required) |
The webhook payload to deliver |
Secret |
string? |
null |
Secret for signature generation; no signature when null |
Headers |
Dictionary<string, string>? |
null |
Optional HTTP headers to include in the delivery |
AttemptNumber |
int |
1 |
Current delivery attempt number |
WebhookDeliveryResult
| Property |
Type |
Description |
Success |
bool |
Whether the delivery was successful |
Status |
WebhookDeliveryStatus |
The delivery status |
HttpStatusCode |
int? |
HTTP status code from the target; null if no response received |
Error |
string? |
Error message for failed deliveries |
Url |
string |
Target URL the webhook was delivered to |
WebhookId |
Guid |
Unique identifier of the delivered webhook |
AttemptNumber |
int |
Attempt number for this delivery result |
AttemptedAt |
DateTime |
UTC timestamp of the delivery attempt |
| Static Method |
Returns |
Description |
Succeeded(url, webhookId, httpStatusCode, attemptNumber) |
WebhookDeliveryResult |
Creates a successful delivery result |
Failed(url, webhookId, error, attemptNumber, httpStatusCode?) |
WebhookDeliveryResult |
Creates a failed delivery result |
BulkDeliveryResult
| Property |
Type |
Description |
Results |
List<WebhookDeliveryResult> |
Individual delivery results for each webhook |
SuccessCount |
int |
Number of successfully delivered webhooks (computed) |
FailureCount |
int |
Number of failed deliveries (computed) |
WebhookSubscription
| Property |
Type |
Default |
Description |
Id |
Guid |
Guid.NewGuid() |
Unique identifier for this subscription |
Url |
string |
(required) |
Target URL for webhook deliveries |
EventType |
string |
(required) |
Event type to listen for; use "*" for all events |
Secret |
string? |
null |
Secret for signing payloads; no signature when null |
IsActive |
bool |
true |
Whether this subscription receives deliveries |
CreatedAt |
DateTime |
DateTime.UtcNow |
UTC creation timestamp |
Metadata |
Dictionary<string, string>? |
null |
Optional metadata key-value pairs |
Inbound Models
IncomingWebhook
| Property |
Type |
Default |
Description |
EventType |
string |
(required) |
The event type of the incoming webhook |
Body |
string |
(required) |
The raw body content of the webhook request |
Signature |
string? |
null |
The signature for verification; null when not provided |
Headers |
Dictionary<string, string> |
new() |
HTTP headers received with the webhook request |
ReceivedAt |
DateTime |
DateTime.UtcNow |
UTC timestamp when the webhook was received |
SourceIp |
string? |
null |
Source IP address of the webhook sender |
WebhookValidationResult
| Property |
Type |
Description |
Success |
bool |
Whether the webhook was processed successfully |
SignatureValid |
bool |
Whether the webhook signature was valid |
Error |
string? |
Error message when validation or processing fails |
EventType |
string? |
Event type extracted from the incoming webhook |
| Static Method |
Returns |
Description |
Succeeded(eventType) |
WebhookValidationResult |
Creates a successful validation result |
Failed(error, signatureValid?) |
WebhookValidationResult |
Creates a failed validation result |
Enums
WebhookDeliveryStatus
| Value |
Description |
Pending |
Delivery is queued and has not yet been attempted |
Delivered |
Webhook was delivered successfully to the target endpoint |
Failed |
Delivery failed and no further retries will be attempted |
Retrying |
Delivery failed but is scheduled for a retry attempt |
WebhookEventType (static constants)
| Constant |
Value |
Description |
All |
"*" |
Wildcard event type that matches all events |
Ping |
"ping" |
Lightweight event type for verifying webhook connectivity |
Integration with Other RepletoryLib Packages
| Package |
Relationship |
RepletoryLib.Common |
Direct dependency -- shared base types |
RepletoryLib.Webhook.Core |
Default implementation of all webhook abstractions with HTTP sender, HMAC validation, in-memory subscriptions, and ASP.NET Core middleware |
Testing
using NSubstitute;
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class OrderWebhookServiceTests
{
[Fact]
public async Task NotifyOrderCreatedAsync_sends_to_active_subscribers()
{
// Arrange
var sender = Substitute.For<IWebhookSender>();
sender.SendAsync(Arg.Any<WebhookDeliveryRequest>(), Arg.Any<CancellationToken>())
.Returns(WebhookDeliveryResult.Succeeded(
"https://example.com/webhooks",
Guid.NewGuid(),
200,
1));
var manager = Substitute.For<IWebhookSubscriptionManager>();
manager.GetByEventTypeAsync("order.created", Arg.Any<CancellationToken>())
.Returns(new List<WebhookSubscription>
{
new() { Url = "https://example.com/webhooks", EventType = "order.created", IsActive = true }
});
var service = new OrderWebhookService(sender, manager);
// Act
await service.NotifyOrderCreatedAsync("order-123", "{\"id\":\"order-123\"}");
// Assert
await sender.Received(1).SendAsync(
Arg.Is<WebhookDeliveryRequest>(r =>
r.Url == "https://example.com/webhooks" &&
r.Payload.EventType == "order.created"),
Arg.Any<CancellationToken>());
}
}
Troubleshooting
| Issue |
Solution |
WebhookDeliveryResult.Success is false |
Check the Error and HttpStatusCode properties for the failure reason; common causes are connection errors or non-2xx responses |
| Subscription not receiving events |
Verify that IsActive is true and that EventType matches the published event type (or use "*" for all events) |
| Signature validation fails |
Ensure the SecretKey in WebhookOptions matches between sender and receiver; check for whitespace or encoding differences |
WebhookValidationResult.SignatureValid is false |
The signature is missing or does not match; verify the signing secret and header name configuration |
| No handlers invoked for incoming webhook |
Ensure IWebhookEventHandler<string> implementations are registered in DI and their EventType matches the incoming event |
License
This project is licensed under the MIT License.
Copyright (c) 2024-2026 Repletory.
For complete documentation, infrastructure setup, and configuration reference,
see the RepletoryLib main repository.