Skip to main content
Back to Guides
Lead ManagementAdvancedPackage Available

Multi-Channel Lead Aggregator - Complete Implementation Guide

Consolidate leads from all sources - website, ads, directories, social media - into one unified system with consistent AI-powered initial contact.

7 min read
Implementation: 2-4 weeks
lead-aggregationmulti-channeldata-normalizationdeduplication

Technology Stack

CRM PlatformGoHighLevel
Automationn8n
AI/LLMOpenAI GPT-4o
MessagingTwilio SMS, SendGrid

Expected Results

100% of leads captured and contacted
Unified lead data across all sources
15+ hours/week on manual data entry
Positive ROI within 6 weeks

Multi-Channel Lead Aggregator

When leads come from 10+ sources, chaos ensues. Some get missed, data is inconsistent, and follow-up is unpredictable. This system creates a single source of truth for all your leads.

The Multi-Channel Problem

Typical lead sources for a growing business:

  • Website contact form
  • Facebook/Instagram ads
  • Google Ads
  • Google Business Profile
  • Yelp/Angi/HomeAdvisor
  • Referral forms
  • Trade show scans
  • Phone calls
  • Email inquiries
  • Social media DMs

Without aggregation:

  • Leads fall through the cracks
  • Duplicate contacts everywhere
  • Inconsistent data quality
  • No unified view of lead sources
  • Impossible to track attribution

System Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Lead Sources                              │
│                                                              │
│  Website │ FB/IG │ Google │ GBP │ Directories │ Social │ Phone
└─────┬─────┴───┬───┴───┬────┴──┬──┴──────┬──────┴────┬───┴──┬──┘
      │         │       │       │         │           │      │
      └─────────┴───────┴───────┴─────────┴───────────┴──────┘
                              │
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                 Lead Aggregation Layer                       │
│                                                              │
│   Normalize → Deduplicate → Enrich → Score → Route          │
└───────────────────────┬─────────────────────────────────────┘
                        │
                        ↓
┌─────────────────────────────────────────────────────────────┐
│              GoHighLevel (Single Source of Truth)           │
│                                                              │
│   Unified Contact Record │ Source Attribution │ AI Response │
└─────────────────────────────────────────────────────────────┘

Source-by-Source Integration

1. Website Contact Forms

// Standard webhook handler
app.post("/webhook/website-form", async (req, res) => {
  const lead = normalizeWebsiteLead(req.body);
  await processLead(lead);
  res.status(200).send("OK");
});

const normalizeWebsiteLead = (data) => ({
  source: "website",
  sourceMedium: "organic",
  firstName: data.first_name || data.name?.split(" ")[0],
  lastName: data.last_name || data.name?.split(" ").slice(1).join(" "),
  email: data.email?.toLowerCase().trim(),
  phone: normalizePhone(data.phone),
  message: data.message,
  interest: data.service || data.interest,
  page: data.form_page || data.referrer,
  utm: {
    source: data.utm_source,
    medium: data.utm_medium,
    campaign: data.utm_campaign,
  },
  timestamp: new Date().toISOString(),
});

2. Facebook Lead Ads

// Facebook Graph API integration
const normalizeFacebookLead = async (webhookData) => {
  const leadData = await fetchFacebookLead(webhookData.leadgen_id);

  return {
    source: "facebook",
    sourceMedium: "paid_social",
    externalId: leadData.id,
    adId: leadData.ad_id,
    formId: leadData.form_id,
    ...extractFieldData(leadData.field_data),
    timestamp: leadData.created_time,
  };
};

const extractFieldData = (fieldData) => {
  const fields = {};
  for (const field of fieldData) {
    const key = mapFacebookField(field.name);
    fields[key] = field.values[0];
  }
  return fields;
};

const mapFacebookField = (fbField) => {
  const mapping = {
    full_name: "fullName",
    email: "email",
    phone_number: "phone",
    city: "city",
    state: "state",
    zip_code: "zip",
  };
  return mapping[fbField] || fbField;
};

3. Google Business Profile

// GBP Message/Lead webhook
const normalizeGBPLead = (data) => ({
  source: "google_business",
  sourceMedium: "organic_local",
  type: data.type, // message, booking, call
  firstName: data.customer_name,
  phone: data.customer_phone,
  message: data.message_content,
  location: data.location_id,
  timestamp: data.timestamp,
});

4. Directory Leads (Angi, HomeAdvisor, Thumbtack)

// Each directory has different integration methods
const directoryIntegrations = {
  angi: {
    method: "webhook",
    normalize: (data) => ({
      source: "angi",
      sourceMedium: "directory",
      firstName: data.customer.first_name,
      lastName: data.customer.last_name,
      email: data.customer.email,
      phone: data.customer.phone,
      project: data.project_details,
      category: data.service_category,
      matchFee: data.lead_fee,
    }),
  },
  homeadvisor: {
    method: "email_parse", // Parse lead notification emails
    normalize: (parsedEmail) => ({
      source: "homeadvisor",
      sourceMedium: "directory",
      ...extractFromEmailBody(parsedEmail),
    }),
  },
  thumbtack: {
    method: "api",
    normalize: (data) => ({
      source: "thumbtack",
      sourceMedium: "directory",
      firstName: data.request.customer.name,
      phone: data.request.customer.phone,
      project: data.request.description,
      budget: data.request.budget,
    }),
  },
};

5. Phone Calls (via Call Tracking)

// CallRail/CallTrackingMetrics webhook
const normalizeCallLead = (data) => ({
  source: "phone_call",
  sourceMedium: data.tracking_source || "direct",
  phone: data.caller_number,
  firstName: data.caller_name || null,
  callDuration: data.duration,
  callRecording: data.recording_url,
  wasAnswered: data.answered,
  trackingNumber: data.tracking_number,
  campaign: data.campaign_name,
  timestamp: data.start_time,
});

6. Social Media DMs

// Instagram/Facebook Messenger integration
const normalizeSocialDM = (data) => ({
  source: data.platform, // instagram, facebook_messenger
  sourceMedium: "social_dm",
  externalId: data.sender_id,
  firstName: data.sender_name,
  message: data.message_text,
  conversationId: data.thread_id,
  timestamp: data.timestamp,
});

Data Normalization

Master Normalization Function

const normalizeLeadData = (rawLead, source) => {
  return {
    // Identity
    firstName: cleanName(rawLead.firstName || rawLead.first_name),
    lastName: cleanName(rawLead.lastName || rawLead.last_name),
    fullName:
      rawLead.fullName || `${rawLead.firstName} ${rawLead.lastName}`.trim(),

    // Contact
    email: cleanEmail(rawLead.email),
    phone: normalizePhone(rawLead.phone || rawLead.phone_number),

    // Location
    address: rawLead.address,
    city: rawLead.city,
    state: rawLead.state,
    zip: normalizeZip(rawLead.zip || rawLead.zip_code),

    // Source Attribution
    source: source,
    sourceMedium: rawLead.sourceMedium,
    campaign: rawLead.campaign || rawLead.utm?.campaign,
    externalId: rawLead.externalId,

    // Lead Details
    interest: rawLead.interest || rawLead.project || rawLead.service,
    message: rawLead.message,
    budget: rawLead.budget,
    timeline: rawLead.timeline,

    // Metadata
    rawData: JSON.stringify(rawLead),
    createdAt: rawLead.timestamp || new Date().toISOString(),
    processedAt: new Date().toISOString(),
  };
};

// Helper functions
const cleanName = (name) => {
  if (!name) return "";
  return name
    .trim()
    .replace(/\s+/g, " ")
    .split(" ")
    .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
    .join(" ");
};

const cleanEmail = (email) => {
  if (!email) return null;
  return email.toLowerCase().trim();
};

const normalizePhone = (phone) => {
  if (!phone) return null;
  const digits = phone.replace(/\D/g, "");
  if (digits.length === 10) return `+1${digits}`;
  if (digits.length === 11 && digits.startsWith("1")) return `+${digits}`;
  return null; // Invalid phone
};

Deduplication

const deduplicateLead = async (normalizedLead) => {
  // Check for existing contact by phone (primary match)
  if (normalizedLead.phone) {
    const phoneMatch = await findContactByPhone(normalizedLead.phone);
    if (phoneMatch) {
      return {
        isDuplicate: true,
        existingContact: phoneMatch,
        matchType: "phone",
      };
    }
  }

  // Check for existing contact by email (secondary match)
  if (normalizedLead.email) {
    const emailMatch = await findContactByEmail(normalizedLead.email);
    if (emailMatch) {
      return {
        isDuplicate: true,
        existingContact: emailMatch,
        matchType: "email",
      };
    }
  }

  // Check for fuzzy name match (last resort, with same company)
  if (normalizedLead.fullName && normalizedLead.company) {
    const nameMatch = await findContactByNameAndCompany(
      normalizedLead.fullName,
      normalizedLead.company,
    );
    if (nameMatch && nameMatch.score > 0.8) {
      return {
        isDuplicate: true,
        existingContact: nameMatch,
        matchType: "name_company",
      };
    }
  }

  return { isDuplicate: false };
};

// Handle duplicates
const handleDuplicate = async (newLead, existingContact, matchType) => {
  // Update existing contact with new source info
  await updateContact(existingContact.id, {
    // Add to source history
    source_history: [
      ...existingContact.source_history,
      { source: newLead.source, date: newLead.createdAt },
    ],
    // Update if new info is better
    email: existingContact.email || newLead.email,
    phone: existingContact.phone || newLead.phone,
    // Add new interest/inquiry
    latest_inquiry: newLead.interest,
    last_activity: new Date().toISOString(),
  });

  // Tag for re-engagement
  await addTag(existingContact.id, "returning_lead");

  // Trigger re-engagement workflow
  await triggerWorkflow("returning_lead", existingContact.id);
};

Lead Routing Rules

const routingRules = {
  // By source
  sourceRouting: {
    angi: { assignTo: "angi_specialist", priority: "high" },
    google_ads: { assignTo: "sales_team", priority: "urgent" },
    referral: { assignTo: "account_manager", priority: "high" },
  },

  // By geography
  geoRouting: {
    CA: { assignTo: "west_coast_team" },
    NY: { assignTo: "east_coast_team" },
    TX: { assignTo: "central_team" },
  },

  // By service type
  serviceRouting: {
    commercial: { assignTo: "commercial_team" },
    residential: { assignTo: "residential_team" },
    emergency: { assignTo: "on_call_tech", priority: "urgent" },
  },

  // By lead score
  scoreRouting: {
    high: { score: 80, assignTo: "closers", priority: "urgent" },
    medium: { score: 50, assignTo: "sdr_team", priority: "high" },
    low: { score: 0, assignTo: "nurture_queue", priority: "normal" },
  },
};

const routeLead = async (lead) => {
  // Apply routing rules in priority order
  let assignment = { assignTo: "general_queue", priority: "normal" };

  // Score-based (highest priority)
  for (const [tier, rule] of Object.entries(routingRules.scoreRouting)) {
    if (lead.score >= rule.score) {
      assignment = { assignTo: rule.assignTo, priority: rule.priority };
      break;
    }
  }

  // Source override
  if (routingRules.sourceRouting[lead.source]) {
    assignment = { ...assignment, ...routingRules.sourceRouting[lead.source] };
  }

  // Service type override
  if (routingRules.serviceRouting[lead.serviceType]) {
    assignment = {
      ...assignment,
      ...routingRules.serviceRouting[lead.serviceType],
    };
  }

  return assignment;
};

Unified Response System

const sendUnifiedResponse = async (lead) => {
  // Determine best channel
  const channel = determineChannel(lead);

  // Generate AI response based on source context
  const response = await generateResponse(lead);

  // Send
  if (channel === "sms" && lead.phone) {
    await sendSMS(lead.phone, response);
  } else if (channel === "email" && lead.email) {
    await sendEmail(lead.email, response);
  }

  // Log
  await logActivity(lead.id, "initial_response", { channel, response });
};

const generateResponse = async (lead) => {
  const prompt = `Generate a personalized initial response for this lead.

Lead Source: ${lead.source}
Name: ${lead.firstName}
Interest: ${lead.interest}
Message: ${lead.message}

Source-specific context:
${getSourceContext(lead.source)}

Generate a warm, helpful response acknowledging their specific request.
Keep under 160 characters for SMS.`;

  return await openai.generate(prompt);
};

const getSourceContext = (source) => {
  const contexts = {
    angi: "They found you through Angi. Reference home improvement expertise.",
    google_ads: "They clicked on an ad. They have specific intent - be direct.",
    referral: "Someone recommended you. Thank them for the referral.",
    phone_call: "They called but didn't connect. Apologize for missing them.",
  };
  return contexts[source] || "Standard inquiry.";
};

Source Attribution Dashboard

const getSourceAnalytics = async (dateRange) => {
  const leads = await db.leads.find({
    createdAt: { $gte: dateRange.start, $lte: dateRange.end },
  });

  return {
    bySource: groupBy(leads, "source"),
    byMedium: groupBy(leads, "sourceMedium"),
    conversionBySource: await calculateConversionBySource(leads),
    costPerLeadBySource: await calculateCPLBySource(leads),
    revenueBySource: await calculateRevenueBySource(leads),
    qualityBySource: await calculateQualityBySource(leads),
  };
};

Ready to unify your lead sources? Get the implementation package or let us build your aggregation system.

Get the Complete Implementation Package

Includes n8n workflow templates, TypeScript integrations, message templates, and step-by-step setup guides. Everything you need to deploy this system.

Request Access

Ready to Transform Your Lead Generation?

Let's discuss how we can implement this system for your business with expert optimization.

Book Strategy Call