Back to Blog
Tutorial

Build a Social Media CRM in Notion Using API Data

April 9, 2026
8 min read
S
By SociaVault Team
NotionCRMIntegrationInfluencer MarketingSocial Media DataNo-Code

Build a Social Media CRM in Notion Using API Data

Notion is already where your team lives. Instead of switching to another tool for influencer tracking or competitor monitoring, you can turn Notion into a social media CRM — automatically populated with real data from SociaVault's API.

By the end of this guide, you'll have a Notion database that:

  • Auto-fills profile stats for any creator you add
  • Tracks follower growth over time
  • Flags accounts that meet your campaign criteria
  • Updates weekly without manual work

No expensive influencer marketing platforms. Just Notion + a script that runs on schedule.


What You'll Build

A Notion database that looks like this:

CreatorPlatformFollowersEng. RateBioVerifiedStatusLast Updated
@natgeoInstagram284M0.12%Inspiring...Contacted2026-04-08
@charlidamelioTikTok155M4.2%❤️In Review2026-04-08
@mkbhdYouTube19.8MShortlisted2026-04-08

The data columns fill automatically. You only manage the workflow columns (Status, Notes, Campaign).


Step 1: Create the Notion Database

Create a new full-page database in Notion with these properties:

PropertyTypePurpose
CreatorTitleUsername/handle
PlatformSelectInstagram, TikTok, YouTube, Twitter
FollowersNumberAuto-filled
FollowingNumberAuto-filled
Posts CountNumberAuto-filled
Engagement RateNumber (%)Calculated
BiographyTextAuto-filled
Is VerifiedCheckboxAuto-filled
Profile URLURLAuto-filled
StatusSelectProspect, Contacted, Negotiating, Active, Declined
CampaignMulti-selectYour campaign tags
NotesTextManual notes
Last UpdatedDateAuto-filled timestamp

Step 2: Set Up the Notion API

  1. Go to developers.notion.com
  2. Create a new integration — name it "Social Media CRM"
  3. Copy the Internal Integration Token
  4. Share your database with the integration (click ••• → Connections → your integration)
  5. Copy the database ID from the URL: notion.so/{database_id}?v=...

Step 3: The Sync Script

This Node.js script reads creators from your Notion database and enriches them with SociaVault data:

const NOTION_KEY = process.env.NOTION_KEY;
const NOTION_DB = process.env.NOTION_DB_ID;
const SOCIAVAULT_KEY = process.env.SOCIAVAULT_API_KEY;

// Get all creators from Notion database
async function getNotionCreators() {
  const res = await fetch(`https://api.notion.com/v1/databases/${NOTION_DB}/query`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOTION_KEY}`,
      'Notion-Version': '2022-06-28',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      filter: {
        property: 'Platform',
        select: { is_not_empty: true }
      }
    })
  });
  return (await res.json()).results;
}

// Get social media data from SociaVault
async function getProfileData(handle, platform) {
  const platformEndpoints = {
    'Instagram': 'instagram/profile',
    'TikTok': 'tiktok/profile',
    'YouTube': 'youtube/channel',
    'Twitter': 'twitter/profile'
  };

  const endpoint = platformEndpoints[platform];
  if (!endpoint) return null;

  const param = platform === 'YouTube' ? 'handle' : 'handle';
  const url = `https://api.sociavault.com/v1/scrape/${endpoint}?${param}=${encodeURIComponent(handle)}`;
  
  const res = await fetch(url, {
    headers: { 'X-API-Key': SOCIAVAULT_KEY }
  });
  return (await res.json()).data;
}

// Normalize data across platforms
function normalizeProfile(data, platform) {
  switch (platform) {
    case 'Instagram':
      return {
        followers: data.follower_count,
        following: data.following_count,
        posts: data.media_count,
        bio: data.biography,
        verified: data.is_verified,
        url: `https://instagram.com/${data.username}`
      };
    case 'TikTok':
      return {
        followers: data.stats?.followerCount,
        following: data.stats?.followingCount,
        posts: data.stats?.videoCount,
        bio: data.user?.signature,
        verified: data.user?.verified,
        url: `https://tiktok.com/@${data.user?.uniqueId}`
      };
    case 'YouTube':
      return {
        followers: parseInt(data.subscriberCount) || 0,
        following: 0,
        posts: parseInt(data.videoCount) || 0,
        bio: data.description?.substring(0, 200),
        verified: false,
        url: `https://youtube.com/@${data.customUrl || data.handle}`
      };
    case 'Twitter':
      return {
        followers: data.legacy?.followers_count,
        following: data.legacy?.friends_count,
        posts: data.legacy?.statuses_count,
        bio: data.legacy?.description,
        verified: data.is_blue_verified,
        url: `https://x.com/${data.core?.screen_name || data.legacy?.screen_name}`
      };
    default:
      return null;
  }
}

// Update Notion page with profile data
async function updateNotionPage(pageId, profile) {
  await fetch(`https://api.notion.com/v1/pages/${pageId}`, {
    method: 'PATCH',
    headers: {
      'Authorization': `Bearer ${NOTION_KEY}`,
      'Notion-Version': '2022-06-28',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      properties: {
        'Followers': { number: profile.followers },
        'Following': { number: profile.following },
        'Posts Count': { number: profile.posts },
        'Biography': { rich_text: [{ text: { content: (profile.bio || '').substring(0, 2000) } }] },
        'Is Verified': { checkbox: profile.verified || false },
        'Profile URL': { url: profile.url },
        'Last Updated': { date: { start: new Date().toISOString().split('T')[0] } }
      }
    })
  });
}

// Main sync function
async function syncCRM() {
  const creators = await getNotionCreators();
  console.log(`Found ${creators.length} creators to sync`);

  for (const page of creators) {
    const handle = page.properties.Creator?.title?.[0]?.plain_text;
    const platform = page.properties.Platform?.select?.name;
    
    if (!handle || !platform) continue;

    try {
      const rawData = await getProfileData(handle, platform);
      if (!rawData) continue;

      const profile = normalizeProfile(rawData, platform);
      if (!profile) continue;

      await updateNotionPage(page.id, profile);
      console.log(`✓ Updated ${handle} (${platform})`);

      // Rate limiting
      await new Promise(r => setTimeout(r, 1000));
    } catch (err) {
      console.error(`✗ Failed ${handle}: ${err.message}`);
    }
  }
  
  console.log('Sync complete');
}

syncCRM();

The Python version:

import os
import time
import requests
from datetime import date

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

PLATFORM_ENDPOINTS = {
    "Instagram": "instagram/profile",
    "TikTok": "tiktok/profile",
    "YouTube": "youtube/channel",
    "Twitter": "twitter/profile",
}

def get_notion_creators():
    url = f"https://api.notion.com/v1/databases/{NOTION_DB}/query"
    headers = {
        "Authorization": f"Bearer {NOTION_KEY}",
        "Notion-Version": "2022-06-28",
        "Content-Type": "application/json",
    }
    body = {"filter": {"property": "Platform", "select": {"is_not_empty": True}}}
    return requests.post(url, headers=headers, json=body).json()["results"]

def get_profile(handle, platform):
    endpoint = PLATFORM_ENDPOINTS.get(platform)
    if not endpoint:
        return None
    r = requests.get(f"{BASE}/{endpoint}", headers=HEADERS, params={"handle": handle})
    return r.json().get("data")

def normalize(data, platform):
    if platform == "Instagram":
        return {
            "followers": data.get("follower_count", 0),
            "following": data.get("following_count", 0),
            "posts": data.get("media_count", 0),
            "bio": data.get("biography", ""),
            "verified": data.get("is_verified", False),
            "url": f"https://instagram.com/{data.get('username', '')}",
        }
    elif platform == "TikTok":
        stats = data.get("stats", {})
        user = data.get("user", {})
        return {
            "followers": stats.get("followerCount", 0),
            "following": stats.get("followingCount", 0),
            "posts": stats.get("videoCount", 0),
            "bio": user.get("signature", ""),
            "verified": user.get("verified", False),
            "url": f"https://tiktok.com/@{user.get('uniqueId', '')}",
        }
    elif platform == "Twitter":
        legacy = data.get("legacy", {})
        return {
            "followers": legacy.get("followers_count", 0),
            "following": legacy.get("friends_count", 0),
            "posts": legacy.get("statuses_count", 0),
            "bio": legacy.get("description", ""),
            "verified": data.get("is_blue_verified", False),
            "url": f"https://x.com/{legacy.get('screen_name', '')}",
        }
    return None

def update_notion(page_id, profile):
    url = f"https://api.notion.com/v1/pages/{page_id}"
    headers = {
        "Authorization": f"Bearer {NOTION_KEY}",
        "Notion-Version": "2022-06-28",
        "Content-Type": "application/json",
    }
    body = {
        "properties": {
            "Followers": {"number": profile["followers"]},
            "Following": {"number": profile["following"]},
            "Posts Count": {"number": profile["posts"]},
            "Biography": {"rich_text": [{"text": {"content": profile["bio"][:2000]}}]},
            "Is Verified": {"checkbox": profile["verified"]},
            "Profile URL": {"url": profile["url"]},
            "Last Updated": {"date": {"start": str(date.today())}},
        }
    }
    requests.patch(url, headers=headers, json=body)

def main():
    creators = get_notion_creators()
    print(f"Syncing {len(creators)} creators...")

    for page in creators:
        props = page["properties"]
        title_arr = props.get("Creator", {}).get("title", [])
        handle = title_arr[0]["plain_text"] if title_arr else None
        platform = props.get("Platform", {}).get("select", {}).get("name")

        if not handle or not platform:
            continue

        try:
            raw = get_profile(handle, platform)
            if not raw:
                continue
            profile = normalize(raw, platform)
            if not profile:
                continue
            update_notion(page["id"], profile)
            print(f"  ✓ {handle} ({platform})")
            time.sleep(1)
        except Exception as e:
            print(f"  ✗ {handle}: {e}")

    print("Done.")

main()

Step 4: Automate the Sync

Option A: Cron Job (VPS/Server)

# Run every day at 8 AM
0 8 * * * cd /path/to/crm-sync && node sync.js >> sync.log 2>&1

Option B: GitHub Actions (Free)

Create .github/workflows/crm-sync.yml:

name: Sync CRM
on:
  schedule:
    - cron: '0 8 * * *'
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: node sync.js
        env:
          NOTION_KEY: ${{ secrets.NOTION_KEY }}
          NOTION_DB_ID: ${{ secrets.NOTION_DB_ID }}
          SOCIAVAULT_API_KEY: ${{ secrets.SOCIAVAULT_API_KEY }}

Option C: Make.com

Use the Make.com integration guide to build a visual workflow that syncs Notion automatically.


Adding Engagement Rate Calculation

You can calculate engagement rates by also pulling recent posts:

async function getEngagementRate(handle, platform) {
  if (platform !== 'Instagram') return null;

  // Get profile for follower count
  const profileRes = await fetch(
    `https://api.sociavault.com/v1/scrape/instagram/profile?handle=${handle}`,
    { headers: { 'X-API-Key': SOCIAVAULT_KEY } }
  );
  const profile = (await profileRes.json()).data;
  
  // Get recent posts
  const postsRes = await fetch(
    `https://api.sociavault.com/v1/scrape/instagram/posts?handle=${handle}`,
    { headers: { 'X-API-Key': SOCIAVAULT_KEY } }
  );
  const posts = (await postsRes.json()).data;

  if (!posts?.length || !profile?.follower_count) return null;

  const totalEngagement = posts.reduce((sum, post) => {
    return sum + (post.like_count || 0) + (post.comment_count || 0);
  }, 0);

  return (totalEngagement / posts.length / profile.follower_count) * 100;
}

Add this to your sync loop and update the Notion Engagement Rate property.


Notion Views for Your CRM

Once data is flowing, create these filtered views:

"Hot Prospects" View

  • Filter: Followers > 10,000 AND Status = "Prospect"
  • Sort: Followers descending

"Campaign Ready" View

  • Filter: Status = "Shortlisted" AND Is Verified = true
  • Group by: Campaign

"Weekly Growth" View

  • Filter: Last Updated = this week
  • Sort: Last Updated descending

"Platform Mix" View

  • Group by: Platform
  • Show: Count, Average followers

SociaVault CRM vs. Paid Platforms

FeatureSociaVault + NotionGrinCreatorIQ
Monthly Cost~$10-30$2,500+Custom ($$$)
Profiles trackedUnlimitedPlan-dependentPlan-dependent
Custom fields✅ UnlimitedLimitedLimited
Workflow integration✅ Notion nativeSeparate toolSeparate tool
Multi-platform✅ 10+ platformsInstagram focusMulti
Ownership✅ Your dataVendor lock-inVendor lock-in

For most agencies and small brands, SociaVault + Notion beats $2,500/month platforms — especially when you only need data tracking, not full campaign management.


Get Started

Sign up free and start building your Notion CRM 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.