2.9 KiB
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
addis short-circuited by a pre-existingcheckhit, so retries from the provider do not raiseDUPLICATE_SUPPRESSION. - Strict tenant guard: every handler enforces
requireTenantso 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 toWEBHOOK_REJECTED.
Usage: Basic
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
import {
buildDeliveryWebhookRoute,
buildUnsubscribeRoute,
definePluginRoutes,
} from "@suaveplan/api-plugin-campaigns";
definePluginRoutes(
[buildDeliveryWebhookRoute(), buildUnsubscribeRoute()],
{ app, registry, pluginName: "campaigns" }
);
API Reference
buildDeliveryWebhookRoute(): PluginRouteEntrybuildUnsubscribeRoute(): 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.