- Docs
- Background work
- Webhooks
Webhooks
Deliver signed outbound webhooks with per-target secrets over HTTP or the retrying queue.
Use webhook for signed outbound event delivery. The interface covers Register, Unregister, and Deliver fan-out. Signatures use t=<timestamp>,v1=<hex-hmac> over <timestamp>.<payload>, verified with hmac.Equal inside a timestamp window. There is no top-level API key in Options: every target gets its own secret at Register time, so one compromised secret never exposes every integration.
Quick start
Section titled “Quick start”WEBHOOK_ADAPTER=queueWEBHOOK_TIMEOUT=10sWEBHOOK_MAX_RETRIES=3WEBHOOK_QUEUE_ADAPTER=memoryWEBHOOK_QUEUE_OPTS_VISIBILITY_TIMEOUT=30sWEBHOOK_QUEUE_OPTS_POLL_TIMEOUT=1sWEBHOOK_QUEUE_OPTS_BUFFER=100WEBHOOK_DEAD_LETTER_TOPIC=webhook.deadWEBHOOK_REPLAY_TOLERANCE=300000000000WEBHOOK_ADAPTER=queueWEBHOOK_MAXRETRIES=3WEBHOOK_REPLAYTOLERANCE=5mBOOKINGS_WEBHOOK_CREATED_TARGET=https://partner.example.com/hooksBOOKINGS_WEBHOOK_CREATED_SECRET=per-target-secretKeep secrets out of the file. Validate rejects negative timeout, max_retries, and replay_tolerance. The queue adapter, not core, enforces the queue rules (adapter required, visibility exceeding timeout). All keys: webhook config reference.
Register a target and deliver
Section titled “Register a target and deliver”package main
import ( "context" "fmt"
webhookhttp "github.com/zenta-dev/zever/adapters/webhook/http" "github.com/zenta-dev/zever/config" "github.com/zenta-dev/zever/container")
func main() { webhookhttp.Register() wh, _ := container.New(config.Default()).Webhook() defer wh.Close()
target := "" // empty means skip, like RegisterDemoTargets if target == "" { fmt.Println("delivery skipped: no target configured") return } _ = wh.Register(context.Background(), "booking.created", target, "secret")}delivery skipped: no target configuredWith a target set and the http adapter, Deliver POSTs the signed payload to every subscriber of the event, with the signature in the X-Hub-Signature-256 header (the queue adapter uses X-Webhook-Signature and verifies it before delivery). Webhook() resolves lazily, caches per container, and retries after errors; Close() error takes no context. See the container.
The bookings example sets adapter: queue. Demo targets come from the environment only (BOOKINGS_WEBHOOK_CREATED_TARGET and _SECRET, plus the CANCELLED pair). RegisterDemoTargets skips pairs with an empty target, so delivery is quietly skipped when unconfigured and the worker runs without them.
Choose an adapter
Section titled “Choose an adapter”| Adapter | Package | Use when |
|---|---|---|
http |
adapters/webhook/http |
Default. Direct POST with no durability. A crash mid-delivery loses the event. |
queue |
adapters/webhook/queue |
Delivery rides the queue: at-least-once, exponential redelivery backoff (500ms base, 72h cap, up to max_retries), then the dead-letter topic. |
With http, N replicas mean N independent retry loops. Prefer queue so retries funnel through shared infrastructure, with a dead-letter topic for exhausted deliveries (webhook.dead in the demo). The bookings README notes the queue variant needs its queue reference wired before doctor goes green.
Production checklist
Section titled “Production checklist”- Never log per-target secrets, and never log raw option maps. Use
config.RedactedServices. - Verifiers must check both the HMAC (constant-time) and the timestamp inside
replay_tolerance(default 5m). - Keep
allow_private_targetsfalse in production to avoid SSRF into internal URLs. - The container closes only resolved services and joins errors via
errors.Join.
Troubleshooting
Section titled “Troubleshooting”The webhook silently does nothing
The target env var is empty, so registration was skipped by design; this is not an error path. Set BOOKINGS_WEBHOOK_CREATED_TARGET and its secret, then restart the worker.
Signatures verify but replays succeed
The verifier checks the HMAC but not the timestamp window. Enforce replay_tolerance on the embedded t= value so hour-old payloads are rejected.
Queue adapter open fails
queue_adapter is unset, or visibility_timeout does not exceed timeout. Mirror the bookings shape (memory adapter, 30s visibility over a 10s timeout).