Back to Blog
Tutorial

Automate Influencer Outreach: From Data Collection to Email Pipeline

April 12, 2026
10 min read
S
By SociaVault Team
Influencer MarketingOutreachAutomationPipelineEmailAPI

Automate Influencer Outreach: From Data Collection to Email Pipeline

Most influencer outreach is painfully manual: scroll through Instagram, copy profile stats into a spreadsheet, find email addresses one by one, write generic messages. A 50-creator campaign takes a week of grunt work before a single email is sent.

You can automate 80% of this pipeline. Here's the complete workflow — from discovering creators to sending personalized outreach — using SociaVault's API and a few scripts.


The Pipeline

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ Discover │ →  │ Collect  │ →  │  Score   │ →  │ Extract  │ →  │  Send    │
│ Creators │    │  Data    │    │ & Filter │    │ Contact  │    │ Outreach │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
   Search          Profile         Fit score       Bio email      Personalized
   hashtags        + posts         based on        parsing        template with
   + keywords      enrichment     your criteria                   real metrics

Step 1: Discover Creators

Find potential influencers by searching hashtags, keywords, or exploring a competitor's followers:

const API_KEY = process.env.SOCIAVAULT_API_KEY;
const BASE = 'https://api.sociavault.com/v1/scrape';
const headers = { 'X-API-Key': API_KEY };

// Strategy 1: Search TikTok by keyword
async function searchTikTok(keyword) {
  const res = await fetch(
    `${BASE}/tiktok/search/users?query=${encodeURIComponent(keyword)}`,
    { headers }
  );
  const data = (await res.json()).data || [];
  return data.map(u => ({
    handle: u.uniqueId || u.unique_id,
    platform: 'tiktok',
    source: `search:${keyword}`
  }));
}

// Strategy 2: Search Instagram hashtag posts, extract authors
async function searchInstagramHashtag(hashtag) {
  const res = await fetch(
    `${BASE}/instagram/hashtag?hashtag=${encodeURIComponent(hashtag)}`,
    { headers }
  );
  const posts = (await res.json()).data || [];
  
  // Extract unique usernames from posts
  const handles = [...new Set(
    posts.map(p => p.owner?.username || p.user?.username).filter(Boolean)
  )];
  
  return handles.map(h => ({
    handle: h,
    platform: 'instagram',
    source: `hashtag:${hashtag}`
  }));
}

// Strategy 3: Check who a competitor follows
async function getCompetitorFollowing(handle) {
  const res = await fetch(
    `${BASE}/instagram/following?handle=${encodeURIComponent(handle)}`,
    { headers }
  );
  const users = (await res.json()).data || [];
  return users.map(u => ({
    handle: u.username,
    platform: 'instagram',
    source: `following:${handle}`
  }));
}

// Run all discovery strategies
async function discoverCreators() {
  const results = [];
  
  // Customize these for your niche
  const tiktokKeywords = ['fitness coach', 'meal prep', 'workout routine'];
  const instagramHashtags = ['fitnessmotivation', 'healthylifestyle'];
  const competitorHandles = ['competitor1', 'competitor2'];

  for (const kw of tiktokKeywords) {
    results.push(...await searchTikTok(kw));
    await new Promise(r => setTimeout(r, 1000));
  }

  for (const tag of instagramHashtags) {
    results.push(...await searchInstagramHashtag(tag));
    await new Promise(r => setTimeout(r, 1000));
  }

  for (const handle of competitorHandles) {
    results.push(...await getCompetitorFollowing(handle));
    await new Promise(r => setTimeout(r, 1000));
  }

  // Deduplicate by handle + platform
  const seen = new Set();
  return results.filter(r => {
    const key = `${r.platform}:${r.handle}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

Step 2: Enrich with Profile Data

Now pull full profile data for each discovered creator:

async function enrichProfile(handle, platform) {
  const endpoints = {
    instagram: 'instagram/profile',
    tiktok: 'tiktok/profile',
    twitter: 'twitter/profile',
  };

  const res = await fetch(
    `${BASE}/${endpoints[platform]}?handle=${encodeURIComponent(handle)}`,
    { headers }
  );
  const data = (await res.json()).data;
  if (!data) return null;

  if (platform === 'instagram') {
    return {
      handle: data.username,
      platform: 'instagram',
      name: data.full_name,
      followers: data.follower_count,
      following: data.following_count,
      posts: data.media_count,
      bio: data.biography || '',
      verified: data.is_verified,
      profileUrl: `https://instagram.com/${data.username}`,
      isPrivate: data.is_private,
      externalUrl: data.external_url
    };
  }

  if (platform === 'tiktok') {
    return {
      handle: data.user?.uniqueId,
      platform: 'tiktok',
      name: data.user?.nickname,
      followers: data.stats?.followerCount,
      following: data.stats?.followingCount,
      posts: data.stats?.videoCount,
      bio: data.user?.signature || '',
      verified: data.user?.verified,
      profileUrl: `https://tiktok.com/@${data.user?.uniqueId}`,
      isPrivate: false,
      externalUrl: data.user?.bioLink?.link
    };
  }

  return null;
}

async function enrichAll(creators) {
  const enriched = [];

  for (const creator of creators) {
    const profile = await enrichProfile(creator.handle, creator.platform);
    if (profile) {
      enriched.push({ ...profile, source: creator.source });
    }
    await new Promise(r => setTimeout(r, 800));
  }

  return enriched;
}

Step 3: Score and Filter

Not every creator is a good fit. Score them based on your campaign criteria:

function scoreCreator(profile, criteria = {}) {
  const {
    minFollowers = 5000,
    maxFollowers = 500000,
    minPosts = 20,
    preferVerified = false,
    mustNotBePrivate = true
  } = criteria;

  let score = 0;
  const reasons = [];

  // Follower range (0-30 points)
  if (profile.followers >= minFollowers && profile.followers <= maxFollowers) {
    score += 30;
    reasons.push('follower count in range');
  } else if (profile.followers < minFollowers) {
    reasons.push('too few followers');
    return { score: 0, reasons, pass: false };
  } else {
    score += 15; // Over max but not disqualifying
    reasons.push('above target range');
  }

  // Follower/following ratio (0-20 points)
  const ratio = profile.followers / Math.max(profile.following, 1);
  if (ratio > 10) {
    score += 20;
    reasons.push('excellent f/f ratio');
  } else if (ratio > 3) {
    score += 15;
    reasons.push('good f/f ratio');
  } else if (ratio > 1) {
    score += 5;
    reasons.push('average f/f ratio');
  } else {
    reasons.push('poor f/f ratio');
  }

  // Content volume (0-15 points)
  if (profile.posts >= minPosts) {
    score += 15;
    reasons.push('active poster');
  } else {
    reasons.push('low post count');
  }

  // Bio quality (0-15 points)
  if (profile.bio.length > 20) {
    score += 10;
    reasons.push('has bio');
  }
  if (profile.externalUrl) {
    score += 5;
    reasons.push('has website link');
  }

  // Verification bonus (0-10 points)
  if (profile.verified) {
    score += preferVerified ? 10 : 5;
    reasons.push('verified');
  }

  // Privacy check
  if (mustNotBePrivate && profile.isPrivate) {
    return { score: 0, reasons: ['private account'], pass: false };
  }

  // Contact info in bio (0-10 points)
  const emailRegex = /[\w.+-]+@[\w-]+\.[\w.-]+/;
  if (emailRegex.test(profile.bio)) {
    score += 10;
    reasons.push('email in bio');
  }

  return {
    score,
    reasons,
    pass: score >= 40  // Minimum qualifying score
  };
}

function filterAndRank(profiles, criteria) {
  return profiles
    .map(profile => {
      const result = scoreCreator(profile, criteria);
      return { ...profile, ...result };
    })
    .filter(p => p.pass)
    .sort((a, b) => b.score - a.score);
}

Step 4: Extract Contact Information

Pull email addresses from bios and external links:

function extractEmail(bio) {
  const emailRegex = /[\w.+-]+@[\w-]+\.[\w.-]+/;
  const match = bio.match(emailRegex);
  return match ? match[0] : null;
}

function extractContactInfo(profile) {
  const email = extractEmail(profile.bio);
  
  // Common business email patterns in bios
  const businessIndicators = [
    'collab', 'partner', 'business', 'inquir', 'sponsor',
    'brand deal', 'dm for', 'email', 'contact'
  ];
  
  const isOpenToCollabs = businessIndicators.some(
    indicator => profile.bio.toLowerCase().includes(indicator)
  );

  return {
    ...profile,
    email,
    isOpenToCollabs,
    contactMethod: email ? 'email' : (isOpenToCollabs ? 'dm' : 'none'),
    websiteFromBio: profile.externalUrl
  };
}

Step 5: Generate Personalized Outreach

Create email templates that reference real data points — because "loved your content" doesn't convince anyone:

function generateOutreach(profile, campaignContext) {
  const {
    brandName = 'Your Brand',
    campaignGoal = 'product collaboration',
    compensation = 'paid partnership'
  } = campaignContext;

  const followerTier = profile.followers > 100000 ? 'large' 
    : profile.followers > 10000 ? 'mid' : 'micro';

  const templates = {
    micro: {
      subject: `${brandName} x @${profile.handle} — Collaboration Idea`,
      body: `Hi ${profile.name || profile.handle},

I found your ${profile.platform} profile while researching ${profile.source.includes('hashtag') ? 'top creators in your niche' : 'creators in our space'}. With ${profile.followers.toLocaleString()} followers and ${profile.posts} posts, you're building something impressive.

${profile.verified ? "Your verified status caught our eye — " : ""}We're a ${brandName} and we'd love to explore a ${campaignGoal} with you.

Quick details:
• Type: ${compensation}
• Platform: ${profile.platform}
• Timeline: Flexible

Would you be open to a quick chat this week?

Best,
[Your Name]
${brandName}`
    },
    mid: {
      subject: `Collaboration Opportunity — ${brandName}`,
      body: `Hi ${profile.name || profile.handle},

We've been following your growth on ${profile.platform}${profile.followers.toLocaleString()} followers with a genuine community is exactly what we look for in partners.

${brandName} is looking for creators for a ${campaignGoal}, and your content style aligns with our brand.

What we're offering:
${compensation}
• Creative freedom with brand guidelines
• Long-term partnership potential

Interested? I'd love to share more details.

Best,
[Your Name]
${brandName}`
    },
    large: {
      subject: `${brandName} Partnership Proposal for @${profile.handle}`,
      body: `Hi ${profile.name || profile.handle},

I'll keep this short — we'd like to work with you.

${brandName} is running a ${campaignGoal} campaign and your ${profile.followers.toLocaleString()}-follower ${profile.platform} audience is a perfect fit.

We'd love to schedule a call with you or your management team to discuss terms.

Available this week?

Best,
[Your Name]
${brandName}`
    }
  };

  return templates[followerTier];
}

Step 6: Full Pipeline

Put it all together:

import fs from 'fs';

async function runOutreachPipeline(campaignConfig) {
  console.log('Step 1: Discovering creators...');
  const discovered = await discoverCreators();
  console.log(`  Found ${discovered.length} unique creators`);

  console.log('Step 2: Enriching profiles...');
  const enriched = await enrichAll(discovered);
  console.log(`  Enriched ${enriched.length} profiles`);

  console.log('Step 3: Scoring and filtering...');
  const qualified = filterAndRank(enriched, campaignConfig.criteria);
  console.log(`  ${qualified.length} creators qualified`);

  console.log('Step 4: Extracting contact info...');
  const withContact = qualified.map(extractContactInfo);
  const contactable = withContact.filter(c => c.email || c.isOpenToCollabs);
  console.log(`  ${contactable.length} have contact info`);

  console.log('Step 5: Generating outreach...');
  const outreach = contactable.map(creator => ({
    ...creator,
    outreach: generateOutreach(creator, campaignConfig)
  }));

  // Export to CSV for review before sending
  const csv = [
    'Handle,Platform,Followers,Score,Email,Contact Method,Subject,Profile URL',
    ...outreach.map(c => [
      c.handle,
      c.platform,
      c.followers,
      c.score,
      c.email || '',
      c.contactMethod,
      `"${c.outreach.subject}"`,
      c.profileUrl
    ].join(','))
  ].join('\n');

  fs.writeFileSync('outreach-list.csv', csv);
  console.log(`\nExported ${outreach.length} creators to outreach-list.csv`);

  // Also save full data as JSON
  fs.writeFileSync('outreach-full.json', JSON.stringify(outreach, null, 2));

  // Summary
  console.log('\n--- Pipeline Summary ---');
  console.log(`Discovered: ${discovered.length}`);
  console.log(`Enriched: ${enriched.length}`);
  console.log(`Qualified: ${qualified.length}`);
  console.log(`Contactable: ${contactable.length}`);
  console.log(`Ready for outreach: ${outreach.length}`);

  return outreach;
}

// Run it
runOutreachPipeline({
  criteria: {
    minFollowers: 5000,
    maxFollowers: 200000,
    minPosts: 30,
    preferVerified: false,
    mustNotBePrivate: true
  },
  brandName: 'FitGear',
  campaignGoal: 'product review',
  compensation: 'free product + $200-500'
});

Python version of the full pipeline:

import os
import csv
import json
import time
import re
import requests

API_KEY = os.environ["SOCIAVAULT_API_KEY"]
BASE = "https://api.sociavault.com/v1/scrape"
HEADERS = {"X-API-Key": API_KEY}

def search_tiktok(keyword):
    r = requests.get(f"{BASE}/tiktok/search/users", headers=HEADERS, params={"query": keyword})
    users = r.json().get("data", [])
    return [{"handle": u.get("uniqueId") or u.get("unique_id"), "platform": "tiktok", "source": f"search:{keyword}"} for u in users]

def enrich_instagram(handle):
    r = requests.get(f"{BASE}/instagram/profile", headers=HEADERS, params={"handle": handle})
    d = r.json().get("data")
    if not d:
        return None
    return {
        "handle": d.get("username"),
        "platform": "instagram",
        "name": d.get("full_name"),
        "followers": d.get("follower_count", 0),
        "following": d.get("following_count", 0),
        "posts": d.get("media_count", 0),
        "bio": d.get("biography", ""),
        "verified": d.get("is_verified", False),
        "url": f"https://instagram.com/{d.get('username')}",
        "external_url": d.get("external_url"),
    }

def score_creator(profile, min_followers=5000, max_followers=500000):
    score = 0
    if profile["followers"] < min_followers:
        return 0
    if min_followers <= profile["followers"] <= max_followers:
        score += 30
    ratio = profile["followers"] / max(profile["following"], 1)
    if ratio > 10:
        score += 20
    elif ratio > 3:
        score += 15
    if profile["posts"] >= 20:
        score += 15
    if len(profile["bio"]) > 20:
        score += 10
    email_match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", profile["bio"])
    if email_match:
        score += 10
    return score

def extract_email(bio):
    match = re.search(r"[\w.+-]+@[\w-]+\.[\w.-]+", bio)
    return match.group(0) if match else None

def run_pipeline():
    # Discover
    creators = search_tiktok("fitness coach")
    print(f"Discovered {len(creators)} creators")

    # Enrich (example: enrich as Instagram profiles)
    enriched = []
    for c in creators[:20]:
        profile = enrich_instagram(c["handle"])
        if profile:
            enriched.append(profile)
        time.sleep(0.8)
    print(f"Enriched {len(enriched)} profiles")

    # Score & filter
    scored = [(p, score_creator(p)) for p in enriched]
    qualified = [(p, s) for p, s in scored if s >= 40]
    qualified.sort(key=lambda x: x[1], reverse=True)
    print(f"Qualified: {len(qualified)}")

    # Export
    with open("outreach.csv", "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["Handle", "Platform", "Followers", "Score", "Email", "URL"])
        for p, s in qualified:
            writer.writerow([p["handle"], p["platform"], p["followers"], s, extract_email(p["bio"]) or "", p["url"]])

    print(f"Exported {len(qualified)} creators to outreach.csv")

run_pipeline()

Best Practices for Influencer Outreach

Do:

  • Personalize every message. Reference specific metrics or content.
  • Be upfront about compensation. Don't waste creators' time.
  • Follow up once. Then move on.
  • Track responses. A 10-15% reply rate is normal for cold outreach.
  • Respect "no." It's part of the process.

Don't:

  • Send identical mass emails
  • Promise "exposure" as payment
  • Automate the actual sending without review
  • Contact creators who explicitly say "no collabs"
  • Spam DMs on multiple platforms simultaneously

Pipeline Cost Estimate

StepAPI CallsCredits
Discovery (5 keywords)55
Enrichment (100 profiles)100100
Total per campaign105~105

That's roughly $1-2 per campaign run for a list of 100 qualified, scored, contact-enriched creators. Compare that to influencer discovery platforms charging $200-500/month.


Get Started

Sign up free and build your first outreach pipeline today.


Found this helpful?

Share it with others who might benefit

Ready to Try SociaVault?

Start extracting social media data with our powerful API. No credit card required.