Skip to content
Preview: outbound-sdk is not on npm yet and its repository is private. Instantly and HeyReach are beta, Smartlead is unverified.What is verified
outbound-sdk
Esc
↑↓navigate↵open⌘Jpreview
On this page

Sender and deliverability insights

Reference for the six insights about senders (domainHealth, senderCapacity, bounceWatch, warmupReadiness, senderLoad, settingsAudit), with every input, what the answer holds and a sample that runs.

Six insights about the mailboxes and LinkedIn seats that send, and the settings campaigns send under. Each is a method on outbound.insights once the insights plugin is added. What every answer carries and how inputs are checked is on the overview.

These insights report. None of them pauses a sender, changes a limit or edits a setting.

The data the samples run on

One tool with five mailboxes on three sending domains, one LinkedIn seat and four active campaigns.

// Made-up data for one tool: five mailboxes on three sending domains, one
// LinkedIn seat, and four active campaigns. The domain send-two.example.com is
// in trouble, and "Agency owners" is set up carelessly.
import type { MemoryData, MemoryStats } from "outbound-sdk/memory";

const WEEKDAYS = [1, 2, 3, 4, 5];

const HEALTHY = {
  blacklisted: false,
  connected: true,
  dkim: true,
  dmarc: true,
  mx: true,
  spf: true,
};

// One row per day for the seven days before 2026-11-14.
const daily = (
  campaignId: string,
  counts: MemoryStats["counts"]
): MemoryStats[] =>
  Array.from({ length: 7 }, (_, index) => ({
    campaignId,
    counts,
    date: `2026-11-${String(7 + index).padStart(2, "0")}`,
  }));

export const seed: MemoryData = {
  campaigns: [
    {
      id: "founders",
      name: "SaaS founders",
      settings: {
        dailyLimit: 60,
        gapMinutes: 10,
        schedule: [
          {
            days: WEEKDAYS,
            from: "09:00",
            timezone: "America/New_York",
            to: "17:00",
          },
        ],
        stopOnReply: true,
        trackClicks: false,
        trackOpens: false,
      },
    },
    {
      id: "agencies",
      name: "Agency owners",
      settings: {
        dailyLimit: 200,
        endDate: "2026-10-31",
        gapMinutes: 2,
        schedule: [{ days: [...WEEKDAYS, 6], from: "06:00", to: "22:00" }],
        stopOnReply: false,
        trackClicks: true,
        trackOpens: true,
      },
    },
    // No settings at all: the tool reports nothing for this campaign.
    { id: "ecom", name: "Ecommerce ops" },
    {
      channel: "linkedin",
      id: "linkedin",
      name: "LinkedIn founders",
      settings: {
        dailyLimit: 20,
        schedule: [
          {
            days: WEEKDAYS,
            from: "09:00",
            timezone: "Europe/London",
            to: "17:00",
          },
        ],
        stopOnReply: true,
      },
    },
  ],
  senders: [
    {
      address: "ana@send-one.example.com",
      campaignIds: ["founders", "agencies", "ecom"],
      dailyLimit: 40,
      health: { ...HEALTHY, warmup: { sent: 200, spam: 4 } },
      id: "ana",
      warmup: { enabled: true, score: 98 },
    },
    {
      address: "ben@send-one.example.com",
      campaignIds: ["founders"],
      dailyLimit: 40,
      health: { ...HEALTHY, warmup: { sent: 200, spam: 6 } },
      id: "ben",
      warmup: { enabled: true, score: 95 },
    },
    {
      address: "cy@send-two.example.com",
      campaignIds: ["agencies"],
      dailyLimit: 30,
      health: {
        ...HEALTHY,
        dkim: false,
        dmarc: false,
        warmup: { sent: 100, spam: 18 },
      },
      id: "cy",
      warmup: { enabled: true, score: 72 },
    },
    {
      address: "dee@send-two.example.com",
      campaignIds: ["agencies"],
      dailyLimit: 30,
      health: {
        ...HEALTHY,
        blacklisted: true,
        connected: false,
        dkim: false,
        dmarc: false,
        issues: ["Sign-in failed"],
        warmup: { sent: 100, spam: 12 },
      },
      id: "dee",
      status: "disconnected",
      warmup: { enabled: false },
    },
    // A new mailbox: attached to nothing, and nothing reported about it.
    { address: "eli@send-three.example.com", id: "eli" },
    {
      campaignIds: ["linkedin"],
      dailyLimit: 20,
      health: { connected: true },
      id: "fay",
      linkedinUrl: "https://www.linkedin.com/in/fay-lin-example",
      name: "Fay Lin",
    },
  ],
  stats: [
    ...daily("founders", { bounced: 1, sent: 50 }),
    ...daily("agencies", { bounced: 3, sent: 40 }),
    ...daily("ecom", { bounced: 1, sent: 10 }),
    // The LinkedIn tool counts no bounces.
    ...daily("linkedin", { sent: 15 }),
    // What each sender of "Agency owners" sent in all its campaigns.
    {
      campaignId: "agencies",
      counts: { bounced: 6, sent: 420 },
      date: "2026-11-13",
      senderId: "ana",
    },
    {
      campaignId: "agencies",
      counts: { bounced: 12, sent: 150 },
      date: "2026-11-13",
      senderId: "cy",
    },
    {
      campaignId: "agencies",
      counts: { bounced: 9, sent: 90 },
      date: "2026-11-13",
      senderId: "dee",
    },
  ],
};
import { createOutbound } from "outbound-sdk";
import { insights } from "outbound-sdk/insights";
import { memory } from "outbound-sdk/memory";

import { seed } from "./seed.ts";

export const outbound = createOutbound({
  adapter: memory({ data: seed, workspaceId: "acme" }),
  plugins: [insights()],
});

domainHealth

Deliverability per sending domain: DNS records, sign-in, block lists and the share of warmup emails landing in spam, with a risk level and the reasons.

Input Default Range What it is
max_senders 50 1 to 500 The most senders to check.
spam_rate_threshold 0.1 0 to 1 A domain whose warmup emails land in spam more often than this share is high risk.

data (DomainHealth):

  • domains: one entry per domain, worst first, with risk (high, medium, low or unknown), reasons, mailboxes (each with address, status, connected, blacklisted, failingDns and the vendor’s own issues), failingDns, disconnected, blacklisted, warmup (pooled over the mailboxes that report it: mailboxes, sent, spam, spamRate, interval) and notReported.
  • linkedinSeats: seats have no domain. Each has name, linkedinUrl, status, connected and issues. Disconnected seats first.
  • mailboxesWithoutDomain: sender refs whose domain is not known.
  • totals: senders, domains, high, medium, low, unknown and linkedinSeats.

How the risk is set:

Risk When
high A mailbox is on a block list, SPF, DKIM or MX fails, or the pooled warmup spam rate is over the threshold.
medium DMARC fails, or a mailbox is disconnected.
low None of the above, and the vendor reported at least one check.
unknown The vendor reported nothing for any mailbox of the domain.
  • Needs: senders.list, senders.health.
  • Costs: one listing of senders, plus 1 request per sender.
  • Notes it can add: a cap was reached.
  • Does not: look up DNS itself. It reports what the vendor says about each mailbox. A check the vendor reports for no mailbox of a domain is named in notReported, and a domain with nothing reported is unknown, not low.
import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.domainHealth({});

for (const domain of data.domains) {
  console.log(`${domain.domain}: ${domain.risk} risk`);

  for (const reason of domain.reasons) {
    console.log(`  - ${reason}`);
  }

  if (domain.notReported.length > 0) {
    console.log(`  not reported: ${domain.notReported.join(", ")}`);
  }
}

console.log(
  "LinkedIn seats:",
  data.linkedinSeats.map((seat) => `${seat.name}, connected ${seat.connected}`)
);

console.log("totals:", data.totals);
send-two.example.com: high risk
  - 1 of 2 mailboxes are on a block list
  - DKIM failing
  - 30 of 200 warmup emails landed in spam (0.15, over 0.1)
  - DMARC failing
  - 1 of 2 mailboxes are disconnected
send-one.example.com: low risk
send-three.example.com: unknown risk
  not reported: spf, dkim, dmarc, mx, connection, blocklist, warmup
LinkedIn seats: [ 'Fay Lin, connected true' ]
totals: {
  domains: 3,
  high: 1,
  linkedinSeats: 1,
  low: 1,
  medium: 0,
  senders: 6,
  unknown: 1
}

senderCapacity

Whether the senders can send what the campaigns ask.

Input Default Range What it is
days 7 1 to 90 How many full days before today to average the sends over.
max_campaigns 25 1 to 200 The most active campaigns to read.
max_senders 200 1 to 1,000 The most senders to read for the workspace, and for each campaign.

data (SenderCapacity):

  • finding: the answer in a sentence or three.

  • capacity: daily (the daily limits of active senders added up), mailboxDaily, linkedinDaily, activeSenders, withLimit, withoutLimit and inactiveSenders.

  • ask: daily (the daily limits of active campaigns added up), campaigns, withLimit and withoutLimit. null when the adapter cannot read campaign settings.

  • sends: averageDaily, total, days, daysWithData, from, to and scope (workspace or campaigns). null when the adapter has no daily stats.

  • utilization: average daily sends divided by daily capacity.

  • askRatio: the daily ask divided by daily capacity. Over 1 means the campaigns ask for more than the senders can send.

  • shortCampaigns: campaigns whose own senders cannot cover their daily limit, with dailyLimit, senderCapacity, shortBy, senders, activeSenders and a finding. Largest gap first. null when it could not be checked.

  • uncheckedCampaigns: campaigns left out of shortCampaigns because a limit is not reported.

  • Needs: senders.list. It reads more when the adapter has campaigns.settings, senders.byCampaign and stats.byDay.

  • Costs: one listing of senders and one of active campaigns, then up to 2 requests per campaign and 1 for daily stats. Without workspace stats it reads daily stats per campaign: up to 3 per campaign.

  • Notes it can add: the daily ask and the short campaigns were left out because the adapter cannot read campaign settings; the short campaigns were left out because it cannot list a campaign’s senders; average daily sends were left out, or cover only the active campaigns read.

  • Does not: count a sender or campaign with no reported limit as 0. They are counted apart, in withoutLimit. In shortCampaigns, a sender shared with another campaign is counted in full for each, so a campaign can look covered while its senders are oversubscribed: askRatio shows that. Only active campaigns are read.

import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.senderCapacity({});

console.log(data.finding);

console.log("capacity:", data.capacity);

console.log("ask:", data.ask);

console.log(
  `utilization ${data.utilization}, ask ratio ${data.askRatio}, sends counted over ${data.sends?.daysWithData} days`
);

for (const campaign of data.shortCampaigns ?? []) {
  console.log(`short by ${campaign.shortBy}: ${campaign.finding}`);
}

console.log("campaigns not checked:", data.uncheckedCampaigns);
The 4 active senders that report a daily limit can send 130 a day; 1 more reports no limit and is not counted. Active campaigns ask for 280, which is 150 more than that. 115 a day were sent on average over the last 7 days.
capacity: {
  activeSenders: 5,
  daily: 130,
  inactiveSenders: 1,
  linkedinDaily: 20,
  mailboxDaily: 110,
  withLimit: 4,
  withoutLimit: 1
}
ask: { campaigns: 4, daily: 280, withLimit: 3, withoutLimit: 1 }
utilization 0.8846, ask ratio 2.1538, sends counted over 7 days
short by 130: Agency owners asks for 200 a day but its 2 active senders can send 70.
campaigns not checked: 1

Five senders are active, but one reports no daily limit, so the capacity of 130 is that of four, and the finding says so. “Ecommerce ops” reports no daily limit, so it is not checked.

bounceWatch

The campaigns whose bounce rate (bounced divided by sent) is over a threshold, and the senders behind them.

Input Default Range What it is
threshold 0.03 0 to 1 The bounce rate that is too high.
min_sent 100 0 or more, whole A campaign with fewer sends than this is too small to judge.
from none YYYY-MM-DD First day to count. Leave out for the vendor’s whole history.
to none YYYY-MM-DD Last day to count. Leave out for the vendor’s whole history.
include_senders true boolean Also check the senders of each campaign over the threshold.
max_campaigns 50 1 to 500 The most campaigns to check.
max_senders_per_campaign 10 1 to 200 The most senders to check for one campaign over the threshold.

data (BounceWatch):

  • over: campaigns over the threshold, worst first, each with bounced, sent, rate, interval, status, a finding, and senders: its senders that are themselves over the threshold (null when senders were not checked).

  • tooFewSends: campaigns with fewer sends than min_sent, whatever their rate.

  • unknown: campaigns the vendor reports no sent or bounce count for, with the reason.

  • totals: campaigns, over, clean, tooFewSends, unknown, and bounced, sent and rate over the campaigns with known counts.

  • threshold, minSent: what was used.

  • Needs: stats.campaign. Checking senders also needs stats.bySender and senders.byCampaign.

  • Costs: one listing of campaigns and 1 request per campaign, plus 1 listing and up to 1 request per sender for each campaign over the threshold.

  • Notes it can add: senders were not checked because the adapter needs stats per sender and the list of a campaign’s senders; a cap was reached.

  • Does not: judge a sender within one campaign. A sender’s rate is over everything it sent in the period, because no vendor reports one sender within one campaign. Every campaign is read, whatever its status. A campaign in tooFewSends or unknown is not clean.

import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.bounceWatch({});

for (const campaign of data.over) {
  console.log(`${campaign.name}: ${campaign.finding}`);
  console.log(
    `  95% interval: ${campaign.interval?.low} to ${campaign.interval?.high}`
  );

  for (const sender of campaign.senders ?? []) {
    console.log(
      `  ${sender.address}: ${sender.bounced} of ${sender.sent} (${sender.rate})`
    );
  }
}

for (const campaign of data.tooFewSends) {
  console.log(`${campaign.name}: ${campaign.finding}`);
}

for (const campaign of data.unknown) {
  console.log(`${campaign.name}: ${campaign.reason}`);
}

console.log("totals:", data.totals);
Agency owners: 21 of 280 sends bounced (0.075), over the 0.03 threshold.
  95% interval: 0.0496 to 0.1119
  dee@send-two.example.com: 9 of 90 (0.1)
  cy@send-two.example.com: 12 of 150 (0.08)
Ecommerce ops: 7 of 70 sends bounced; under 100 sends is too few to judge.
LinkedIn founders: The vendor does not report a bounce count.
totals: {
  bounced: 35,
  campaigns: 4,
  clean: 1,
  over: 1,
  rate: 0.05,
  sent: 700,
  tooFewSends: 1,
  unknown: 1
}

The third sender of “Agency owners”, ana@send-one.example.com, bounced 6 of 420. That is under the threshold, so that sender is not listed.

warmupReadiness

Which senders are not ready to send, each with its reasons.

Input Default Range What it is
min_score 90 0 to 100 The warmup score a mailbox needs to be ready.
check_health false boolean Also check each mailbox’s DNS records and warmup spam.
max_spam_rate 0.1 0 to 1 With check_health, a mailbox whose warmup emails land in spam more often than this share is not ready.
max_senders 100 1 to 1,000 The most senders to check.

data (WarmupReadiness):

  • notReady: the senders with at least one reason, most reasons first. Each has kind, address, name, status, warmupEnabled, score, dailyLimit, reasons and findings (each reason in plain words).
  • byReason: how many senders have each reason.
  • ready, total, linkedinSeats.
  • healthChecked: true when DNS records and warmup spam were checked too.

The reasons:

Reason When
not_active The sender’s status is not active.
warmup_off The vendor says warmup is off.
score_low The warmup score is under min_score.
score_unknown The vendor reports no warmup score.
no_daily_limit No daily limit is set or reported.
dns_failing With check_health: a DNS record fails.
warmup_spam_high With check_health: warmup spam is over max_spam_rate.
  • Needs: senders.list. check_health also needs senders.health.
  • Costs: one listing of senders, plus 1 request per mailbox with check_health.
  • Notes it can add: DNS records and warmup spam were not checked because the adapter cannot read a sender’s health; a cap was reached.
  • Does not: treat a missing score as 0. A null score means the vendor does not report one, which counts as not ready under its own reason. A LinkedIn seat is judged on its status only.
import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.warmupReadiness({
  check_health: true,
});

for (const sender of data.notReady) {
  console.log(`${sender.address}:`);

  for (const finding of sender.findings) {
    console.log(`  - ${finding}`);
  }
}

console.log(`ready: ${data.ready} of ${data.total}`);

console.log("by reason:", data.byReason);
dee@send-two.example.com:
  - Status is disconnected
  - Warmup is off
  - The vendor reports no warmup score
  - DKIM, DMARC failing
  - 12 of 100 warmup emails landed in spam (0.12, over 0.1)
cy@send-two.example.com:
  - Warmup score 72 is under 90
  - DKIM, DMARC failing
  - 18 of 100 warmup emails landed in spam (0.18, over 0.1)
eli@send-three.example.com:
  - The vendor reports no warmup score
  - No daily limit is set or reported
ready: 3 of 6
by reason: {
  dns_failing: 2,
  no_daily_limit: 1,
  not_active: 1,
  score_low: 1,
  score_unknown: 2,
  warmup_off: 1,
  warmup_spam_high: 2
}

senderLoad

Senders that are overused or unused, and campaigns that depend on too few senders.

Input Default Range What it is
max_campaigns_per_sender 3 1 or more, whole A sender attached to more campaigns than this is overused.
statuses ["active"] at least one campaign status Campaign statuses to look at.
max_campaigns 25 1 to 200 The most campaigns to read.
max_senders 200 1 to 1,000 The most senders to read for the workspace, and for each campaign.

data (SenderLoad):

  • overused: senders in more campaigns than allowed, most loaded first, with campaigns, over, campaignRefs and a finding.

  • unused: active senders attached to none of the campaigns read.

  • campaigns: campaigns with an issue (no_sender, single_sender or inactive_sender), with senders, issues, the inactive senders and a finding.

  • totals: senders, activeSenders, campaigns, overused, unused and campaignsAtRisk.

  • maxCampaignsPerSender: what was used.

  • Needs: senders.list, senders.byCampaign.

  • Costs: one listing of senders and one of campaigns, then 1 request per campaign.

  • Notes it can add: a cap was reached.

  • Does not: count campaigns outside statuses. A sender used only by a paused campaign shows as unused unless paused is included. It counts campaigns, not volume: use senderCapacity for that.

import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.senderLoad({
  max_campaigns_per_sender: 2,
});

for (const sender of data.overused) {
  console.log("overused:", sender.finding);
}

console.log(
  "unused:",
  data.unused.map((sender) => sender.address)
);

for (const campaign of data.campaigns) {
  console.log(`${campaign.issues.join(", ")}: ${campaign.finding}`);
}

console.log("totals:", data.totals);
overused: ana@send-one.example.com is attached to 3 campaigns, 1 over the limit of 2.
unused: [ 'eli@send-three.example.com' ]
inactive_sender: Agency owners has 3 senders, 1 of them not active.
single_sender: Ecommerce ops has 1 sender.
single_sender: LinkedIn founders has 1 sender.
totals: {
  activeSenders: 5,
  campaigns: 4,
  campaignsAtRisk: 3,
  overused: 1,
  senders: 6,
  unused: 1
}

settingsAudit

Risky campaign settings, per campaign and by severity.

Input Default Range What it is
statuses ["active"] at least one campaign status Campaign statuses to audit.
max_window_hours 12 1 to 24 A sending window longer than this is flagged.
min_window_hours 2 0 to 24 A sending window shorter than this is flagged.
min_gap_minutes 5 0 or more A wait between two sends shorter than this is flagged.
max_daily_limit_per_sender 50 1 or more A campaign’s daily limit should not exceed this many sends per attached sender.
max_campaigns 25 1 to 200 The most campaigns to audit.
max_senders_per_campaign 100 1 to 1,000 The most senders to count for one campaign.

data (SettingsAudit):

  • campaigns: the campaigns with at least one finding, most serious first. Each has name, status and findings (each with check, severity, a finding sentence and the value behind it).
  • byCheck: how many campaigns each check flagged.
  • totals: campaigns, withFindings, clean, and the number of high, medium and low findings.

The checks:

Check Severity Fires when
limit_over_senders high The daily limit is over max_daily_limit_per_sender times the number of attached senders.
end_date_passed high The campaign is active but its end date has passed.
reply_does_not_stop high A lead who replies keeps getting the next steps.
no_window medium No sending window is set or reported.
window_long medium A window is longer than max_window_hours.
no_daily_limit medium No daily limit is set or reported.
gap_short medium Sends are closer together than min_gap_minutes.
open_tracking medium Open tracking is on.
click_tracking medium Click tracking is on.
weekend low A window includes Saturday or Sunday.
window_short low A window is shorter than min_window_hours.
no_timezone low A window has no timezone.
  • Needs: campaigns.settings. Comparing the daily limit with the sender count also needs senders.byCampaign.
  • Costs: one listing of campaigns, then 1 request per campaign, or 2 when its senders can be listed.
  • Notes it can add: the daily limit was not compared with the sender count because the adapter cannot list a campaign’s senders; a cap was reached.
  • Does not: tell a missing setting from one that is not set, for a window, a timezone or a daily limit: the vendor cannot tell them apart either, so all three are flagged. Any other setting the vendor reports nothing for is skipped. The severities are fixed.
import { outbound } from "./outbound.ts";

const { data } = await outbound.insights.settingsAudit({});

for (const campaign of data.campaigns) {
  console.log(`${campaign.name}:`);

  for (const finding of campaign.findings) {
    console.log(`  ${finding.severity} ${finding.check}: ${finding.finding}`);
  }
}

console.log("totals:", data.totals);
Agency owners:
  high limit_over_senders: The daily limit of 200 is over 50 a day for each of its 3 senders (150).
  high end_date_passed: The campaign is active but its end date 2026-10-31 has passed.
  high reply_does_not_stop: A lead who replies keeps getting the next steps.
  medium window_long: A sending window is 16 hours long, over 12.
  medium gap_short: Sends are 2 minutes apart, under 5.
  medium open_tracking: Open tracking is on, a deliverability risk.
  medium click_tracking: Click tracking is on; it rewrites links, a deliverability risk.
  low weekend: A sending window includes Saturday or Sunday.
  low no_timezone: A sending window has no timezone.
Ecommerce ops:
  medium no_window: No sending window is set or reported, so it can send at any hour.
  medium no_daily_limit: No daily limit is set or reported.
totals: { campaigns: 4, clean: 2, high: 3, low: 2, medium: 6, withFindings: 2 }

“Ecommerce ops” has no settings in the data, so it gets the two findings a tool that reports nothing would get.

Was this page helpful?