A reliable webhook dispatcher service written in Go. Postbird handles webhook registration, event ingestion, and guaranteed delivery with automatic retries.
- Webhook Registration - Register endpoints with custom headers, secrets, and retry policies
- Event Ingestion - Receive events via API and queue them for delivery
- Reliable Delivery - Automatic retries with exponential backoff and jitter
- HMAC Signatures - Stripe/GitHub-style signature verification for security
- Delivery Tracking - Track delivery status, attempts, response codes, and latency
- Dead Letter Queue - Failed deliveries are moved to DLQ for manual review/replay
- Prometheus Metrics - Built-in observability with Prometheus metrics
- Simple API - RESTful API for managing webhooks and monitoring deliveries
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Producer ββββββΆβ Queue ββββββΆβ Workers β
β (API) β β (in-memory)β β (goroutine β
β β β β β pool) β
βββββββββββββββ βββββββββββββββ ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Target β
β Endpoint β
βββββββββββββββ
# Build and run
make run
# Or with Docker
make docker-up# Create a webhook
curl -X POST http://localhost:8080/api/v1/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-endpoint.com/webhook",
"description": "My webhook",
"events": ["user.created", "order.completed"]
}'
# Send an event
curl -X POST http://localhost:8080/api/v1/events \
-H "Content-Type: application/json" \
-d '{
"type": "user.created",
"payload": {
"user_id": "123",
"email": "user@example.com"
}
}'
# Check delivery status
curl http://localhost:8080/api/v1/deliveries/{delivery_id}
# Retry a failed delivery
curl -X POST http://localhost:8080/api/v1/deliveries/{delivery_id}/retry| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/webhooks |
Register a new webhook |
GET |
/api/v1/webhooks |
List all webhooks |
GET |
/api/v1/webhooks/:id |
Get a specific webhook |
PUT |
/api/v1/webhooks/:id |
Update a webhook |
DELETE |
/api/v1/webhooks/:id |
Delete a webhook |
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/v1/events |
Ingest an event |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/deliveries |
List deliveries |
GET |
/api/v1/deliveries/:id |
Get delivery status |
POST |
/api/v1/deliveries/:id/retry |
Retry a failed delivery |
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/dlq |
List DLQ items |
POST |
/api/v1/dlq/:id/replay |
Replay from DLQ |
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/metrics |
Prometheus metrics |
{
"url": "https://api.example.com/webhooks",
"description": "Production webhook",
"events": ["user.created", "user.updated"],
"headers": {
"X-Custom-Header": "value"
},
"retry_policy": {
"max_attempts": 5,
"initial_interval_seconds": 10,
"max_interval_seconds": 3600
}
}If events is empty or not provided, the webhook will receive all event types. Otherwise, it will only receive events matching the specified types.
Postbird signs all webhook payloads using HMAC-SHA256. The signature is included in the X-Postbird-Signature header:
X-Postbird-Signature: t=1699459200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
func verifySignature(payload []byte, secret, signature string) error {
parts := parseSignature(signature) // Parse t= and v1= values
timestamp := parts["t"]
expectedSig := parts["v1"]
// Recreate the signed payload
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(payload))
// Compute HMAC-SHA256
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(signedPayload))
actualSig := hex.EncodeToString(h.Sum(nil))
if !hmac.Equal([]byte(expectedSig), []byte(actualSig)) {
return errors.New("signature mismatch")
}
// Check timestamp is recent (within 5 minutes)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Since(time.Unix(ts, 0)) > 5*time.Minute {
return errors.New("signature expired")
}
return nil
}| Header | Description |
|---|---|
X-Postbird-Signature |
HMAC-SHA256 signature |
X-Postbird-Timestamp |
Unix timestamp |
X-Postbird-Webhook-ID |
Webhook identifier |
X-Postbird-Delivery-ID |
Delivery identifier |
X-Postbird-Event-Type |
Event type |
Content-Type |
application/json |
User-Agent |
Postbird/1.0 |
Postbird uses exponential backoff with jitter:
delay = initial_interval * multiplier^attempt + jitter
Default values:
- Initial interval: 10 seconds
- Max interval: 1 hour
- Multiplier: 2.0
- Max attempts: 5
Retry schedule: 10s β 20s β 40s β 80s β 160s (capped at max_interval)
| Environment Variable | Default | Description |
|---|---|---|
HOST |
0.0.0.0 |
Server host |
PORT |
8080 |
Server port |
DB_DRIVER |
sqlite |
Database driver |
DB_DSN |
postbird.db?_journal_mode=WAL |
Database connection string |
WORKER_COUNT |
10 |
Number of delivery workers |
WORKER_REQUEST_TIMEOUT |
30s |
HTTP request timeout |
QUEUE_BUFFER_SIZE |
10000 |
In-memory queue buffer |
# Install dependencies
make deps
# Run tests
make test
# Run linter
make lint
# Format code
make fmt
# Build Docker image
make docker
# Run with Docker Compose
make docker-up
# View logs
make docker-logspostbird/
βββ cmd/
β βββ server/
β βββ main.go # Application entry point
βββ internal/
β βββ api/ # HTTP handlers and router
β βββ config/ # Configuration
β βββ metrics/ # Prometheus metrics
β βββ queue/ # Queue interface and implementations
β βββ storage/ # Database repositories
β βββ webhook/ # Domain models and signing
β βββ worker/ # Delivery workers
βββ docker-compose.yml
βββ Dockerfile
βββ Makefile
βββ prometheus.yml
βββ go.mod
βββ README.md
- PostgreSQL support for production
- Redis queue for distributed deployment
- Web dashboard for monitoring
- Rate limiting per webhook
- Webhook validation (test delivery)
- Event filtering with JSONPath
- Batch event ingestion
- OpenTelemetry tracing
MIT