Skip to content

Webhooks

Granit.Webhooks provides outbound webhook dispatch with HMAC-SHA256 signed deliveries, retry policies, and ISO 27001-compliant audit trails.

  • DirectoryGranit.Webhooks/ Core publisher, HMAC signatures, in-memory default
    • Granit.Webhooks.EntityFrameworkCore Durable subscriptions + delivery audit
    • Granit.Webhooks.Wolverine Durable outbox dispatch via Wolverine
PackageRoleDepends on
Granit.WebhooksIWebhookPublisher, HMAC delivery, domain eventsGranit.Timing
Granit.Webhooks.EntityFrameworkCoreIsolated DbContext for subscriptions + deliveriesGranit.Webhooks, Granit.Persistence
Granit.Webhooks.WolverineDurable outbox dispatch, retry policiesGranit.Webhooks, Granit.Wolverine
graph TD
    W[Granit.Webhooks] --> T[Granit.Timing]
    WEF[Granit.Webhooks.EntityFrameworkCore] --> W
    WEF --> P[Granit.Persistence]
    WW[Granit.Webhooks.Wolverine] --> W
    WW --> WLV[Granit.Wolverine]

[DependsOn(typeof(GranitWebhooksModule))]
public class AppModule : GranitModule { }

Uses in-memory subscription store and Channel-based in-process dispatch. Suitable for development and integration tests.

Inject IWebhookPublisher and call PublishAsync with a logical event type and payload:

public class DocumentUploadedHandler(IWebhookPublisher webhookPublisher)
{
public async Task HandleAsync(
DocumentUploaded evt, CancellationToken cancellationToken)
{
await webhookPublisher.PublishAsync(
"document.uploaded",
new { evt.DocumentId, evt.FileName, evt.UploadedBy },
cancellationToken).ConfigureAwait(false);
}
}

The publisher serializes the payload into a WebhookEnvelope, captures the ambient tenant context, and dispatches a WebhookTrigger to the outbox. The fanout handler resolves matching subscriptions and enqueues one SendWebhookCommand per subscriber.

Every HTTP POST to a subscriber endpoint carries a standardized JSON envelope:

{
"eventId": "a1b2c3d4-...",
"eventType": "document.uploaded",
"tenantId": "d4e5f6a7-...",
"timestamp": "2026-03-13T10:30:00Z",
"apiVersion": "1.0",
"data": {
"documentId": "f8e9d0c1-...",
"fileName": "report.pdf",
"uploadedBy": "user-42"
}
}

The eventId is stable across retry attempts, allowing subscribers to deduplicate.

Each delivery is signed with the subscription’s secret using HMAC-SHA256 in Stripe format:

x-granit-signature: t=1710323400,v1=5d3b2a1c...

The signed payload is {unix_timestamp}.{body_json}. Subscribers should:

  1. Extract the timestamp and signature from the header
  2. Recompute the HMAC using the shared secret
  3. Constant-time compare the signatures
  4. Reject requests older than 5 minutes (replay protection)

Exponential backoff: 30s, 2m, 10m, 30m, 2h, 12h. After 6 retries (~14h30 total), the message moves to the Dead-Letter Queue and the subscription is suspended.

When consecutive failures exceed the threshold, the subscription transitions:

FailuresStatusDomain event
0Active
Threshold reachedSuspendedWebhookSubscriptionSuspended
Non-retriable (4xx)DeactivatedWebhookSubscriptionDeactivated

Suspension records SuspendedAt and SuspendedBy for ISO 27001 audit compliance.

WebhookDeliveryAttempt is an INSERT-only entity (no soft-delete) that records every delivery attempt: HTTP status, duration, payload hash (SHA-256), and optional full payload when StorePayload is enabled.

{
"Webhooks": {
"HttpTimeoutSeconds": 10,
"MaxParallelDeliveries": 20,
"StorePayload": false
}
}
PropertyDefaultDescription
HttpTimeoutSeconds10HTTP request timeout (5–120 seconds)
MaxParallelDeliveries20Max parallel SendWebhookCommand on the delivery queue (1–100)
StorePayloadfalsePersist full JSON body alongside delivery attempts
CategoryKey typesPackage
ModulesGranitWebhooksModule, GranitWebhooksEntityFrameworkCoreModule, GranitWebhooksWolverineModuleWebhooks
PublisherIWebhookPublisher (PublishAsync<TPayload>)Granit.Webhooks
SubscriptionsIWebhookSubscriptionReader, IWebhookSubscriptionWriter, WebhookSubscriptionGranit.Webhooks
DeliveryWebhookDeliveryAttempt, WebhookEnvelope, IWebhookDeliveryReader, IWebhookDeliveryWriterGranit.Webhooks
EventsWebhookSubscriptionSuspended, WebhookSubscriptionDeactivatedGranit.Webhooks
OptionsWebhooksOptionsGranit.Webhooks
ExtensionsAddGranitWebhooks(), AddGranitWebhooksEntityFrameworkCore()