Skip to content

Engagement

Look up how likely a recipient is to open or click your email, for any address.

The engagement score is platform-wide. It reflects how a mailbox has responded to all mail sent through Outbound, not only yours, so you can look up an address you have never mailed (a new signup, an imported lead, a CRM contact). Lookups use your API key like every other call.

You only ever see the score and its explanation. Nothing about other senders is returned: no counts of how often others mail the address and no indication of who they are. When you have mailed the address yourself, mailedByYou carries your own send history.

All examples assume you've set the API key in the constructor. For multi-tenant usage, pass { apiKey } as the last argument to any method. See Configuration.


Get One Address

ts
const { recipient } = await outbound.engagement.get('ada@example.com');

if (!recipient.known) {
  console.log('No history yet, treat as a new contact');
} else if (recipient.score === -1) {
  console.log(`Bad history: ${recipient.reasonLabel}`);
} else {
  console.log(`Score ${recipient.score} (${recipient.bandLabel})`);
}

The address is matched case-insensitively and surrounding whitespace is ignored.

Response

ts
{
  recipient: {
    email: 'ada@example.com',
    known: true,
    score: 63,
    band: 1,
    bandLabel: 'engaged',
    reason: 'engaged',
    reasonLabel: 'Engaging recently',
    overMailed: false,
    lastEngagedAt: '2026-09-08T13:23:42.106Z',
    scoreComputedAt: '2026-09-16T19:31:13.508Z',
    scoreVersion: 3,
    mailedByYou: {
      sends: 3,
      firstSentAt: '2026-08-17T15:43:11.705Z',
      lastSentAt: '2026-09-08T13:20:38.795Z'
    }
  }
}

An address Outbound has never mailed is not an error. It comes back with known: false and every score field set to null:

ts
{
  recipient: {
    email: 'brand-new@example.com',
    known: false,
    score: null,
    band: null,
    bandLabel: null,
    reason: null,
    reasonLabel: null,
    overMailed: null,
    lastEngagedAt: null,
    scoreComputedAt: null,
    scoreVersion: null,
    mailedByYou: null
  }
}

Possible Errors

ErrorStatusCause
BadRequestError400Address missing or not a valid email address
AuthenticationError401Invalid or missing API key

Get Many Addresses

Look up to 500 addresses in one request:

ts
const result = await outbound.engagement.bulkGet({
  emails: ['ada@example.com', 'Bob@Example.com', 'brand-new@example.com', 'not-an-email'],
});

for (const r of result.recipients) {
  console.log(r.email, r.known ? r.score : 'no history');
}

console.log(result.summary);
// { requested: 4, accepted: 3, rejected: 1, known: 2, unknown: 1 }

emails accepts an array or a comma-separated string.

Response

ts
{
  recipients: [
    { email: 'ada@example.com', known: true, score: 63, band: 1, bandLabel: 'engaged', /* ... */ },
    { email: 'bob@example.com', known: true, score: -1, band: 4, bandLabel: 'blocked', reason: 'complaint', /* ... */ },
    { email: 'brand-new@example.com', known: false, score: null, /* ... */ }
  ],
  rejected: [
    { email: 'not-an-email', reason: 'invalid_format' }
  ],
  summary: { requested: 4, accepted: 3, rejected: 1, known: 2, unknown: 1 }
}
FieldContents
recipientsOne record per valid address, in the order you sent them. Addresses are lowercased and duplicates are collapsed, so match results back on the lowercased address
rejected{ email, reason } for each address that failed validation. These were not looked up. Same reasons as suppressions
summaryCounts. requested is what you sent, before deduplication

A malformed address never fails the whole call; it lands in rejected.

Larger lists

Split anything above 500 addresses into chunks:

ts
import type { EngagementScore } from '@masters-union/outbound-sdk';

const scores = new Map<string, EngagementScore>();

for (let i = 0; i < emails.length; i += 500) {
  const { recipients } = await outbound.engagement.bulkGet({ emails: emails.slice(i, i + 500) });
  for (const r of recipients) scores.set(r.email, r);
}

const score = scores.get(someEmail.trim().toLowerCase());

Possible Errors

ErrorStatusCause
BadRequestError400emails missing or empty, or more than 500 addresses supplied
AuthenticationError401Invalid or missing API key

Reading the Score

The score runs from -1 to 100. Higher means more likely to engage. Three low values mean very different things, so always read score together with reason:

ScoreMeaning
-1Bad history. The mailbox marked a message as spam, hard bounced with no delivery since, or unsubscribed. reason says which
0Silence. Enough mail was delivered to judge, and it was never opened or clicked
50 in band no_dataUnknown. Too little mail delivered to judge yet. A neutral starting point, not a bad one
1 to 100 otherwiseEarned from engagement. Recent clicks count most, then recent opens, then the share of delivered mail that was opened or clicked

Bands

bandbandLabelMeaning
0no_dataNot enough delivered mail to judge
1engagedRecent opens or clicks, score 40 or above
2disengagedEnough mail delivered but little or no engagement, engagement that has gone stale, or an unsubscribe
3undeliverableHard bounced, nothing delivered since
4blockedMarked a message as spam

Reasons

reasonreasonLabelTypical score
complaintMarked a message as spam-1
hard_bounceHard bounced with no delivery since-1
unsubscribedUnsubscribed-1
never_engagedNever opened or clicked0
insufficient_evidenceToo little mail to judge yet50, or a provisional score while evidence builds up
engagedEngaging recently40 to 100
stale_engagementEngagement has gone staleBelow 40

Other fields

FieldMeaning
knownfalse when Outbound has never mailed this address. Every score field is then null
overMailedtrue when the address received more than 7 messages in the last 7 days (from all senders) without opening or clicking anything in the last 30 days. This lowers the score
lastEngagedAtMost recent open or click, from any sender. null if never
scoreComputedAtWhen this score was last calculated
scoreVersionVersion of the scoring formula that produced it
mailedByYouYour own history with this address: { sends, firstSentAt, lastSentAt }, or null if you have never mailed it

Good to know

  • The score never blocks a send. Only your suppression list stops mail to an address. A -1 from an unsubscribe may come from another sender's mail and does not add the address to your list. How you act on it is your decision.
  • Scores update continuously. They are recalculated shortly after each delivery, open, click, bounce or complaint, and re-checked nightly so old engagement fades.
  • Opens are a weaker signal than clicks. Some mail clients and security scanners open mail automatically, so a click weighs more than an open.
  • No alias merging. a.b@gmail.com, ab@gmail.com and ab+news@gmail.com are looked up as three separate addresses.

TypeScript

EngagementScore is a union on known, so checking it narrows every other field:

ts
import type { EngagementScore } from '@masters-union/outbound-sdk';

function isRisky(r: EngagementScore): boolean {
  if (!r.known) return false;
  return r.score === -1 || r.bandLabel === 'undeliverable';
}

Exported types: EngagementScore, KnownEngagementScore, UnknownEngagementScore, EngagementBand, EngagementBandLabel, EngagementReason, EngagementSendHistory, GetEngagementResponse, BulkGetEngagementParams, BulkGetEngagementResponse.

Released under the MIT License.