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

Collections

The five read methods every collection shares, the filters each collection takes and the capability each filter needs, lookups by email or profile, message bodies, and bulk reads where a missing record is data.

A collection is a kind of record you can list: outbound.campaigns, outbound.leads and eight more. All ten have list and listAll. Campaigns, senders and leads also have get, exists and getMany.

The methods

Method Returns Notes
list(options?) Promise<PageResult<Item>> One page: items, cursor, complete and meta.
listAll(options?) ListAll<Item> An async iterator over every page. Pages are fetched as you iterate, under the rate budget.
get(id, options?) Promise<WithMeta<Item>> One record with meta on it. Throws NotFound for a missing record.
exists(id, options?) Promise<boolean> false for a missing record. Any other failure still throws.
getMany(ids, options?) Promise<BulkResult<Item>> Several records at once. A record that fails is listed, not thrown.

options on list and listAll holds the collection’s filters, the paging options (limit, cursor, and max on listAll) and the per-call options (timeout, retries, deadline, maxRequests, signal). Paging is covered on the pagination page and the per-call options on the retries page.

An id is the vendor’s own id or the ref of a record. On a combined instance it must be a ref.

import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";

const outbound = createOutbound({ adapter: memory({ data: demoData() }) });

// One page. Filters and paging options go in the same object.
const active = await outbound.campaigns.list({ limit: 10, status: "active" });

console.log(
  "active:",
  active.items.map((campaign) => campaign.name)
);

// `since` and `until` are inclusive ISO 8601 dates or timestamps.
const recent = await outbound.leads.list({
  campaignId: "cmp_founders",
  since: "2026-10-03",
});

console.log(
  "added since 3 October:",
  recent.items.map((lead) => lead.email)
);

const senders = await outbound.senders.list({ campaignId: "cmp_agencies" });

console.log(
  "senders on cmp_agencies:",
  senders.items.map((sender) => sender.address)
);

const thread = await outbound.messages.list({ threadId: "thr_1" });

console.log(
  "thr_1:",
  thread.items.map((message) => message.direction)
);

// The collections with no filters.
const lists = await outbound.lists.list();
const tags = await outbound.tags.list();
const blocked = await outbound.blocklist.list();
const webhooks = await outbound.webhooks.list();
const workspaces = await outbound.workspaces.list();

console.log(
  lists.items[0]?.name,
  "|",
  tags.items[0]?.name,
  "|",
  blocked.items[0]?.value,
  "|",
  webhooks.items[0]?.events,
  "|",
  workspaces.items[0]?.name
);
active: [ 'Q4 SaaS founders' ]
added since 3 October: [ 'person2@example.com' ]
senders on cmp_agencies: [ 'outreach2@example.com' ]
thr_1: [ 'out', 'in' ]
SaaS founders | client-a | competitor.example | [ 'reply_received' ] | default

Filters

A filter the adapter cannot serve is refused with Unsupported before any request. The capability column says which key decides that. Check outbound.capabilities first when your code must run on more than one vendor: see capabilities.

campaigns

Filter Type Needs
status "active", "paused", "draft", "completed", "archived" or "unknown" campaigns.status

campaigns.list and campaigns.get need no capability. Every adapter has them.

senders

Filter Type Needs
none senders.list
campaignId string senders.byCampaign, and not senders.list

senders.get needs senders.get.

leads

Filter Type Needs
none leads.list
campaignId string leads.byCampaign, and not leads.list
listId string lists.leads, and not leads.list
email string leads.lookup
linkedinUrl string leads.lookup
since, until ISO 8601 date or timestamp, inclusive leads.since

campaignId and listId together are refused with Invalid. The date filters compare a lead’s createdAt. leads.get needs leads.get.

Some vendors have no list of every lead: leads exist only inside a campaign. That is why the scoped listings do not depend on leads.list.

messages

Filter Type Needs
any messages.list
campaignId string
leadId string
threadId string, the threadId of an earlier message messages.byThread
since, until ISO 8601 date or timestamp, inclusive messages.since
include: ["body"] messages.body

The date filters compare sentAt.

replies

Filter Type Needs
any replies.list
campaignId string
category "positive", "negative", "neutral", "ooo", "bounce", "unsubscribe" or "unknown" replies.category
since, until ISO 8601 date or timestamp, inclusive replies.since
include: ["body"] messages.body

The rest

Collection Filter Needs
lists none lists.list
tags none tags
blocklist none blocklist
webhooks campaignId webhooks.list
workspaces none workspaces

Dates

since and until take anything Date.parse reads, such as 2026-10-03 or 2026-10-03T08:00:00Z. Both ends are inclusive, and the SDK turns each into a UTC timestamp before the adapter sees it. A value it cannot read, or a since after until, is Invalid.

Where a vendor cannot filter by date, the adapter declares the filter as client. The SDK then filters each page after fetching it and marks the result derived. A page of such a listing can hold fewer records than you asked for, or none, while more pages remain. See filter grades.

Lookups

email and linkedinUrl on leads find one person without walking every lead. They match the exact address or profile. The SDK trims the value and lowercases an email before the lookup.

import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";

const outbound = createOutbound({ adapter: memory({ data: demoData() }) });

// A lookup by exact address. The SDK trims and lowercases it first.
const found = await outbound.leads.list({ email: " Person1@Example.com " });

for (const lead of found.items) {
  console.log(lead.ref, lead.campaignRef, lead.status);
}

const nobody = await outbound.leads.list({ email: "nobody@example.com" });

console.log("no match:", nobody.items.length, nobody.complete);
memory:default:lead_1 memory:default:cmp_founders replied
no match: 0 true
  • A lookup returns one record per campaign the person is in, so one address can come back several times. Each record has its own campaignRef.
  • A lookup alone needs leads.lookup and not leads.list. Add campaignId or listId to narrow it.
  • No match is an empty page, not an error.

What a vendor matches on is an adapter fact. The adapter pages say which of the two each vendor can look up.

Message bodies

Message and Reply have bodyText and bodyHtml. Both are null unless the call asks for bodies with include: ["body"], because on some vendors a body costs extra requests.

import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";

const outbound = createOutbound({ adapter: memory({ data: demoData() }) });

// Bodies are left out unless you ask for them.
const plain = await outbound.replies.list({ category: "positive" });

console.log("without include:", plain.items[0]?.bodyText);

const withBody = await outbound.replies.list({
  category: "positive",
  include: ["body"],
});

console.log("with include:", withBody.items[0]?.bodyText);
without include: null
with include: Thanks, happy to chat next week.

"body" is the only value include takes. Asking for bodies on an adapter whose messages.body is false is refused with Unsupported. A body can still be null after you asked: the vendor did not return one for that message.

One record, and several

import { createOutbound } from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";

const outbound = createOutbound({ adapter: memory({ data: demoData() }) });

const campaign = await outbound.campaigns.get("cmp_founders");

console.log(campaign.name, campaign.meta.evidence);

console.log(await outbound.campaigns.exists("cmp_founders"));

console.log(await outbound.campaigns.exists("cmp_gone"));

// A missing record does not fail the batch. It is listed under `errors`.
const { errors, results } = await outbound.campaigns.getMany(
  ["cmp_agencies", "cmp_gone", "cmp_founders"],
  { concurrency: 2 }
);

for (const { id, value } of results) {
  console.log("ok:", id, value.name);
}

for (const { error, id } of errors) {
  console.log("failed:", id, error.code, "-", error.message);
}
Q4 SaaS founders exact
true
false
ok: cmp_agencies Agency owners
ok: cmp_founders Q4 SaaS founders
failed: cmp_gone NotFound - Campaign cmp_gone was not found

getMany returns two lists, each in the order of the ids you passed:

Field Holds
results { id, value } for each record that was read. value is the record with its meta.
errors { id, error } for each that was not. error is an OutboundError.
  • It does not throw for a record that fails. A missing id, a rate limit and a vendor error all land in errors. Check errors.length before you treat the batch as whole.
  • It does throw, before any request, when the adapter cannot read that kind of record at all.
  • concurrency is how many records are fetched at once. The default is 4. The rate budget still applies to every request.
  • The per-call options cover the whole batch. maxRequests: 10 is ten requests for all the ids together, and the ids past the budget come back in errors as RateLimited.
  • Handles: the same collections scoped to one campaign or one list.
  • Results: what meta tells you about a page or a record.

Was this page helpful?