# 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

```sh
WEBHOOK_ADAPTER=queue
WEBHOOK_TIMEOUT=10s
WEBHOOK_MAX_RETRIES=3
WEBHOOK_QUEUE_ADAPTER=memory
WEBHOOK_QUEUE_OPTS_VISIBILITY_TIMEOUT=30s
WEBHOOK_QUEUE_OPTS_POLL_TIMEOUT=1s
WEBHOOK_QUEUE_OPTS_BUFFER=100
WEBHOOK_DEAD_LETTER_TOPIC=webhook.dead
WEBHOOK_REPLAY_TOLERANCE=300000000000
```

```sh
WEBHOOK_ADAPTER=queue
WEBHOOK_MAXRETRIES=3
WEBHOOK_REPLAYTOLERANCE=5m
BOOKINGS_WEBHOOK_CREATED_TARGET=https://partner.example.com/hooks
BOOKINGS_WEBHOOK_CREATED_SECRET=per-target-secret
```

Keep 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](https://zenta-dev.github.io/zever/reference/config/#webhook).

## Register a target and deliver

```go title="main.go" check
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")
}
```

```text
delivery skipped: no target configured
```

With 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](https://zenta-dev.github.io/zever/concepts/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

| 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](https://zenta-dev.github.io/zever/guides/background/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

- 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_targets` false in production to avoid SSRF into internal URLs.
- The container closes only resolved services and joins errors via `errors.Join`.

## Troubleshooting

<details>
<summary>The webhook silently does nothing</summary>

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.
</details>

<details>
<summary>Signatures verify but replays succeed</summary>

The verifier checks the HMAC but not the timestamp window. Enforce `replay_tolerance` on the embedded `t=` value so hour-old payloads are rejected.
</details>

<details>
<summary>Queue adapter open fails</summary>

`queue_adapter` is unset, or `visibility_timeout` does not exceed `timeout`. Mirror the bookings shape (memory adapter, 30s visibility over a 10s timeout).
</details>

## Related

- [Queue](https://zenta-dev.github.io/zever/guides/background/queue/): Retry and backoff under delivery.
- [Locks and idempotency](https://zenta-dev.github.io/zever/guides/security/lock-idempotency/): Dedupe retried events.
- [Scheduler and jobs](https://zenta-dev.github.io/zever/guides/background/scheduler-jobs/): Worker wiring.
