Unified.to
All articles

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[] with type (WORK, HOME, OTHER)
    • telephones[] with type (WORK, HOME, OTHER, FAX, MOBILE)
  • company linkage: company (string), company_ids[]
  • timestamps: created_at, updated_at
  • enrichment: link_urls[], metadata[]
  • raw for 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
  • raw when 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:

  • limit defaults to 100
  • offset is zero-based
  • stop paging when returned results are fewer than limit
  • use updated_gte to 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.

Start your 30-day free trial

Book a demo

All articles