How to Power Outbound Campaigns with CRM Contact Data Using Unified's CRM API
February 2, 2026
Outbound campaigns don't fail because of messaging. They fail because the audience isn't well defined.
When you support multiple CRMs, 'send an email to our contacts' quickly becomes ambiguous. Contact fields vary. Company context may or may not exist. Updates arrive at different times. If you can't explain why a contact was included in a campaign, it's hard to trust the results—or debug them.
Unified's CRM API lets you build outbound audiences on top of a normalized contact and company model. Instead of syncing everything into a separate system or branching logic per CRM, you can fetch contacts, enrich them with company data, and produce a campaign-ready list using documented fields and predictable pagination.
This guide shows how to build a clean, reusable outbound audience using contacts and companies only, with Unified's CRM API and the TypeScript SDK. No campaign tools, no UI, and no assumptions about delivery channels.
Prerequisites
- Node.js v18+
- A Unified account with a CRM integration enabled
- Your Unified API key
- A customer CRM
connectionId
Step 1: Set up your project
mkdir crm-outbound-audience
cd crm-outbound-audience
npm init -y
npm install @unified-api/typescript-sdk dotenv
Create a .env file:
UNIFIED_API_KEY=your_unified_api_key
CONNECTION_CRM=your_customer_crm_connection_id
Step 2: Initialize the SDK
import "dotenv/config";
import { UnifiedTo } from "@unified-api/typescript-sdk";
const { UNIFIED_API_KEY, CONNECTION_CRM } = process.env;
if (!UNIFIED_API_KEY) throw new Error("Missing UNIFIED_API_KEY");
if (!CONNECTION_CRM) throw new Error("Missing CONNECTION_CRM");
const sdk = new UnifiedTo({
security: { jwt: UNIFIED_API_KEY },
});
const connectionId = CONNECTION_CRM;
Step 3: Understand the outbound building blocks
For outbound audience construction, you only need two CRM objects.
Contacts (CrmContact)
Contacts represent people you can reach. Useful fields include:
- identity:
id,name,first_name,last_name,title,department - contact methods:
emails[]withtype(WORK,HOME,OTHER)telephones[]withtype(WORK,HOME,OTHER,FAX,MOBILE)
- company linkage:
company(string),company_ids[] - timestamps:
created_at,updated_at - enrichment:
link_urls[],metadata[] rawfor CRM-specific data if needed
Companies (CrmCompany)
Companies provide firmographic context for targeting and personalization.
Commonly used fields:
- identity:
id,name - firmographics:
industry,employees,is_active,timezone - domains:
domains[] - enrichment:
tags[],metadata[] - relationships:
contact_ids[] - timestamps:
created_at,updated_at rawwhen CRM-specific data is required
Required fields and write support vary by CRM. Always verify field support for your integration in the Feature Support tab.
Step 4: Fetch contacts safely (pagination + freshness)
The contact list endpoint supports pagination and incremental filtering:
limitdefaults to 100offsetis zero-based- stop paging when returned results are fewer than
limit - use
updated_gteto fetch only recently changed contacts
type Sort = "name" | "updated_at" | "created_at";
type Order = "asc" | "desc";
async function fetchAllContacts(opts?: {
pageSize?: number;
updated_gte?: string;
sort?: Sort;
order?: Order;
query?: string;
}) {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const out: any[] = [];
while (true) {
const page = await sdk.crm.listCrmContacts({
connectionId,
limit: pageSize,
offset,
updated_gte: opts?.updated_gte ?? "",
sort: opts?.sort ?? "updated_at",
order: opts?.order ?? "asc",
query: opts?.query ?? "",
});
if (!page || page.length === 0) break;
out.push(...page);
if (page.length < pageSize) break;
offset += pageSize;
}
return out;
}
This pattern keeps outbound audiences:
- explainable
- reproducible
- incremental
Step 5: Fetch companies for enrichment
Contacts often reference companies by ID. To enrich your audience with firmographic data, fetch companies and build a lookup map.
async function fetchAllCompanies(opts?: {
pageSize?: number;
updated_gte?: string;
}) {
const pageSize = opts?.pageSize ?? 100;
let offset = 0;
const out: any[] = [];
while (true) {
const page = await sdk.crm.listCrmCompanies({
connectionId,
limit: pageSize,
offset,
updated_gte: opts?.updated_gte ?? "",
sort: "updated_at",
order: "asc",
});
if (!page || page.length === 0) break;
out.push(...page);
if (page.length < pageSize) break;
offset += pageSize;
}
return out;
}
function indexById(rows: any[]): Record<string, any> {
const out: Record<string, any> = {};
for (const r of rows) {
if (!r?.id) continue;
out[String(r.id)] = r;
}
return out;
}
Step 6: Define an outbound audience row
An outbound audience row should be:
- deterministic
- built only from documented fields
- explicit about missing data
- easy to inspect and debug
Here's a conservative, campaign-ready shape:
type OutboundContact = {
contact_id: string;
first_name?: string;
last_name?: string;
title?: string;
department?: string;
primary_email?: string;
email_type?: string;
primary_phone?: string;
company_id?: string;
company_name?: string;
company_industry?: string;
company_employees?: number;
company_is_active?: boolean;
company_domains?: string[];
last_updated_at?: string;
};
Step 7: Build the outbound audience
This step joins contacts to companies and extracts the fields needed for outreach and segmentation.
function buildOutboundAudience(input: {
contacts: any[];
companiesById: Record<string, any>;
}): OutboundContact[] {
const { contacts, companiesById } = input;
const out: OutboundContact[] = [];
for (const c of contacts) {
if (!c?.id) continue;
const email = c.emails?.[0];
const phone = c.telephones?.[0];
const companyId = c.company_ids?.[0];
const company = companyId ? companiesById[companyId] : undefined;
out.push({
contact_id: c.id,
first_name: c.first_name,
last_name: c.last_name,
title: c.title,
department: c.department,
primary_email: email?.email,
email_type: email?.type,
primary_phone: phone?.telephone,
company_id: companyId,
company_name: company?.name ?? c.company,
company_industry: company?.industry,
company_employees: company?.employees,
company_is_active: company?.is_active,
company_domains: company?.domains,
last_updated_at: c.updated_at,
});
}
return out;
}
This produces an audience you can:
- filter by firmographics
- segment by role or department
- personalize messaging
- audit when contacts were last updated
Step 8: Segment the audience for campaigns
Once you have a normalized audience, segmentation becomes straightforward and CRM-agnostic.
Examples:
const enterpriseEngineeringContacts = audience.filter(
(c) =>
c.company_employees !== undefined &&
c.company_employees >= 1000 &&
c.department === "Engineering"
);
const activeCompanyContacts = audience.filter(
(c) => c.company_is_active === true
);
These filters behave consistently across CRMs because they're applied after normalization.
Step 9: Use the audience in outbound workflows
Unified provides the audience. Delivery happens elsewhere.
From here, you can:
- send contacts to an email provider
- sync to a sales engagement tool
- enqueue jobs for SMS or call workflows
- generate CSVs for review or approval
Keep delivery concerns separate from audience construction. It makes campaigns easier to reason about and debug.
Summary
Using Unified's CRM API, you can power outbound campaigns with a clean, explainable audience built from normalized contact and company data:
- Fetch contacts incrementally using
updated_gte - Enrich with firmographic context from companies
- Build deterministic, inspectable audience rows
- Segment without CRM-specific logic
- Reuse the same pipeline across customers and CRMs
By separating audience definition from message delivery, you keep outbound systems flexible, auditable, and easier to maintain as you add new CRM integrations.