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

Records

Every record the SDK returns and each of its fields, the six fields all records share, how refs are built and parsed, the exported Zod schemas, and why a missing value is null and never 0.

Every adapter maps its vendor’s data onto the same records. A Campaign from Instantly and a Campaign from HeyReach have the same fields with the same meaning. Each record type is exported from outbound-sdk as a TypeScript type, and most have an exported Zod schema.

Which fields a vendor can fill is an adapter fact: see the adapter pages. A field a vendor cannot fill is null.

Fields every record has

Field Type Holds
provider string The adapter that produced the record, for example instantly.
workspaceId string Your label for the credential it came from.
sourceId string The vendor’s own id, always a string.
ref string provider:workspaceId:sourceId. Unique across vendors and clients.
schemaVersion 1 SCHEMA_VERSION. Bumped only for a breaking change to these shapes.
raw JSON The vendor’s original payload for the record. null on the in-memory adapter.

MetricRow is the exception: it has provider, workspaceId and schemaVersion, and a scopeRef in place of its own ref.

raw is the vendor’s data unmapped, so it holds whatever personal data the vendor sent. The redact plugin removes it.

Refs

import {
  campaignSchema,
  createOutbound,
  makeRef,
  parseRef,
  SCHEMA_VERSION,
} from "outbound-sdk";
import { demoData, memory } from "outbound-sdk/memory";

const outbound = createOutbound({
  adapter: memory({ data: demoData(), workspaceId: "acme" }),
});

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

// The six fields every record carries.
console.log(campaign.provider, campaign.workspaceId, campaign.sourceId);

console.log(campaign.ref);

console.log(campaign.schemaVersion === SCHEMA_VERSION, campaign.raw);

// A ref is three parts joined with colons.
console.log(makeRef("instantly", "acme", "0193-ab"));

console.log(parseRef(campaign.ref));

console.log(parseRef("not-a-ref"));

// The ref and the vendor's own id name the same record.
const again = await outbound.campaigns.get(campaign.ref);

console.log(again.sourceId);

// The exported schemas check a record that came from somewhere else,
// for example your own database.
console.log(campaignSchema.safeParse(campaign).success);

console.log(campaignSchema.safeParse({ name: "No ref" }).success);
memory acme cmp_founders
memory:acme:cmp_founders
true null
instantly:acme:0193-ab
{ provider: 'memory', sourceId: 'cmp_founders', workspaceId: 'acme' }
null
cmp_founders
true
false
  • A ref names one record on one credential. Two clients on the same vendor, or one client on two vendors, can never be confused, because the workspace label and the provider are in the ref.
  • Wherever the SDK takes an id, it takes a ref. On an instance with one adapter both work. A ref from another workspace is refused with Invalid.
  • On a combined instance, use the ref. A bare id cannot say which adapter it belongs to. See troubleshooting.
  • Fields that point at another record hold its ref, not its id: campaignRef, leadRef, senderRef, campaignRefs, scopeRef.
  • Store the ref. It is the key to use in your own database.

makeRef(provider, workspaceId, sourceId) builds one. parseRef(ref) returns { provider, workspaceId, sourceId }, or null for a string that is not a ref. A vendor id may itself contain colons: everything after the second colon is the sourceId. A workspaceId cannot be empty or contain a colon, and an adapter constructed with one throws Invalid.

Null is not zero

A value the vendor does not report is null. It is never 0, "" or false.

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

// One day of results. The vendor reported sends, replies and bounces, and
// said nothing about opens.
const outbound = createOutbound({
  adapter: memory({
    data: {
      campaigns: [{ id: "cmp_1" }],
      senders: [{ id: "mbx_1" }],
      stats: [
        {
          campaignId: "cmp_1",
          counts: { bounced: 0, replied: 6, sent: 200 },
          date: "2026-11-10",
        },
      ],
    },
  }),
});

const { rows } = await outbound.campaign("cmp_1").stats();
const [row] = rows;

if (row !== undefined) {
  // 0 was reported. null was not reported.
  console.log("bounced:", row.bounced, "| opened:", row.opened);

  const rates = computeRates(row);

  console.log("bounce rate:", rates.bounceRate);
  console.log("open rate:", rates.openRate);
  console.log("reply rate:", rates.replyRate);
}

// Fields work the same way.
const sender = await outbound.sender("mbx_1").get();

console.log(sender.address, sender.dailyLimit, sender.warmup);
bounced: 0 | opened: null
bounce rate: { basis: 'sent', value: 0 }
open rate: { basis: null, value: null }
reply rate: { basis: 'sent', value: 0.03 }
null null null

bounced: 0 means the vendor counted bounces and found none. opened: null means the vendor gave no open count: tracking was off, or its API does not report it. A rate built on a missing count is null too. Print “not reported”, not a zero, and do not add null counts up as zeros. sumCounts from outbound-sdk adds rows and keeps a count null unless at least one row reports it.

Arrays are the exception. tags, issues, events and campaignRefs are empty arrays when there is nothing to report, and custom is an empty object.

Campaign

Field Type Holds
name string
status "active", "paused", "draft", "completed", "archived", "unknown" The vendor’s status mapped onto these six.
channel "email" or "linkedin"
createdAt ISO timestamp or null
tags string[] Tag names.

Sequence

A campaign’s steps. Returned by campaign(id).sequence() and campaign(id).subsequences.

Field Type Holds
campaignRef string The campaign it belongs to.
kind "main" or "subsequence" The campaign’s own steps, or a follow-up path.
name string or null A subsequence’s name. null for the main sequence.
steps SequenceStep[]

SequenceStep:

Field Type Holds
index integer The step’s position, counted from 0.
action string email, or a LinkedIn action such as connection_request.
delayDays number or null
parentIndex integer or null The step that runs before this one. null for the first step.
branch string or null The outcome of the parent’s condition that leads here, in the vendor’s words, for example accepted. null on a straight path.
variants SequenceVariant[]

SequenceVariant:

Field Type Holds
variantKey string Names the variant within its step.
subject string or null Template text.
body string or null Template text, never a rendered message.
contentHash string A hash of subject and body, so stats can tell old copy from new.

Sender

A mailbox or a LinkedIn seat.

Field Type Holds
kind "mailbox" or "linkedin"
address string or null The mailbox address, lowercased. null for a LinkedIn seat.
linkedinUrl string or null
name string or null
status "active", "paused", "disconnected", "error", "unknown"
dailyLimit count or null
warmup { enabled, score } or null enabled is a boolean or null, score a number or null.

SenderHealth

What the vendor knows about a sender’s deliverability. Its sourceId is the sender’s id.

Field Type Holds
senderRef string
domain string or null The mailbox’s domain, lowercased. null for a LinkedIn seat.
connected boolean or null true when the vendor can sign in to the mailbox or seat.
dns { spf, dkim, dmarc, mx } Each true, false or null: whether the record is present and valid.
blacklisted boolean or null true when the domain or address is on a block list the vendor checks.
issues string[] Problems in the vendor’s own words.
checkedAt ISO timestamp or null When the vendor last checked.
warmup { sent, received, inbox, spam, score } or null Warmup traffic over the vendor’s reporting window.

Lead

Field Type Holds
email string or null Lowercased and trimmed.
linkedinUrl string or null
firstName, lastName, company, title string or null
status "active", "paused", "completed", "replied", "bounced", "unsubscribed", "unknown"
interest string or null The vendor’s own label for how interested the lead is, for example Meeting Booked.
campaignRef string or null
createdAt ISO timestamp or null
custom Record<string, string> The lead’s custom fields as the vendor stores them.
tags string[]
engagement LeadEngagement or null null when the vendor reports no per-lead counts on this read.

LeadEngagement, what a lead did with the campaign’s messages as the vendor counts it:

Field Type Holds
opens, clicks, replies count or null
lastContactedAt, lastOpenedAt, lastRepliedAt ISO timestamp or null
stepIndex integer or null The last step sent to the lead, counted from 0.
repliedStepIndex integer or null The step the lead replied to, counted from 0.

A lead is a person in one campaign. The same person in two campaigns is two Lead records. The identity plugin links them without merging them.

Message

One message sent or received.

Field Type Holds
direction "out" or "in"
channel "email" or "linkedin"
sentAt ISO timestamp or null
subject string or null
bodyText string or null Plain text with quoted history removed. null unless the call asked for bodies and the vendor returned one.
bodyHtml string or null The same rule.
campaignRef, leadRef, senderRef string or null
threadId string or null Pass it to outbound.thread(id).
stepIndex integer or null The sequence step that sent it.
variantKey string or null The variant that sent it.

Reply

A received message with a category. It has every Message field, direction is always "in", and it adds:

Field Type Holds
category "positive", "negative", "neutral", "ooo", "bounce", "unsubscribe", "unknown"
categorySource "vendor", "rule" or null Who decided the category. null when it is unknown. Never a model.
isAuto boolean or null true for an auto-reply, null when the vendor does not say.

MetricRow

One row of counts. Every count is a number or null. Its fields are listed on the stats page.

CampaignSettings

How a campaign is set up to send. Its sourceId is the campaign’s id.

Field Type Holds
campaignRef string
dailyLimit count or null The most messages or LinkedIn actions per day.
dailyNewLeads count or null The most new leads contacted per day.
gapMinutes number or null The shortest wait between two sends, in minutes.
startDate, endDate YYYY-MM-DD or null
plainText boolean or null true when emails are sent without HTML.
stopOnReply boolean or null true when a lead who replies gets no more steps.
stopOnAutoReply boolean or null The same for an automatic reply.
tracking { opens, clicks } Each a boolean or null.
schedule SendingWindow[] The windows in which the campaign may send.

SendingWindow:

Field Type Holds
name string or null
days integers 0 to 6 Days of the week the window is open. 0 is Sunday.
from, to HH:MM or null In timezone.
timezone string or null An IANA name where the vendor gives one, otherwise the vendor’s own label.

LeadList

A saved list that campaigns draw from.

Field Type Holds
name string
kind "leads" or "companies"
size count or null How many records the list holds.
createdAt ISO timestamp or null

Tag

Field Type Holds
name string
description string or null

BlockEntry

One address, domain or profile the account never contacts.

Field Type Holds
kind "email", "domain", "linkedin", "company", "other"
value string Lowercased for emails and domains.
createdAt ISO timestamp or null

WebhookSubscription

A webhook registered with the vendor. The SDK lists them. It does not create or change them.

Field Type Holds
url string Where the vendor posts. It can hold a secret in its path or query.
name string or null
events string[] Event names in the vendor’s own words.
active boolean or null
campaignRefs string[] The campaigns the webhook is limited to. Empty for every campaign.
createdAt ISO timestamp or null

Workspace

Field Type Holds
name string

Schemas

Each record is defined once as a Zod schema and its TypeScript type is inferred from it. The SDK checks every record against its schema before returning it. A record that does not match fails the call with a Provider error naming the field, so a malformed record never reaches your code.

The schemas are exported for checking records that come from somewhere else, such as your own database or a queue:

Schema Type
campaignSchema, campaignStatusSchema Campaign, CampaignStatus
sequenceSchema Sequence
senderSchema, senderStatusSchema Sender, SenderStatus
senderHealthSchema SenderHealth
leadSchema, leadStatusSchema Lead, LeadStatus
messageSchema Message
replySchema, replyCategorySchema Reply, ReplyCategory
metricRowSchema, metricScopeSchema MetricRow, MetricScope
campaignSettingsSchema CampaignSettings
leadListSchema LeadList
tagSchema Tag
blockEntrySchema BlockEntry
webhookSubscriptionSchema WebhookSubscription
workspaceSchema Workspace
channelSchema, evidenceSchema Channel, Evidence
outboundEventSchema, eventTypeSchema OutboundEvent, EventType: see webhook events

SequenceStep, SequenceVariant, LeadEngagement and SendingWindow are exported as types only. Entity is the union of every record a list or get call returns.

SCHEMA_VERSION is 1. Every record carries it as schemaVersion, so data you stored can say which shape it was written in.

Was this page helpful?