# Webhook routes ## Purpose The webhook-route module owns the two webhook endpoints: a delivery event ingestion endpoint that normalises and persists provider events through `DeliveryEventStore`, and a one-click unsubscribe endpoint driven by a base64-JSON token. The delivery handler is the authoritative fan-out point: it persists the event and — for the three "negative" events (`unsubscribed`, `bounced`, `complained`) — additionally inserts a `CampaignSuppressionEntry` so the sender pipeline can short-circuit future deliveries to the same contact. The unsubscribe endpoint hardcodes `channel: "email"` because the token format does not carry a channel field (the design assumes unsubscribe links are always embedded in email messages). Multi-channel unsubscribe is a future extension — when that ships, the token shape will gain a `channel` field and the handler will decode it. ## Features - 2 builders: `buildDeliveryWebhookRoute`, `buildUnsubscribeRoute`. - Delivery handler fan-out: records the event, then optionally adds a suppression entry for `unsubscribed` / `bounced` / `complained`. - Idempotent suppression: a duplicate `add` is short-circuited by a pre-existing `check` hit, so retries from the provider do not raise `DUPLICATE_SUPPRESSION`. - Strict tenant guard: every handler enforces `requireTenant` so provider webhooks must traverse a tenant-aware auth layer before hitting the handler. - Token decode failures map to `INVALID_UNSUBSCRIBE_TOKEN` (HTTP 400); schema failures on the delivery body map to `WEBHOOK_REJECTED`. ## Usage: Basic ```ts import { buildDeliveryWebhookRoute, buildUnsubscribeRoute, } from "@suaveplan/api-plugin-campaigns"; app.post(buildDeliveryWebhookRoute().route.path, (c) => buildDeliveryWebhookRoute().handler(c) ); app.get(buildUnsubscribeRoute().route.path, (c) => buildUnsubscribeRoute().handler(c) ); ``` ## Usage: Advanced ```ts import { buildDeliveryWebhookRoute, buildUnsubscribeRoute, definePluginRoutes, } from "@suaveplan/api-plugin-campaigns"; definePluginRoutes( [buildDeliveryWebhookRoute(), buildUnsubscribeRoute()], { app, registry, pluginName: "campaigns" } ); ``` ## API Reference - `buildDeliveryWebhookRoute(): PluginRouteEntry` - `buildUnsubscribeRoute(): PluginRouteEntry` ## Implementation Notes The negative-event mapping (`unsubscribed → unsubscribe`, `bounced → bounce`, `complained → complaint`) is exhaustive but intentionally narrow — we do not auto-suppress on `dropped` or `deferred` because those are transient provider states that should trigger investigation rather than permanent suppression. A future change can expand the mapping if operators request a `dropped → manual` auto-suppression policy. The handler returns `{ eventId, suppressed }` so the calling provider can correlate retries against a stable id and operators can confirm via response inspection whether the fan-out fired.