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.
Technology Stack
Expected Results
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 AccessRelated Guides
Trade Show & Event Lead Processor - Complete Implementation Guide
Process event leads instantly with personalized AI outreach while the interaction is fresh. Import business cards, scan badges, and follow up same-day.
Review & Referral Request System - Complete Implementation Guide
Automatically request reviews on Google/Yelp after sales, ask for referrals, and re-engage past customers for repeat business.
AI Appointment Setter - Complete Implementation Guide
Build a conversational AI that qualifies leads through SMS or chat, handles objections, and books appointments directly into your calendar.
Ready to Transform Your Lead Generation?
Let's discuss how we can implement this system for your business with expert optimization.
Book Strategy Call