How to Build a Lead Enrichment Pipeline with APIs (2026)
Step-by-step guide to building an automated lead enrichment pipeline using APIs. From data collection to CRM integration, with code examples and best practices.
What is a Lead Enrichment Pipeline?
A lead enrichment pipeline is an automated system that takes basic lead information (like an email address or company domain) and enhances it with additional data points from external sources. Instead of manually researching each lead, the pipeline does it automatically in seconds.
A typical pipeline flow looks like this:
- Lead enters your system (form submission, CSV upload, API webhook)
- Pipeline validates and cleans the data
- Pipeline calls enrichment APIs to gather additional information
- Enriched data is scored and routed to the appropriate team
- Data is synced to your CRM with all new attributes
Architecture Overview
We'll build a pipeline with these components:
Pipeline Components
- Input Layer: Webhook endpoint or queue to receive leads
- Validation Layer: Email validation, deduplication, data cleaning
- Enrichment Layer: API calls to gather profile and company data
- Scoring Layer: Calculate lead score based on enriched attributes
- Routing Layer: Assign leads to sales reps based on criteria
- Output Layer: Sync to CRM, send notifications, trigger workflows
Step 1: Set Up the Input Layer
First, create an endpoint to receive leads. This could be a webhook from your website forms, a CSV upload handler, or a queue consumer.
Example: Express.js Webhook Endpoint
const express = require('express');
const app = express();
app.post('/webhooks/new-lead', async (req, res) => {
const { email, name, company } = req.body;
// Acknowledge receipt immediately
res.status(200).json({ received: true });
// Process asynchronously
processLead({ email, name, company })
.catch(err => console.error('Lead processing failed:', err));
});
async function processLead(lead) {
// Validation, enrichment, and CRM sync happen here
console.log('Processing lead:', lead.email);
}
app.listen(3000);Key principle: Always respond to webhooks quickly (under 5 seconds). Process the lead asynchronously to avoid timeouts.
Step 2: Validate and Clean Data
Before enriching, validate that the data is usable. Check email deliverability, remove duplicates, and standardize formats.
Email Validation
async function validateEmail(email) {
// Basic format check
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return { valid: false, reason: 'Invalid format' };
}
// Check against disposable email domains
const disposableDomains = ['tempmail.com', '10minutemail.com'];
const domain = email.split('@')[1];
if (disposableDomains.includes(domain)) {
return { valid: false, reason: 'Disposable email' };
}
// Optional: Use email validation API
// const result = await emailValidationAPI.verify(email);
return { valid: true };
}Deduplication
async function checkDuplicate(email) {
// Check if lead already exists in database
const existing = await db.leads.findOne({ email });
if (existing) {
// Update last_seen timestamp
await db.leads.update({ email }, {
last_seen: new Date(),
submission_count: existing.submission_count + 1
});
return { isDuplicate: true, leadId: existing.id };
}
return { isDuplicate: false };
}Step 3: Enrich with Profile Data
Now the core enrichment step. Call APIs to gather professional profile data, company information, and social profiles.
Reverse Email Lookup
async function enrichProfile(email) {
try {
// Call Netrows reverse email lookup
const response = await fetch(
`https://api.netrows.com/api/v1/people/reverse-email?email=${email}`,
{
headers: {
'Authorization': `Bearer ${process.env.NETROWS_API_KEY}`
}
}
);
const profile = await response.json();
return {
full_name: profile.full_name,
job_title: profile.headline,
company: profile.company,
linkedin_url: profile.profile_url,
location: profile.location,
skills: profile.skills,
experience_years: calculateExperience(profile.experience)
};
} catch (error) {
console.error('Profile enrichment failed:', error);
return null;
}
}Company Enrichment
async function enrichCompany(companyDomain) {
try {
const response = await fetch(
`https://api.netrows.com/api/v1/companies/by-domain?domain=${companyDomain}`,
{
headers: {
'Authorization': `Bearer ${process.env.NETROWS_API_KEY}`
}
}
);
const company = await response.json();
return {
company_name: company.name,
company_size: company.employee_count,
industry: company.industry,
company_linkedin: company.linkedin_url,
headquarters: company.headquarters,
founded_year: company.founded_year,
website: company.website
};
} catch (error) {
console.error('Company enrichment failed:', error);
return null;
}
}Step 4: Calculate Lead Score
Use the enriched data to calculate a lead score. This helps prioritize which leads sales should contact first.
function calculateLeadScore(enrichedLead) {
let score = 0;
// Job title scoring
const seniorTitles = ['VP', 'Director', 'Head of', 'Chief', 'C-level'];
if (seniorTitles.some(title => enrichedLead.job_title?.includes(title))) {
score += 30;
}
// Company size scoring
if (enrichedLead.company_size >= 100 && enrichedLead.company_size <= 1000) {
score += 25; // Sweet spot for mid-market
} else if (enrichedLead.company_size > 1000) {
score += 20; // Enterprise
}
// Industry scoring
const targetIndustries = ['Technology', 'SaaS', 'Financial Services'];
if (targetIndustries.includes(enrichedLead.industry)) {
score += 20;
}
// Engagement scoring
if (enrichedLead.submission_count > 1) {
score += 15; // Returning visitor
}
// LinkedIn profile completeness
if (enrichedLead.linkedin_url && enrichedLead.skills?.length > 5) {
score += 10;
}
return Math.min(score, 100); // Cap at 100
}Scoring tiers:
- 80-100: Hot lead - Immediate follow-up
- 60-79: Warm lead - Follow-up within 24 hours
- 40-59: Qualified lead - Add to nurture campaign
- 0-39: Cold lead - Long-term nurture or disqualify
Step 5: Route to Sales Team
Based on the lead score and attributes, route to the appropriate sales rep or team.
async function routeLead(enrichedLead) {
let assignedTo = null;
// Route by company size
if (enrichedLead.company_size > 1000) {
assignedTo = await getAvailableRep('enterprise');
} else if (enrichedLead.company_size >= 100) {
assignedTo = await getAvailableRep('mid-market');
} else {
assignedTo = await getAvailableRep('smb');
}
// Route by geography
if (enrichedLead.location?.includes('EMEA')) {
assignedTo = await getAvailableRep('emea');
}
// Hot leads go to senior reps
if (enrichedLead.lead_score >= 80) {
assignedTo = await getAvailableRep('senior');
}
return assignedTo;
}
async function getAvailableRep(segment) {
// Round-robin assignment within segment
const reps = await db.salesReps.find({
segment,
active: true
});
// Find rep with fewest active leads
const repWithLeastLeads = reps.sort((a, b) =>
a.active_leads - b.active_leads
)[0];
return repWithLeastLeads;
}Step 6: Sync to CRM
Finally, push the enriched lead to your CRM with all the new data points.
Salesforce Example
async function syncToSalesforce(enrichedLead) {
const sfClient = new SalesforceClient({
clientId: process.env.SF_CLIENT_ID,
clientSecret: process.env.SF_CLIENT_SECRET
});
await sfClient.createLead({
FirstName: enrichedLead.first_name,
LastName: enrichedLead.last_name,
Email: enrichedLead.email,
Title: enrichedLead.job_title,
Company: enrichedLead.company_name,
LinkedIn__c: enrichedLead.linkedin_url,
LeadScore__c: enrichedLead.lead_score,
CompanySize__c: enrichedLead.company_size,
Industry: enrichedLead.industry,
OwnerId: enrichedLead.assigned_to,
LeadSource: 'Website',
Status: enrichedLead.lead_score >= 60 ? 'Working' : 'New'
});
}HubSpot Example
async function syncToHubSpot(enrichedLead) {
const hubspot = require('@hubspot/api-client');
const client = new hubspot.Client({
accessToken: process.env.HUBSPOT_TOKEN
});
await client.crm.contacts.basicApi.create({
properties: {
email: enrichedLead.email,
firstname: enrichedLead.first_name,
lastname: enrichedLead.last_name,
jobtitle: enrichedLead.job_title,
company: enrichedLead.company_name,
linkedin_url: enrichedLead.linkedin_url,
hs_lead_status: enrichedLead.lead_score >= 60 ? 'OPEN' : 'NEW',
lead_score: enrichedLead.lead_score,
company_size: enrichedLead.company_size,
industry: enrichedLead.industry
}
});
}Step 7: Error Handling & Retries
APIs can fail. Build robust error handling and retry logic.
async function enrichWithRetry(email, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const profile = await enrichProfile(email);
return profile;
} catch (error) {
console.error(`Enrichment attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
// Log to error tracking service
await logEnrichmentFailure(email, error);
return null; // Give up after max retries
}
// Exponential backoff
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Complete Pipeline Example
Putting it all together:
async function processLead(rawLead) {
try {
// Step 1: Validate
const emailCheck = await validateEmail(rawLead.email);
if (!emailCheck.valid) {
console.log('Invalid email:', emailCheck.reason);
return;
}
// Step 2: Check duplicates
const dupCheck = await checkDuplicate(rawLead.email);
if (dupCheck.isDuplicate) {
console.log('Duplicate lead:', dupCheck.leadId);
return;
}
// Step 3: Enrich profile
const profile = await enrichWithRetry(rawLead.email);
if (!profile) {
console.log('Enrichment failed, saving basic lead');
await db.leads.create(rawLead);
return;
}
// Step 4: Enrich company
const companyDomain = extractDomain(rawLead.email);
const company = await enrichCompany(companyDomain);
// Step 5: Combine data
const enrichedLead = {
...rawLead,
...profile,
...company,
enriched_at: new Date()
};
// Step 6: Calculate score
enrichedLead.lead_score = calculateLeadScore(enrichedLead);
// Step 7: Route to sales
enrichedLead.assigned_to = await routeLead(enrichedLead);
// Step 8: Save to database
await db.leads.create(enrichedLead);
// Step 9: Sync to CRM
await syncToSalesforce(enrichedLead);
// Step 10: Notify sales rep if hot lead
if (enrichedLead.lead_score >= 80) {
await notifySalesRep(enrichedLead);
}
console.log('Lead processed successfully:', enrichedLead.email);
} catch (error) {
console.error('Pipeline error:', error);
await logPipelineFailure(rawLead, error);
}
}Best Practices
1. Process Asynchronously
Use queues (Redis, RabbitMQ, AWS SQS) to process leads in the background. This prevents timeouts and allows you to handle spikes in volume.
2. Cache Enrichment Results
If the same email or company appears multiple times, cache the enrichment results for 30 days to save API costs.
3. Monitor Pipeline Health
Track metrics like:
- Enrichment success rate
- Average processing time
- API error rates
- Lead score distribution
- CRM sync failures
4. Implement Rate Limiting
Respect API rate limits. Use a rate limiter library to avoid hitting limits and getting blocked.
5. Log Everything
Comprehensive logging helps debug issues and understand pipeline performance. Log each step with timestamps and relevant context.
Deployment Options
Serverless (AWS Lambda, Vercel Functions)
Pros: Auto-scaling, pay-per-use, no server management
Cons: Cold starts, execution time limits
Container-Based (Docker, Kubernetes)
Pros: Full control, consistent environment, good for high volume
Cons: More complex, requires infrastructure management
Background Workers (Node.js, Python)
Pros: Simple, flexible, easy to debug
Cons: Need to manage scaling manually
Build Your Pipeline with Netrows
Netrows provides all the APIs you need for lead enrichment: LinkedIn profiles, company data, reverse email lookup, and more. Get started with 100 free credits and build your pipeline today.