Files
skills/docs/api-stack/api/api-plugin-campaigns/src/routes/webhooks/routes.md
2026-05-29 08:20:56 +01:00

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 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

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(): 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.