Skip to main content

Webhook Channel

The Hermodr.Publisher.Webhook package delivers CloudEvent instances over HTTP to a configured endpoint URL, with optional HMAC request signing, exponential-backoff retries, and pluggable serialisers.

Installation

dotnet add package Hermodr.Publisher.Webhook

Registration

Inline configuration

using Hermodr;

builder.Services
.AddEventPublisher()
.AddWebhooks(options =>
{
options.EndpointUrl = "https://partner.example.com/events";
options.SigningSecret = "s3cr3t";
options.SignatureAlgorithm = WebhookSignatureAlgorithm.HmacSha256;
options.MaxRetryCount = 3;
});

From appsettings.json

builder.Services
.AddEventPublisher()
.AddWebhooks("Events:Webhook");
// appsettings.json
{
"Events": {
"Webhook": {
"EndpointUrl": "https://partner.example.com/events",
"SigningSecret": "s3cr3t",
"SignatureAlgorithm": "HmacSha256",
"MaxRetryCount": 3,
"RetryDelay": "00:00:01",
"RetryBackoffMultiplier": 2.0,
"RequestTimeout": "00:00:30"
}
}
}

Options reference

WebhookPublishOptions

Delivery settings (nullable — null in a per-call override inherits the channel default):

PropertyTypeEffective defaultDescription
EndpointUrlstring?(required)URL of the webhook endpoint
SigningSecretstring?nullShared secret for HMAC signing; no signature header is sent when omitted
SignatureAlgorithmWebhookSignatureAlgorithm?HmacSha256HMAC algorithm used to sign the body
MessageFormatstring?"json"Serialisation format. Use EventMessageFormat constants (for built-ins: "json", "xml", "cloudevents+json", "cloudevents+xml") or a custom serializer format key
MaxRetryCountint?3Maximum delivery attempts; 0 disables retries
RetryDelayTimeSpan?1 sInitial delay between retries
RetryBackoffMultiplierdouble?2.0Multiplier for exponential backoff
RequestTimeoutTimeSpan?30 sTimeout per individual HTTP request
AdditionalHeadersIDictionary<string, string>{}Extra HTTP headers merged into every request; per-call entries win on key collision

Channel-structural settings (always taken from the channel-level defaults; ignored in per-call overrides):

PropertyTypeDefaultDescription
SignatureHeaderNamestringX-Webhook-SignatureHTTP header carrying the computed signature
SignatureAlgorithmHeaderNamestring?X-Webhook-Signature-AlgorithmHeader advertising the algorithm used; set to null to suppress
DeliveryIdHeaderNamestringX-Webhook-DeliveryHeader carrying a unique delivery identifier
EventTypeHeaderNamestringX-Webhook-EventHeader carrying the event type
TimestampHeaderNamestringX-Webhook-TimestampUnix-epoch timestamp header (used in signature payload to prevent replay attacks)
RetryableStatusCodesISet<int>429, 500, 502, 503, 504HTTP status codes that trigger a retry
HttpClientNamestring?nullNamed HttpClient resolved from IHttpClientFactory; defaults to the internal channel name

TimestampHeaderName carries the Unix timestamp used in signature computation. The channel uses CloudEvent.time when present; otherwise it falls back to IEventSystemTime.UtcNow. This keeps signatures deterministic in tests when you replace the clock with UseSystemTime<TClock>().

Typed channel

Use AddWebhooks<TEvent>() to register a channel that receives only events whose data class is TEvent. At construction time the typed channel (WebhookPublishChannel<TEvent>) merges the general WebhookPublishOptions with the type-specific WebhookPublishOptions<TEvent>: non-null typed values win; null values fall back to the base defaults. AdditionalHeaders are merged at the dictionary level — typed entries win on key collision. Channel-structural properties (SignatureHeaderName, DeliveryIdHeaderName, RetryableStatusCodes, …) are always taken from the base options and cannot be overridden per event type.

builder.Services
.AddEventPublisher()
// General catch-all webhook
.AddWebhooks(opts =>
{
opts.EndpointUrl = "https://partner.example.com/events";
opts.SigningSecret = "shared-secret";
opts.MaxRetryCount = 3;
opts.SignatureAlgorithm = WebhookSignatureAlgorithm.HmacSha256;
})
// OrderPlaced delivers to a dedicated endpoint with its own secret
.AddWebhooks<OrderPlaced>(opts =>
{
opts.EndpointUrl = "https://orders.example.com/hooks";
opts.SigningSecret = "order-secret";
// MaxRetryCount and SignatureAlgorithm inherited from base
});

From configuration:

builder.Services
.AddEventPublisher()
.AddWebhooks("Events:Webhook")
.AddWebhooks<OrderPlacedData>("Events:Webhook:Orders");
{
"Events": {
"Webhook": {
"EndpointUrl": "https://partner.example.com/events",
"SigningSecret": "shared-secret",
"MaxRetryCount": 3,
"Orders": {
"EndpointUrl": "https://orders.example.com/hooks",
"SigningSecret": "order-secret"
}
}
}
}

See Typed Channels for the full merge semantics and further examples.

Signature algorithms

ValueAlgorithmNote
HmacSha256HMAC-SHA256Recommended default
HmacSha384HMAC-SHA384
HmacSha512HMAC-SHA512
HmacSha1HMAC-SHA1Deprecated; included for legacy compatibility

The signature is computed over <timestamp>.<body> and sent in the configured signature header.

Message formats

Use EventMessageFormat constants when selecting a built-in format.

MessageFormat valueContent-TypeDescription
"json" (EventMessageFormat.Json)application/jsonPlain JSON payload (default)
"xml" (EventMessageFormat.Xml)application/xmlPlain XML payload
"cloudevents+json" (EventMessageFormat.CloudEventsJson)application/cloudevents+jsonFull CloudEvents JSON envelope
"cloudevents+xml" (EventMessageFormat.CloudEventsXml)application/cloudevents+xmlFull CloudEvents XML envelope

Per-delivery options

Pass a WebhookPublishOptions instance as the second argument to PublishAsync. Only the properties you set (non-null) override the channel default — all others fall back to the values configured at registration time. AdditionalHeaders are merged: per-call entries win on key collision.

using Hermodr;

// Resolve the concrete channel directly from DI.
var webhookChannel = serviceProvider.GetRequiredService<WebhookPublishChannel>();

// Override endpoint and secret for this delivery only;
// everything else (MaxRetryCount, SignatureAlgorithm, …) is inherited.
await webhookChannel.PublishAsync(@event, new WebhookPublishOptions
{
EndpointUrl = "https://dynamic-endpoint.example.com/hook",
SigningSecret = "per-tenant-secret",
SignatureAlgorithm = WebhookSignatureAlgorithm.HmacSha512
});

You can also supply overrides through the non-generic IEventPublishChannel interface — casting the options to EventPublishOptions works because WebhookPublishOptions inherits from it:

IEventPublishChannel channel = serviceProvider.GetRequiredService<WebhookPublishChannel>();
await channel.PublishAsync(@event, new WebhookPublishOptions { EndpointUrl = "https://..." });

Batch delivery

The channel implements IBatchEventPublishChannel for dispatching multiple events in a single HTTP call:

var batchChannel = serviceProvider.GetRequiredService<IBatchEventPublishChannel>();

await batchChannel.PublishBatchAsync(events, new WebhookPublishOptions
{
EndpointUrl = "https://partner.example.com/events/batch"
});

Custom serialiser

Register a custom IEventSerializer by adding it to the service collection as a singleton enumerable entry for IEventSerializer. The channel picks it up by matching its Format key.

public class ProtobufEventSerializer : IEventSerializer
{
public string Format => "protobuf";
public string ContentType => "application/x-protobuf";
public string BatchContentType => "application/x-protobuf";

public byte[] Serialize(CloudEvent @event)
{
// ... protobuf serialisation
throw new NotImplementedException();
}

public byte[] SerializeBatch(IReadOnlyList<CloudEvent> events)
{
// ... batch serialisation
throw new NotImplementedException();
}
}
builder.Services
.AddEventPublisher()
.AddWebhooks(options => options.MessageFormat = "protobuf");

// Register the custom serializer so the channel discovers it via DI
builder.Services.AddSingleton<IEventSerializer, ProtobufEventSerializer>();

Custom signature provider

Implement IWebhookSignatureProvider and register it as a singleton:

public class Ed25519SignatureProvider : IWebhookSignatureProvider
{
public static readonly Ed25519SignatureProvider Default = new();
public WebhookSignatureAlgorithm Algorithm => (WebhookSignatureAlgorithm)100; // custom value
public string AlgorithmName => "ed25519";

public string ComputeSignature(byte[] payload, long timestamp, string secret)
{
// ... Ed25519 signing
throw new NotImplementedException();
}
}
builder.Services
.AddEventPublisher()
.AddWebhooks(options => { /* ... */ });

// Register the custom provider so the channel discovers it via DI
builder.Services.AddSingleton<IWebhookSignatureProvider, Ed25519SignatureProvider>();