This guide walks you through setting up WebhookEngine, creating an application, registering endpoints, and sending your first webhook -- all in under 10 minutes.
- Docker and Docker Compose
That's it. WebhookEngine runs as a single container alongside PostgreSQL.
git clone https://github.com/voyvodka/webhook-engine.git
cd webhook-engine
docker compose -f docker/docker-compose.yml up -dWebhookEngine is now running at http://localhost:5100.
Open http://localhost:5100 in your browser.
Default credentials:
| Field | Value |
|---|---|
admin@example.com |
|
| Password | changeme |
Change these in production by setting
WebhookEngine__DashboardAuth__AdminEmailandWebhookEngine__DashboardAuth__AdminPasswordenvironment variables.
An application represents a tenant or service that sends webhooks. Each application gets its own API key.
- In the dashboard, go to Applications
- Click Create Application
- Enter a name (e.g. "My SaaS App")
- Copy the generated API key (format:
whe_{appId}_{random}) -- you won't see it again
Or via API:
# Create an application (requires dashboard cookie auth)
curl -X POST http://localhost:5100/api/v1/applications \
-H "Content-Type: application/json" \
-b "your-session-cookie" \
-d '{"name": "My SaaS App"}'Event types categorize webhooks (e.g. order.created, user.updated, payment.failed).
export API_KEY="whe_abc123_your-api-key"
curl -X POST http://localhost:5100/api/v1/event-types \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order.created",
"description": "Fired when a new order is placed"
}'Response:
{
"data": {
"id": "a1b2c3d4-...",
"name": "order.created",
"description": "Fired when a new order is placed",
"isArchived": false,
"createdAt": "2026-02-27T10:00:00Z"
},
"meta": { "requestId": "req_..." }
}An endpoint is a URL that receives webhook deliveries. Endpoints can filter by event type.
curl -X POST http://localhost:5100/api/v1/endpoints \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhook",
"description": "Production webhook handler",
"filterEventTypes": ["a1b2c3d4-..."]
}'If
filterEventTypesis omitted or empty, the endpoint receives all event types.
curl -X POST http://localhost:5100/api/v1/messages \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventType": "order.created",
"payload": {
"orderId": "ORD-001",
"amount": 99.99,
"currency": "USD"
},
"idempotencyKey": "order-001"
}'Response (HTTP 202 Accepted):
{
"data": {
"messageIds": ["msg_..."],
"endpointCount": 1,
"eventType": "order.created"
},
"meta": { "requestId": "req_..." }
}The message is now queued. WebhookEngine's delivery worker will pick it up and deliver it to all matching endpoints within seconds.
curl http://localhost:5100/api/v1/messages/{messageId} \
-H "Authorization: Bearer $API_KEY"Message status values:
| Status | Meaning |
|---|---|
pending |
Queued, waiting for delivery |
sending |
Currently being delivered |
delivered |
Successfully delivered (HTTP 2xx) |
failed |
Delivery failed, will retry |
deadletter |
All retries exhausted |
View delivery attempts:
curl http://localhost:5100/api/v1/messages/{messageId}/attempts \
-H "Authorization: Bearer $API_KEY"Every webhook delivery includes HMAC-SHA256 signature headers following the Standard Webhooks spec:
| Header | Example |
|---|---|
webhook-id |
msg_abc123 |
webhook-timestamp |
1709042400 (Unix seconds) |
webhook-signature |
v1,K6x9h3... (base64) |
The signed content is: {webhook-id}.{webhook-timestamp}.{body}
import hmac, hashlib, base64
def verify(body: str, secret: str, headers: dict) -> bool:
signed = f"{headers['webhook-id']}.{headers['webhook-timestamp']}.{body}"
expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).digest()
expected_sig = f"v1,{base64.b64encode(expected).decode()}"
return hmac.compare_digest(headers['webhook-signature'], expected_sig)import { createHmac } from "crypto";
function verify(body: string, secret: string, headers: Record<string, string>): boolean {
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${body}`;
const hash = createHmac("sha256", secret).update(signed).digest("base64");
return headers["webhook-signature"] === `v1,${hash}`;
}using System.Security.Cryptography;
using System.Text;
bool Verify(string body, string secret, IDictionary<string, string> headers)
{
var signed = $"{headers["webhook-id"]}.{headers["webhook-timestamp"]}.{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signed));
var expected = $"v1,{Convert.ToBase64String(hash)}";
return headers["webhook-signature"] == expected;
}For production use, see the complete verification helpers in
samples/signature-verification/which include timestamp tolerance checks and constant-time comparison.
Install the NuGet package (or reference the project directly):
using WebhookEngine.Sdk;
using var client = new WebhookEngineClient("whe_abc_your-api-key", "http://localhost:5100");
// Create event type
var eventType = await client.EventTypes.CreateAsync(new CreateEventTypeRequest
{
Name = "invoice.paid",
Description = "Invoice payment received"
});
// Create endpoint
var endpoint = await client.Endpoints.CreateAsync(new CreateEndpointRequest
{
Url = "https://your-app.example.com/webhook",
FilterEventTypes = [eventType!.Id]
});
// Send webhook
var result = await client.Messages.SendAsync(new SendMessageRequest
{
EventType = "invoice.paid",
Payload = new { invoiceId = "INV-001", amount = 250.00 }
});
Console.WriteLine($"Sent to {result!.EndpointCount} endpoint(s)");See the full sample app in samples/WebhookEngine.Sample.Sender/.
Failed deliveries are automatically retried with exponential backoff:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | 5 seconds | 5s |
| 2 | 30 seconds | 35s |
| 3 | 2 minutes | ~2.5 min |
| 4 | 15 minutes | ~17.5 min |
| 5 | 1 hour | ~1.3 hr |
| 6 | 6 hours | ~7.3 hr |
| 7 | 24 hours | ~31.3 hr |
After all 7 attempts, the message moves to dead letter status. You can manually retry dead-letter messages:
curl -X POST http://localhost:5100/api/v1/messages/{messageId}/retry \
-H "Authorization: Bearer $API_KEY"WebhookEngine tracks endpoint health. If an endpoint fails 5 consecutive deliveries, its circuit breaker opens:
- Closed (healthy): Deliveries proceed normally
- Open (failing): Deliveries are skipped, messages remain in queue
- Half-open (recovering): After 5 minutes, one test delivery is attempted
This prevents hammering unhealthy endpoints and wasting resources.
The real-time dashboard at http://localhost:5100 shows:
- Delivery statistics (last 24h)
- Endpoint health indicators (Active / Degraded / Failed)
- Message log with filtering (by event type, endpoint, status, date range)
- Delivery attempt details (request headers, response body, latency)
- Live delivery feed via SignalR
Scrape http://localhost:5100/metrics for detailed delivery metrics. See the README for the full metrics list.
- Self-Hosting Guide -- production deployment, security, and configuration
- API Reference -- complete endpoint documentation
- Architecture -- system design and internals
- Contributing -- how to contribute