Notifications
When a projection produces new state, Hapnd can push that change to your infrastructure. Notifications fire on projection state changes, not raw events — you receive the computed state, ready to use.
Three delivery mechanisms are available. Choose based on your use case, or combine them.
Webhooks
Section titled “Webhooks”Outbound HTTP POST to your endpoint with HMAC-SHA256 signatures for verification. Best for server-to-server integration where you want Hapnd to push to you reliably.
Configuration
Section titled “Configuration”Webhook configuration is provided when you upload a projection:
curl -X POST https://hapnd-api.lightestnight.workers.dev/projections/upload \ -H "X-API-Key: your_key" \ -F 'config={"notifications":{"webhook":{"url":"https://your-api.com/hooks/hapnd"}}}'Optional fields:
{ "notifications": { "webhook": { "url": "https://your-api.com/hooks/hapnd", "isolateSecret": true, "headers": { "X-Custom-Header": "value" } } }}isolateSecret gives this projection its own webhook secret instead of sharing the tenant-level default. Custom headers are forwarded with each delivery (max 10 headers).
Webhook Payload
Section titled “Webhook Payload”{ "type": "projection.state_changed", "sequence": 1, "projectionId": "proj_abc123", "aggregateId": "order_123", "aggregateType": "order", "version": 5, "state": { "total": 125.00, "items": ["Widget", "Gadget"], "itemCount": 2 }, "triggeredBy": "evt_xyz789", "timestamp": "2026-01-09T12:00:00Z"}state contains the full computed projection state — the output of your Apply method.
Webhook Headers
Section titled “Webhook Headers”| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Hapnd-Signature | sha256=<hex-encoded HMAC-SHA256 of the request body> |
X-Hapnd-Delivery-Id | {projectionId}-{sequence} — unique per delivery |
X-Hapnd-Attempt | Attempt number (1–5) |
Plus any custom headers you configured.
Signature Verification
Section titled “Signature Verification”Every webhook includes an HMAC-SHA256 signature in the X-Hapnd-Signature header. The signature is hex-encoded and prefixed with sha256=. Verify it before processing the payload.
C#:
var header = request.Headers["X-Hapnd-Signature"].ToString();var payload = await new StreamReader(request.Body).ReadToEndAsync();
// Strip the "sha256=" prefixvar receivedSignature = header.Replace("sha256=", "");
// Compute HMAC-SHA256 as hexusing var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret));var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));var computed = Convert.ToHexString(hash).ToLowerInvariant();
if (receivedSignature != computed){ return Results.Unauthorized();}TypeScript:
const header = request.headers.get("X-Hapnd-Signature")!;const payload = await request.text();
const receivedSignature = header.replace("sha256=", "");
const encoder = new TextEncoder();const key = await crypto.subtle.importKey( "raw", encoder.encode(webhookSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);const signatureBuffer = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));const computed = Array.from(new Uint8Array(signatureBuffer)) .map(b => b.toString(16).padStart(2, "0")) .join("");
if (receivedSignature !== computed) { return new Response("Unauthorized", { status: 401 });}Webhook Secrets
Section titled “Webhook Secrets”Hapnd generates webhook secrets automatically using the whsec_ prefix convention. Secrets are managed at two levels:
- Tenant-level — a shared secret used by all projections by default
- Per-projection — an isolated secret for projections that need independent rotation
The active secret is returned in the upload response when you configure a webhook:
{ "success": true, "projectionId": "proj_abc123", "status": "compiling", "notifications": { "webhook": { "enabled": true, "secret": "whsec_K7xN2bQ9mP4xR1sT8vW5yZ2a...", "isolated": false } }}The isolated field tells you whether this is a projection-specific secret (true) or the tenant-wide shared secret (false).
Rotate secrets via the API:
# Rotate tenant-level secret (affects all projections using the shared secret)curl -X POST https://hapnd-api.lightestnight.workers.dev/webhooks/rotate \ -H "X-API-Key: your_key"
# Rotate an isolated projection secretcurl -X POST https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/webhook/rotate \ -H "X-API-Key: your_key"Delivery and Retries
Section titled “Delivery and Retries”Failed deliveries are retried up to 5 times with exponential backoff. After all retries are exhausted, the delivery is sent to a dead-letter queue for investigation. The notification log is the source of truth — you can always replay missed deliveries by polling /projections/{id}/notifications with the last sequence you successfully processed.
Webhook delivery times out after 10 seconds. Return a 2xx status code to acknowledge receipt.
WebSocket Streaming
Section titled “WebSocket Streaming”Real-time push via persistent WebSocket connections. Best for dashboards, live UIs, and reactive workflows where latency matters.
.NET SDK
Section titled “.NET SDK”var subscription = hapnd.Subscriptions() .OnStateChanged<OrderState>(async (update, ct) => { Console.WriteLine($"Order {update.AggregateId} total: {update.State.Total}"); }) .OnError(async (error, ct) => { error.Action.Reconnect(); }) .Subscribe();
// Later:await subscription.DisposeAsync();TypeScript SDK
Section titled “TypeScript SDK”const subscription = hapnd.subscribe({ projections: [ { id: "proj_orders", onUpdate: async (update) => { console.log(`Order ${update.aggregateId} total: ${update.state.total}`); }, }, ], onError: async (error) => { error.action.reconnect(); },});
// Later:await subscription.close();Each projection gets its own WebSocket connection with automatic reconnection and sequence tracking. See the .NET SDK and TypeScript SDK docs for full details.
REST Polling
Section titled “REST Polling”Cursor-based polling for environments where WebSockets aren’t practical or you prefer pull-based consumption.
# First request — get latest notificationscurl https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications \ -H "X-API-Key: your_key"
# Subsequent requests — pass the last sequence to get only new notificationscurl "https://hapnd-api.lightestnight.workers.dev/projections/proj_abc/notifications?afterSequence=42" \ -H "X-API-Key: your_key"The response includes an array of notifications, each with a sequence number. Pass the highest sequence as afterSequence on your next request to get only new changes.
Choosing a Mechanism
Section titled “Choosing a Mechanism”| Mechanism | Best for | Trade-offs |
|---|---|---|
| Webhooks | Server-to-server, reliable delivery | Hapnd manages retries and DLQ; you provide an endpoint |
| WebSocket | Real-time dashboards, reactive workflows | Requires persistent connection; SDK handles reconnection |
| Polling | Simple integrations, batch processing | You control the pace; higher latency than push |
You can use multiple mechanisms for the same projection. For example, WebSocket for your dashboard and webhooks for your billing system.
Notification Retention
Section titled “Notification Retention”Notification logs are retained for 7 days. After that, they’re archived to R2 storage and removed from the active log. The daily cleanup runs at 3am UTC.
If you need to reprocess notifications older than 7 days, the projection can be re-activated to replay from historical events.