Deliverability & compliance
Healthy delivery through Resend, plus the only compliance surface CogniLead carries: a per-tenant suppression list and RFC 8058 one-click unsubscribe. Nothing more is sold or promised on compliance.
On this page
Deliverability keeps the outbound channel healthy and the compliance surface honest. CogniLead leans on plain SMTP from pooled sender mailboxes for the transport itself, and owns three things on top of it: a warmup ramp for new sender domains, a reputation circuit-breaker that watches Google Postmaster Tools and pulls a domain out of rotation before it gets burned, and the two hygiene primitives outbound actually requires — a suppression list and one-click unsubscribe.
Warmup ramp
A new sender domain does not start at full volume. dailyCapFor(warmthDays) (lib/pipeline/stages/warming-engine.ts) reads a time-based curve — by default [5, 8, 12, 16, 22, 30, 40, 55, 70, 90] sends/day, indexed by the domain's warmth_days and plateauing at the last value — configurable via WARMING_DAILY_CAP_CURVE. That curve answers "how high COULD this domain ramp today if everything is healthy" — a ceiling, not a decision by itself.
Adaptive ramp (the sub-threshold zone)
adaptiveDailyCap() (lib/pipeline/stages/warming-cap.ts) fills the gap between "ramping fine" and "so bad the breaker pauses it": a domain whose reputation reads MEDIUM/UNKNOWN or whose complaints or bounces are creeping up but not yet breaker-grade. It classifies each domain into one of three states every pass, checked in order of severity so a single bad signal is enough to act — deliverability damage compounds and is expensive to undo:
- HEALTHY → advance. Reputation HIGH/MEDIUM, spam rate ≤ 0.1%, bounce rate ≤ 2%. Take the full curve value for the day and bump warmth_days.
- MARGINAL → hold. Reputation UNKNOWN/unset, spam rate between 0.1% and 0.3%, or bounce rate between 2% and 5%. Freeze today's cap at its current level and do NOT advance the curve — give the domain a cycle to recover before ramping further.
- DEGRADED → regress. Reputation LOW/BAD, spam rate above 0.3%, or bounce rate above 5% (any one of these is enough). Cut the cap to half the curve ceiling (WARMING_REGRESS_FACTOR, default 0.5), floored at the curve's first step so a domain is never stranded at a cap too low to ever recover its volume.
These thresholds are deliberately one notch softer than the hard breaker below (which pauses entirely at spam > 0.3%): the adaptive ramp starts decelerating at the same 0.3% and starts holding well before it, so a struggling domain glides into the brake instead of slamming into it. All five thresholds are overridable via WARMING_SPAM_RATE_HOLD_MAX, WARMING_SPAM_RATE_DEGRADE_MAX, WARMING_BOUNCE_RATE_HOLD_MAX, WARMING_BOUNCE_RATE_DEGRADE_MAX, and WARMING_REGRESS_FACTOR.
Reputation circuit-breaker
evaluateBreaker() (lib/pipeline/stages/reputation-breaker.ts) is the hard safety brake, driven by a Google Postmaster Tools reading for the domain. Defaults track Google's own documented thresholds: a spam_rate_max of 0.003 (0.3%) — above this Gmail already throttles the domain on its own — and a pause on domain_reputation of LOW or BAD. Either signal alone triggers a pause action; the caller stamps paused_at on the sender_domains row, and the DB-backed sender pool (§2) excludes any domain with paused_at set from being picked, within one pool-refresh cycle (default 30s).
Resume is deliberately conservative: a single clean reading is enough, but it must show domain_reputation HIGH or MEDIUM — an UNKNOWN reading (e.g. Postmaster returning REPUTATION_CATEGORY_UNSPECIFIED for a low-volume domain) is treated as non-actionable, so a paused domain stays paused rather than auto-resuming on an opaque signal. An already-paused domain that receives another bad reading is reclassified as a no-op rather than re-stamping paused_at, so the pause timestamp reflects when the problem started, not the most recent poll.
Suppression list
Each tenant has one global suppression list (repo().suppressions), keyed on email and/or domain. It is checked twice per lead's lifetime at minimum: once by intersect() before the lead is ever registered (§3), and again by dispatchStep() immediately before every step 1+ goes out, in case the address unsubscribed, bounced, or complained between steps. A hit at either point is a hard gate — the lead (or that specific step) is dropped, not just flagged. Entries can arrive from four sources: an unsubscribe, a hard/soft-repeat bounce, a manual API/dashboard entry, or (via the MCP suppress tool or POST /api/v1/suppressions) any of unsubscribe | bounce_hard | bounce_soft_repeat | reply_negative | gdpr_request | manual as the recorded reason. A suppression against one campaign suppresses the address across every campaign that tenant runs — there is no per-campaign scoping.
One-click unsubscribe
Every outbound message carries an RFC 8058 one-click unsubscribe: a List-Unsubscribe header with both an HTTPS link and a mailto: fallback, plus List-Unsubscribe-Post: List-Unsubscribe=One-Click so a compliant mail client (Gmail, Outlook, most modern clients) can act on a single click with no page load and no confirmation screen. The HTTPS branch verifies an HMAC-signed token and adds the address to the suppression list. The mailto: branch is handled by POST /api/v1/unsubscribe: a mailbox-provider inbound webhook posts { from, subject }, the token is extracted from the subject line (tolerant of a mail client mangling surrounding text), verified, and applied the same way — the route always returns 200 even on an unparseable token so the mailbox provider never retries forever over a client-side mangling issue.
Bounce and complaint tracking
Bounce/complaint notifications arrive at POST /api/webhooks/ses (relay sends, via Amazon SES → SNS) or POST /api/webhooks/mta (CogniLead-operated mail servers); both correlate back to the send row by Message-ID and write bounced_at plus a structured bounce_reason. Where that SNS wiring isn't configured, a send is fire-and-forget past sent_at — operators should watch their SMTP provider's own bounce dashboard and feed hard bounces into the suppression list.