DEV Community

Cover image for Build a Lead Enrichment Pipeline That Actually Works
APIVerve
APIVerve

Posted on • Originally published at blog.apiverve.com

Build a Lead Enrichment Pipeline That Actually Works

Sales teams hate CRMs full of email addresses and nothing else.

"Who is this person? What company? Where are they located? Should I call them or is it 3am there?"

The data exists. It's just not in your system yet. And every hour your sales team spends manually researching leads is an hour they're not selling.

This is what lead enrichment solves. Someone signs up with just an email, and by the time the page finishes loading, you know their name, company, location, social profiles, and whether they're worth calling.

Let me show you how to build this.

What's Actually Possible

From a single email address and IP (which you already have from the signup request), you can automatically discover:

  • Full name — From their Gravatar profile
  • Profile photo — Also Gravatar
  • Company name — From the email domain (jane@acme.com = Acme)
  • Job title and bio — Gravatar profiles often include this
  • Social profiles — LinkedIn, Twitter, GitHub links
  • Physical location — From their IP address
  • Timezone — So you know when to call

Not everyone has all this data public, but a surprising number of tech and business professionals do. And those are exactly the leads you want.

The "Aha" Moment

Here's what happens when you add lead enrichment to a signup flow. Before, the sales dashboard shows:

New Lead: jane.doe@company.com
Location: Unknown
Company: Unknown
Enter fullscreen mode Exit fullscreen mode

After:

New Lead: Jane Doe
Company: Company Inc (Software, 50-200 employees)
Location: Austin, TX (CST)
LinkedIn: linkedin.com/in/janedoe
Photo: [actual headshot]
Best time to call: 9am-5pm CST (currently 10:30am)
Enter fullscreen mode Exit fullscreen mode

Their sales reps went from "who is this?" to "calling Jane now" in seconds instead of minutes. Meeting booking rate went up 40%.

The Technical Implementation

You need three data sources:

  1. Email validation — Confirm it's real and extract the domain
  2. IP geolocation — Get location from their signup request
  3. Gravatar lookup — Fetch public profile data

Here's each piece:

Email Validation (First Filter)

Don't waste resources enriching fake emails.

async function validateAndExtract(email) {
  const res = await fetch(
    `https://api.apiverve.com/v1/emailvalidator?email=${encodeURIComponent(email)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  if (!data.isValid) {
    return { valid: false };
  }

  return {
    valid: true,
    domain: data.domain,
    isCompanyEmail: data.isCompanyEmail,
    isFreeEmail: data.isFreeEmail,
    // Already gives us a company signal!
    companyGuess: data.isCompanyEmail
      ? formatCompanyName(data.domain)
      : null
  };
}

function formatCompanyName(domain) {
  // acme-corp.com -> Acme Corp
  return domain
    .split('.')[0]
    .split('-')
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
    .join(' ');
}
Enter fullscreen mode Exit fullscreen mode

Right away you know: Is this a company email? If so, you've got the company name from the domain. jane@slack.com probably works at Slack.

IP Geolocation

Every HTTP request includes the client's IP. Turn it into a location.

async function getLocationFromIP(ip) {
  const res = await fetch(
    `https://api.apiverve.com/v1/iplookup?ip=${encodeURIComponent(ip)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    city: data.city,
    region: data.region,
    country: data.country,
    timezone: data.timezone,
    coordinates: data.coordinates,
    // Calculate business hours
    localTime: getLocalTime(data.timezone),
    isBusinessHours: isBusinessHours(data.timezone)
  };
}

function getLocalTime(timezone) {
  return new Date().toLocaleString('en-US', {
    timeZone: timezone,
    hour: 'numeric',
    minute: '2-digit',
    hour12: true
  });
}

function isBusinessHours(timezone) {
  const localHour = new Date().toLocaleString('en-US', {
    timeZone: timezone,
    hour: 'numeric',
    hour12: false
  });
  const hour = parseInt(localHour);
  return hour >= 9 && hour < 17;
}
Enter fullscreen mode Exit fullscreen mode

Now you know where they are and whether it's a good time to call. No more accidentally calling London at 3am because you forgot the timezone.

Gravatar Profile Lookup

Here's where the magic happens. Gravatar profiles are public and often include rich data.

async function getGravatarProfile(email) {
  const res = await fetch(
    `https://api.apiverve.com/v1/gravatarlookup?email=${encodeURIComponent(email)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );

  const { data, status } = await res.json();

  // Not everyone has a Gravatar
  if (status !== 'ok' || !data) {
    return null;
  }

  // Parse social accounts into a cleaner format
  const socials = {};
  if (data.accounts) {
    data.accounts.forEach(account => {
      socials[account.name.toLowerCase()] = {
        url: account.url,
        username: account.username
      };
    });
  }

  return {
    displayName: data.displayName,
    photo: data.thumbnailUrl,
    bio: data.aboutMe,
    location: data.currentLocation,
    company: data.company,
    website: data.profileUrl,
    socials,
    // Extract specific valuable links
    linkedin: socials.linkedin?.url,
    twitter: socials.twitter?.url || socials.x?.url,
    github: socials.github?.url
  };
}
Enter fullscreen mode Exit fullscreen mode

Developers especially love Gravatar. Their profiles often include GitHub, LinkedIn, Twitter, and a bio. You're getting data that would take a human 10 minutes to Google.

Putting It All Together

Here's the complete enrichment pipeline:

async function enrichLead(email, ip) {
  const startTime = Date.now();

  // Step 1: Validate email
  const emailData = await validateAndExtract(email);
  if (!emailData.valid) {
    return {
      success: false,
      reason: 'invalid_email',
      enrichmentTime: Date.now() - startTime
    };
  }

  // Step 2: Run location and Gravatar lookups in parallel
  const [location, gravatar] = await Promise.all([
    getLocationFromIP(ip),
    getGravatarProfile(email)
  ]);

  // Step 3: Build the enriched profile
  const enriched = {
    // Core info
    email,
    emailDomain: emailData.domain,
    isCompanyEmail: emailData.isCompanyEmail,

    // Name (from Gravatar or derived from email)
    name: gravatar?.displayName || deriveNameFromEmail(email),
    photo: gravatar?.photo,

    // Company info
    company: gravatar?.company || emailData.companyGuess,

    // Location
    location: gravatar?.location || formatLocation(location),
    timezone: location.timezone,
    localTime: location.localTime,
    isBusinessHours: location.isBusinessHours,

    // Social links
    linkedin: gravatar?.linkedin,
    twitter: gravatar?.twitter,
    github: gravatar?.github,
    bio: gravatar?.bio,

    // Metadata
    enrichmentTime: Date.now() - startTime,
    enrichmentLevel: calculateEnrichmentLevel(gravatar, emailData),
    enrichedAt: new Date().toISOString()
  };

  return {
    success: true,
    data: enriched
  };
}

function deriveNameFromEmail(email) {
  // john.doe@company.com -> John Doe
  const username = email.split('@')[0];
  return username
    .replace(/[._]/g, ' ')
    .replace(/\d+/g, '')
    .split(' ')
    .filter(Boolean)
    .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
    .join(' ') || null;
}

function formatLocation(loc) {
  if (loc.city && loc.region) {
    return `${loc.city}, ${loc.region}`;
  }
  return loc.country;
}

function calculateEnrichmentLevel(gravatar, emailData) {
  // How much data did we get?
  if (gravatar?.displayName && gravatar?.linkedin) return 'full';
  if (gravatar?.displayName || emailData.isCompanyEmail) return 'partial';
  return 'minimal';
}
Enter fullscreen mode Exit fullscreen mode

Three API calls (one for validation, one for IP, one for Gravatar). Run the last two in parallel. Total time: usually under 500ms.

What You Get

Here's a real example of enriched output:

{
  "success": true,
  "data": {
    "email": "alex.johnson@techstartup.io",
    "emailDomain": "techstartup.io",
    "isCompanyEmail": true,
    "name": "Alex Johnson",
    "photo": "https://gravatar.com/avatar/abc123...",
    "company": "TechStartup",
    "location": "San Francisco, CA",
    "timezone": "America/Los_Angeles",
    "localTime": "10:30 AM",
    "isBusinessHours": true,
    "linkedin": "https://linkedin.com/in/alexjohnson",
    "twitter": "https://twitter.com/alexj",
    "github": "https://github.com/alexjohnson",
    "bio": "Engineering lead at TechStartup. Building developer tools.",
    "enrichmentTime": 423,
    "enrichmentLevel": "full",
    "enrichedAt": "2025-12-09T18:30:00.000Z"
  }
}
Enter fullscreen mode Exit fullscreen mode

Your sales rep sees this 2 seconds after the signup. They know it's an engineering lead at a tech startup in SF, it's 10:30am there (good time to call), and they can connect on LinkedIn first if they want to warm up the lead.

Integration Patterns

Real-Time (Recommended for Sales-Led Products)

Enrich during signup, display immediately in admin dashboard:

app.post('/signup', async (req, res) => {
  const { email, password } = req.body;
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;

  // Create account
  const user = await createUser(email, password);

  // Enrich in background, don't slow down signup
  enrichLead(email, ip)
    .then(result => {
      if (result.success) {
        saveEnrichment(user.id, result.data);

        // Notify sales if it's a hot lead
        if (result.data.isCompanyEmail && result.data.linkedin) {
          notifySalesSlack(result.data);
        }
      }
    })
    .catch(console.error);

  // Return immediately
  res.json({ success: true });
});
Enter fullscreen mode Exit fullscreen mode

Batch (For Existing Lists)

Already have emails without enrichment? Process them:

async function enrichExistingLeads() {
  const unenrichedLeads = await db.query(`
    SELECT id, email, signup_ip
    FROM leads
    WHERE enrichment_data IS NULL
    LIMIT 100
  `);

  for (const lead of unenrichedLeads) {
    const result = await enrichLead(lead.email, lead.signup_ip);

    await db.query(`
      UPDATE leads
      SET enrichment_data = $1, enriched_at = NOW()
      WHERE id = $2
    `, [JSON.stringify(result.data), lead.id]);

    // Rate limit ourselves
    await sleep(100);
  }
}
Enter fullscreen mode Exit fullscreen mode

Run this as a cron job to slowly enrich your backlog.

Lead Scoring Based on Enrichment

Once you have this data, you can score leads automatically:

function scoreLead(enrichment) {
  let score = 0;
  const signals = [];

  // Company email = serious buyer
  if (enrichment.isCompanyEmail) {
    score += 25;
    signals.push('company_email');
  }

  // Has LinkedIn = findable and professional
  if (enrichment.linkedin) {
    score += 20;
    signals.push('has_linkedin');
  }

  // Full Gravatar profile = engaged professional
  if (enrichment.enrichmentLevel === 'full') {
    score += 15;
    signals.push('rich_profile');
  }

  // Bio mentions relevant keywords
  if (enrichment.bio) {
    const relevantTerms = ['engineer', 'developer', 'lead', 'manager', 'director', 'founder', 'cto', 'ceo'];
    if (relevantTerms.some(term => enrichment.bio.toLowerCase().includes(term))) {
      score += 20;
      signals.push('decision_maker');
    }
  }

  // US-based (adjust for your market)
  if (enrichment.timezone?.startsWith('America/')) {
    score += 10;
    signals.push('us_timezone');
  }

  return { score, signals, priority: score >= 50 ? 'high' : score >= 25 ? 'medium' : 'low' };
}
Enter fullscreen mode Exit fullscreen mode

High-scoring leads get immediate sales attention. Low-scoring leads go to nurture campaigns. No manual sorting required.

What If There's No Gravatar?

Not everyone has a Gravatar profile. Your enrichment pipeline still provides value:

  • Email domain tells you the company
  • IP location tells you where they are
  • Timezone tells you when to call
  • isCompanyEmail tells you if it's a B2B lead

That's still more than "email address and nothing else."

For leads without Gravatar data, you might:

  • Trigger a LinkedIn search task for your SDRs
  • Use additional enrichment services
  • Add to a "needs research" queue

The point is automation handles the 60% of cases that have public data, freeing humans for the rest.

Privacy Considerations

Everything in this pipeline uses publicly available data:

  • IP geolocation is based on network routing information
  • Gravatar profiles are explicitly public (users opt-in)
  • Email domain inference is... just reading the email

You're not scraping anything. You're not buying shady data lists. You're aggregating information that leads have made public.

That said, be transparent. Your privacy policy should mention you collect location data and may look up public profile information. Most B2B buyers expect this and don't mind.

The Cost Math

Let's price this out:

  • Email validation: 1 credit
  • IP lookup: 1 credit
  • Gravatar lookup: 1 credit
  • Total: 3 credits per lead

On APIVerve's Starter plan ({{plan.starter.price}}/month), you get {{plan.starter.calls}} credits. That's thousands of leads fully enriched.

Compare to dedicated lead enrichment services (Clearbit, ZoomInfo) charging $0.10-$0.50+ per enrichment. You're looking at 5-15x cost savings, depending on the service.

And you're not locked into anyone's data format. You control the pipeline.


Lead enrichment isn't magic. It's just connecting dots that already exist publicly. The email tells you the company. The IP tells you the location. Gravatar tells you who they are.

Three API calls. One sales team that actually has context. Deals that close faster because nobody's wasting time researching.

The Email Validator, IP Lookup, and Gravatar Lookup APIs are all available with the same API key. Start enriching your leads today.


Originally published at APIVerve Blog

Top comments (0)