---
title: See everyone you have contacted at one company before a call, from its domain
description: One function that takes a company domain and returns each person at it across campaigns and tools, with who replied, who the vendor marked interested and who replied last. People are matched by exact email domain only.
sidebar:
  label: Account brief
related:
  - /docs/insights/people
  - /docs/adapters/combine
  - /docs/guides/person-across-email-and-linkedin
---

Ten minutes before a call with someone at a company, you want to know who else there you have written to, and what came back. `accountSummary` takes the company's domain, or any email address at it, and returns each person whose address is at that domain: name, title, the campaigns they are in, their status and interest label in each, and open, click and reply counts. It adds totals and the most recent reply.

It matches on the exact email domain and nothing else. A lead the tool knows only by a LinkedIn profile is not found, even when its company field names the company. The answer says how many leads it read that had no address.

> **Preview: not published yet**
>
> `outbound-sdk` is not on npm, and its GitHub repository is private, so there
> is no install command that works today. Instantly and HeyReach are `beta`:
> their reads were run, read-only, against one live workspace each. Smartlead is
> `unverified`: written from its public documentation and tested offline only.
> See [what is verified](/docs/status).

## Before you start

- Node 22 or later.
- The code below runs on the in-memory adapter, so it needs no vendor key. Each sample on this page is a file in the repository that the test suite runs; the output shown is what it printed.
- For real data: an API key for each tool. A read-only key is enough. No method of the SDK writes to a vendor.

## What you get

```text title="Output"
northwind.example: 3 people in 3 campaigns, 2 replied, 1 marked interested
Last reply: Ava Chen on 2026-11-12

  Ava Chen <ava.chen@northwind.example>, CEO
    replied, interested; replies 2, clicks 1, opens 7
    in mailtool:agency:em_founders: replied, Interested
    in mailtool:agency:em_ops: active
    in lntool:agency:li_execs: active
  Ben Ortiz <ben.ortiz@northwind.example>, Head of Operations
    replied, labelled, not as interested; replies 1, clicks 0, opens 3
    in mailtool:agency:em_ops: replied, Not Interested
  Cy Dunn <cy.dunn@northwind.example>
    no reply, no label; replies 0, clicks 0, opens 1
    in mailtool:agency:em_founders: active

Read 7 leads. 1 have no email address, so a domain cannot match them.
```

## Write the brief

```ts title="brief.ts"
import type { Outbound } from "outbound-sdk";
import type { InsightsExtension } from "outbound-sdk/insights";

// A count the vendor does not report is null, never 0. Say so.
const count = (value: number | null): string =>
  value === null ? "not reported" : String(value);

// `interested` is null when the person carries no label at all.
const interest = (interested: boolean | null): string => {
  if (interested === null) {
    return "no label";
  }

  return interested ? "interested" : "labelled, not as interested";
};

/** Everyone the instance holds at one company domain, as text. */
export const accountBrief = async (
  outbound: Outbound & InsightsExtension,
  domain: string
): Promise<string> => {
  const { data, meta } = await outbound.insights.accountSummary({ domain });
  const { lastReply, totals } = data;

  const lines = [
    `${data.domain}: ${totals.people} people in ${totals.campaigns} campaigns, ` +
      `${totals.replied} replied, ${totals.interested} marked interested`,
    lastReply === null
      ? "No reply with a known time."
      : `Last reply: ${lastReply.name ?? lastReply.email} on ${lastReply.at.slice(0, 10)}`,
    "",
  ];

  // Most recent reply first. That order comes from the insight.
  for (const person of data.people) {
    lines.push(
      `  ${person.name ?? "No name"} <${person.email}>${
        person.title === null ? "" : `, ${person.title}`
      }`,
      `    ${person.replied ? "replied" : "no reply"}, ${interest(person.interested)}; ` +
        `replies ${count(person.replies)}, clicks ${count(person.clicks)}, opens ${count(person.opens)}`,
      ...person.records.map(
        (record) =>
          `    in ${record.campaign ?? "no campaign"}: ${record.status}${record.interest === null ? "" : `, ${record.interest}`}`
      )
    );
  }

  lines.push(
    "",
    `Read ${totals.leadsRead} leads. ${totals.leadsWithoutEmail} have no email address, so a domain cannot match them.`
  );

  if (!meta.complete || meta.notes.length > 0) {
    lines.push("", ...meta.notes.map((note) => `Note: ${note}`));
  }

  return lines.join("\n");
};
```

What the insight does:

1. It reads leads: from the tool's list of every lead when the adapter has one, otherwise campaign by campaign, up to `max_campaigns` campaigns. Either way it stops at `max_leads` (default 1,000).
2. It keeps the leads whose address is at the domain, and groups them by address. One person in three campaigns is one entry with three `records`.
3. For each person it adds up the counts over their records, and takes the first known name, title and company.
4. It sorts people by most recent reply, then people who replied at an unknown time, then the rest by address.

Three things in the answer are worth copying:

- **`interested` has three values.** `true` when one of the person's labels is a positive one, `false` when they carry a label and none is positive, `null` when they carry no label. "No label" is not "not interested".
- **A count is `null` when the vendor reports none.** A sum over records ignores the unknown ones: Ava's LinkedIn record has no counts, and her totals come from the two email records.
- **`totals.leadsWithoutEmail` tells you what the domain could not see.** One lead in the output above has no address. That is Eve, who works at the company.

Records name their campaign by ref. To print a campaign's name, read it with `outbound.campaign(ref).get()`, which is one request each.

## Try it without a key

Two made-up tools and four people at one company, one of them known by profile only.

```ts title="seed.ts"
// Two made-up tools for one agency, with four people at one company. The
// company's domain is northwind.example: `.example` is reserved, so it is
// nobody's real domain.
import type { MemoryData } from "outbound-sdk/memory";

export const DOMAIN = "northwind.example";

export const emailTool: MemoryData = {
  campaigns: [
    { id: "em_founders", name: "Founders Q4" },
    { id: "em_ops", name: "Ops leaders" },
  ],
  leads: [
    // Ava is in both email campaigns. She replied twice in the first.
    {
      campaignId: "em_founders",
      company: "Northwind",
      email: "ava.chen@northwind.example",
      engagement: {
        clicks: 1,
        lastContactedAt: "2026-11-10T09:00:00.000Z",
        lastRepliedAt: "2026-11-12T15:00:00.000Z",
        opens: 5,
        replies: 2,
      },
      firstName: "Ava",
      id: "f_ava",
      interest: "Interested",
      lastName: "Chen",
      status: "replied",
      title: "CEO",
    },
    {
      campaignId: "em_ops",
      email: "ava.chen@northwind.example",
      engagement: { clicks: 0, opens: 2, replies: 0 },
      id: "o_ava",
    },
    // Ben replied once and was labelled not interested.
    {
      campaignId: "em_ops",
      company: "Northwind",
      email: "ben.ortiz@northwind.example",
      engagement: {
        clicks: 0,
        lastRepliedAt: "2026-11-05T09:00:00.000Z",
        opens: 3,
        replies: 1,
      },
      firstName: "Ben",
      id: "o_ben",
      interest: "Not Interested",
      lastName: "Ortiz",
      status: "replied",
      title: "Head of Operations",
    },
    // Cy opened one email. No label.
    {
      campaignId: "em_founders",
      email: "cy.dunn@northwind.example",
      engagement: { clicks: 0, opens: 1, replies: 0 },
      firstName: "Cy",
      id: "f_cy",
      lastName: "Dunn",
    },
    // Someone at another company.
    { campaignId: "em_founders", email: "dee@tailspin.example", id: "f_dee" },
  ],
};

export const linkedinTool: MemoryData = {
  campaigns: [
    { channel: "linkedin", id: "li_execs", name: "LinkedIn executives" },
  ],
  leads: [
    // The LinkedIn tool holds Ava's address, so her record here joins the rest.
    {
      campaignId: "li_execs",
      email: "ava.chen@northwind.example",
      id: "p_ava",
      linkedinUrl: "https://www.linkedin.com/in/ava-chen-example",
    },
    // Eve works at Northwind, but the tool has her profile only. A domain
    // cannot find her.
    {
      campaignId: "li_execs",
      company: "Northwind",
      firstName: "Eve",
      id: "p_eve",
      linkedinUrl: "https://www.linkedin.com/in/eve-marsh-example",
    },
  ],
};
```

```ts title="run.ts"
import { createOutbound } from "outbound-sdk";
import { combine } from "outbound-sdk/combine";
import { insights } from "outbound-sdk/insights";
import { memory } from "outbound-sdk/memory";

import { accountBrief } from "./brief.ts";
import { DOMAIN, emailTool, linkedinTool } from "./seed.ts";

// Two tools behind one instance. `name` stands in for the vendor.
const outbound = createOutbound({
  adapter: combine([
    memory({ data: emailTool, name: "mailtool", workspaceId: "agency" }),
    memory({ data: linkedinTool, name: "lntool", workspaceId: "agency" }),
  ]),
  plugins: [insights()],
});

// An email address works too: its domain is used.
console.log(await accountBrief(outbound, DOMAIN));
```

That is the script that printed the brief above.

## Point it at your vendor

Swap the adapters. Nothing else changes. One tool works too.

```ts title="outbound.ts"
import { createOutbound } from "outbound-sdk";
import { combine } from "outbound-sdk/combine";
import { heyreach } from "outbound-sdk/heyreach";
import { insights } from "outbound-sdk/insights";
import { instantly } from "outbound-sdk/instantly";

// Reads INSTANTLY_API_KEY and HEYREACH_API_KEY. One tool works too: pass
// its adapter alone.
export const outbound = createOutbound({
  adapter: combine([
    instantly({ workspaceId: "agency" }),
    heyreach({ workspaceId: "agency" }),
  ]),
  // One brief may spend at most 60 vendor requests.
  maxRequests: 60,
  plugins: [insights()],
});
```

A combined instance promises only what every tool behind it can do. With HeyReach in it, there is no list of every lead, so leads are read campaign by campaign, and the answer carries a note that names the tools whose records have no per-lead counts. Records from a tool that does report counts still carry them.

## What each vendor can serve

The insight, run on the in-memory adapter limited to what each vendor's adapter declares. This shows which calls a vendor's adapter would refuse and which notes it adds. It does not show what that vendor's API returns.

```ts title="vendors.ts"
import { createOutbound, OutboundError } from "outbound-sdk";
import { heyreach } from "outbound-sdk/heyreach";
import { insights } from "outbound-sdk/insights";
import { instantly } from "outbound-sdk/instantly";
import { memory } from "outbound-sdk/memory";
import { smartlead } from "outbound-sdk/smartlead";

import { VENDORS } from "../_shared/as-vendor.ts";
import { DOMAIN, emailTool } from "./seed.ts";

// What each vendor's adapter can do once an instance has resolved it: a
// capability the adapter leaves out is `false` here. This guide turns on
// `leads.engagement`, which Smartlead's adapter leaves out.
const DECLARED = {
  heyreach: () =>
    createOutbound({ adapter: heyreach({ apiKey: "not-used" }) }).capabilities,
  instantly: () =>
    createOutbound({ adapter: instantly({ apiKey: "not-used" }) }).capabilities,
  smartlead: () =>
    createOutbound({ adapter: smartlead({ apiKey: "not-used" }) }).capabilities,
};

// Counts are not printed here: the in-memory data has counts a real vendor
// may not report. Only what is refused, and what is noted, carries over.
for (const vendor of VENDORS) {
  const outbound = createOutbound({
    adapter: memory({
      capabilities: DECLARED[vendor](),
      data: emailTool,
      name: vendor,
      workspaceId: "agency",
    }),
    plugins: [insights()],
  });

  try {
    // oxlint-disable-next-line no-await-in-loop -- one vendor at a time.
    const { meta } = await outbound.insights.accountSummary({
      domain: DOMAIN,
    });

    console.log(
      `${vendor}: ok, leads read ${outbound.capabilities["leads.list"] ? "from one list" : "campaign by campaign"}`
    );

    for (const note of meta.notes) {
      console.log(`  note: ${note}`);
    }
  } catch (error) {
    if (!OutboundError.is(error)) {
      throw error;
    }

    console.log(`${vendor}: ${error.code}: ${error.message}`);
  }
}
```

```text title="Output"
instantly: ok, leads read from one list
smartlead: ok, leads read from one list
  note: This tool reports no per-lead open, click or reply counts, so those are null.
heyreach: ok, leads read campaign by campaign
  note: This tool reports no per-lead open, click or reply counts, so those are null.
```

No adapter refuses the call: it needs leads by list or by campaign, and each has one of them. The Smartlead and HeyReach adapters report no per-lead open, click or reply counts, so those are `null` for them and the brief shows who is in which campaign, their status and their label. On HeyReach a lead is found only when HeyReach holds an email address for it.

## What it costs

One request per page of leads read. `max_leads` (default 1,000, at most 20,000) sets the ceiling, and a vendor request returns about 50 to 100 leads, so a full read at the default is roughly 10 to 20 requests. It reads every lead to find the few at the domain: there is no lookup by domain. When leads are read campaign by campaign, add one request per page of campaigns; `max_campaigns` defaults to 20. `maxRequests` on the instance caps the whole call; past it the call fails with `RateLimited` instead of spending your vendor quota.

## When it cannot answer

```ts title="failures.ts"
import { createOutbound, OutboundError } from "outbound-sdk";
import { insights } from "outbound-sdk/insights";
import { memory } from "outbound-sdk/memory";

import { DOMAIN, emailTool } from "./seed.ts";

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

// 1. An email provider's domain is not one company.
try {
  await outbound.insights.accountSummary({ domain: "someone@gmail.com" });
} catch (error) {
  if (OutboundError.is(error)) {
    console.log("free domain:", error.code, "-", error.message);
  }
}

// 2. A company name is not a domain. Nothing is matched by name.
try {
  await outbound.insights.accountSummary({ domain: "Northwind" });
} catch (error) {
  if (OutboundError.is(error)) {
    console.log("a name:", error.code, "-", error.message);
  }
}

// 3. Nobody at the domain. That is an answer, not an error.
const nobody = await outbound.insights.accountSummary({
  domain: "initech.example",
});

console.log(
  "nobody:",
  nobody.data.totals.people,
  "people in",
  nobody.data.totals.leadsRead,
  "leads read"
);

// 4. max_leads stopped the read: people past the cap are missing.
const capped = await outbound.insights.accountSummary({
  domain: DOMAIN,
  max_leads: 2,
});

console.log(
  "capped:",
  capped.data.totals.people,
  capped.meta.complete,
  capped.meta.notes
);

// 5. An adapter that can list leads neither way refuses the insight.
const noLeads = createOutbound({
  adapter: memory({
    capabilities: { "leads.byCampaign": false, "leads.list": false },
    data: emailTool,
  }),
  plugins: [insights()],
});

try {
  await noLeads.insights.accountSummary({ domain: DOMAIN });
} catch (error) {
  if (OutboundError.is(error)) {
    console.log("no leads:", error.code, "-", error.message);
  }
}
```

```text title="Output"
free domain: Invalid - gmail.com is an email provider, not one company. Pass allow_free_domains: true to read it anyway.
a name: Invalid - "Northwind" is not a domain or an email address
nobody: 0 people in 5 leads read
capped: 1 false [ 'Stopped at 2 leads; more exist.' ]
no leads: Unsupported - account_summary is not available: the memory adapter supports neither leads.list nor leads.byCampaign
```

| What happened | What you get | What to do |
| --- | --- | --- |
| The domain is an email provider such as `gmail.com` | `Invalid`: it is not one company | Pass the company's own domain, or `allow_free_domains: true` if you mean it. |
| Text that is not a domain or an address, such as a company name | `Invalid`, before any request | Pass `acme.com`, an address at it, or its URL. |
| Nobody at the domain | `people` is empty and `totals.leadsRead` says how many leads were looked at | Check the spelling, and check `meta.complete`. |
| More leads than `max_leads`, or more campaigns than `max_campaigns` | `meta.complete` is `false` and a note says where it stopped. People past the cap are missing. | Raise the cap. |
| The adapter can list leads neither way | `Unsupported`, before any request | Use an adapter that declares `leads.list` or `leads.byCampaign`. |
| A rejected key | `Unauthorized`. It is not retried. | Check the key and its scope. |

## Limits

- Exact domain only. `acme.com` does not find people at `eu.acme.com` or `acme.co.uk`, and a person who signed up with a personal address is not at the company as far as this goes.
- LinkedIn-only leads are never found. `totals.leadsWithoutEmail` counts every lead without an address that was read, at any company.
- When leads are read campaign by campaign, every campaign is read whatever its status, in the order the vendor lists them, until a cap is reached.
- `lastReply` is the most recent reply with a known time. A person the vendor marks as replied with no time is counted in `totals.replied` and cannot be `lastReply`.
- On an `unverified` adapter, check a brief against the vendor's own lead view before a call.
