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
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
{
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:
{
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
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | Address missing or not a valid email address |
AuthenticationError | 401 | Invalid or missing API key |
Get Many Addresses
Look up to 500 addresses in one request:
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
{
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 }
}| Field | Contents |
|---|---|
recipients | One 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 |
summary | Counts. 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:
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
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | emails missing or empty, or more than 500 addresses supplied |
AuthenticationError | 401 | Invalid 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:
| Score | Meaning |
|---|---|
-1 | Bad history. The mailbox marked a message as spam, hard bounced with no delivery since, or unsubscribed. reason says which |
0 | Silence. Enough mail was delivered to judge, and it was never opened or clicked |
50 in band no_data | Unknown. Too little mail delivered to judge yet. A neutral starting point, not a bad one |
1 to 100 otherwise | Earned from engagement. Recent clicks count most, then recent opens, then the share of delivered mail that was opened or clicked |
Bands
band | bandLabel | Meaning |
|---|---|---|
0 | no_data | Not enough delivered mail to judge |
1 | engaged | Recent opens or clicks, score 40 or above |
2 | disengaged | Enough mail delivered but little or no engagement, engagement that has gone stale, or an unsubscribe |
3 | undeliverable | Hard bounced, nothing delivered since |
4 | blocked | Marked a message as spam |
Reasons
reason | reasonLabel | Typical score |
|---|---|---|
complaint | Marked a message as spam | -1 |
hard_bounce | Hard bounced with no delivery since | -1 |
unsubscribed | Unsubscribed | -1 |
never_engaged | Never opened or clicked | 0 |
insufficient_evidence | Too little mail to judge yet | 50, or a provisional score while evidence builds up |
engaged | Engaging recently | 40 to 100 |
stale_engagement | Engagement has gone stale | Below 40 |
Other fields
| Field | Meaning |
|---|---|
known | false when Outbound has never mailed this address. Every score field is then null |
overMailed | true 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 |
lastEngagedAt | Most recent open or click, from any sender. null if never |
scoreComputedAt | When this score was last calculated |
scoreVersion | Version of the scoring formula that produced it |
mailedByYou | Your 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
-1from 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.comandab+news@gmail.comare looked up as three separate addresses.
TypeScript
EngagementScore is a union on known, so checking it narrows every other field:
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.