Cross-Platform Creator Profiles with One API (2026)

Look up the same creator on TikTok, YouTube, Instagram, and X with one schema and one API key — no per-platform scrapers, no drifting field names.

Agency rosters and creator databases usually start as a spreadsheet column per network: TikTok handle here, YouTube there, Instagram somewhere else. The painful part is not fetching four pages — it is deciding whether @janesmith on TikTok is the same person as @jane.smith on Instagram, then normalizing follower counts that YouTube calls subscribers and everyone else calls followers.

Product teams building creator intelligence need four things at once: discovery when the roster is empty, identity linking when handles disagree, comparable metrics for ranking, and fresh numbers before a client meeting. Home-grown scrapers tend to ship those one at a time, each with its own JSON shape and breakage schedule.

This guide walks through an identity resolution pass: the same workflow intelligence teams run before a campaign shortlist, but with API calls you can schedule, log, and replay. We use a fictional mid-tier fitness creator (sarahfit) as the running example; swap in your roster handles.

The short version

Call /v1/tiktok/profiles/{handle}, /v1/youtube/channel, /v1/instagram/profiles/{handle}, and /v1/twitter/profiles/{handle} with one API key. Every response shares the same envelope — branch on data.lookupStatus, merge into one warehouse row, and log meta.creditsCharged per platform.

You'll need an API key and the TypeScript SDK or curl. Try a live lookup in the Playground.

Why same handle ≠ same person

DIY scrapers force you to maintain four codepaths that break on different schedules. Social Fetch removes that maintenance, but it does not remove the identity problem: platforms do not share a global user ID you can join on.

PitfallWhat goes wrongWhat to do instead
Exact-handle fan-out@nova on TikTok is a dancer; @nova on X is a game studioTreat each platform lookup as a candidate edge, not ground truth
Ignoring bio linksCRM says sarahfit everywhere; her YouTube is SarahFitOfficialParse profile.bio and channel.description for declared URLs first
HTTP 200 = dataPrivate Instagram returns 200 with lookupStatus: "private"Branch on lookupStatus, not status code
Stale CSV importsFollower count from last quarter wins the sortStore capturedAt and re-pull before client deliverables

When a platform changes its HTML, we absorb it upstream. Your parser keeps reading metrics.followers (or the YouTube equivalent inside metrics) instead of re-mapping subscriberCount from a new JSON blob every month.

DIY per platformSocial Fetch
Four proxy stacks, four breakage schedulesOne maintained API surface
Field names differ (followers vs subscriberCount)Normalized metrics + profile objects
Auth and rate-limit logic duplicatedOne x-api-key header on metered routes

The response envelope

Every profile route returns the same top-level keys. Your ETL can treat TikTok and Instagram identically at the outer layer:

{
  "data": {
    "lookupStatus": "found",
    "profile": {
      "handle": "sarahfit",
      "displayName": "Sarah Fit",
      "verified": false,
      "bio": "YT: youtube.com/@SarahFitOfficial"
    },
    "metrics": {
      "followers": 284000,
      "following": 412,
      "likes": 4200000,
      "videos": 318
    }
  },
  "meta": {
    "requestId": "req_01example",
    "creditsCharged": 1,
    "version": "v1"
  }
}

YouTube swaps profile for channel and may include metrics.subscribers — still inside data.metrics. Branch on lookupStatus before reading counts; not_found responses often omit metrics entirely.

Identity pipeline at a glance

Phase 0: Sketch the creator row

Before the first API call, decide what one person looks like in your database. A flat table works for agencies; graph products often use a parent creators row and child creator_platforms rows.

Minimum columns worth defining upfront:

ColumnExampleSource
creatorIdcrt_8f2ayour UUID
platformtiktokenum per network
handlesarahfitpath param you queried
lookupStatusfounddata.lookupStatus from that call
followers284000metrics.followers or subscribers
capturedAtISO timestampyour clock at ingest
requestIdreq_01…meta.requestId for support

Also reserve matchMethod (declared_link, exact_handle, fuzzy_name, manual) so analysts know why two handles were linked. You will thank yourself when a client disputes a merge.

Phase 1: Anchor on one network

Pick the network where your seed data is most trustworthy — usually whichever handle the client gave you first, or TikTok if you discovered the creator via search.

TikTok profile lookup:

Request
curl -sS \
  -H "x-api-key: $SOCIALFETCH_API_KEY" \
  "https://api.socialfetch.dev/v1/tiktok/profiles/charlidamelio"

Reference: Get TikTok profile. One credit per completed lookup, including not_found.

Read the anchor response before fanning out:

import { SocialFetchClient } from "@socialfetch/sdk";

const client = new SocialFetchClient({
  apiKey: process.env.SOCIALFETCH_API_KEY!,
});

const anchor = await client.tiktok.getProfile({ handle: "sarahfit" });

if (!anchor.ok) {
  console.error(anchor.error.code, anchor.error.requestId);
  process.exit(1);
}

const { lookupStatus, profile, metrics } = anchor.value.data;

if (lookupStatus !== "found") {
  // Stop or try alternate spellings — don't burn 3 more credits blindly
  console.log("anchor unresolved:", lookupStatus);
} else {
  console.log(profile?.displayName, metrics?.followers);
}

If the anchor is not_found, try common variants (sarah.fit, sarah_fit) before parallelizing the other networks. Each attempt is another credit.

Instagram and YouTube bios are where creators paste their “real” handles. Pull Instagram first when your CRM only has a first name or brand string — link-in-bio pages often list the TikTok @ explicitly.

Request
const response = await fetch(
  "https://api.socialfetch.dev/v1/instagram/profiles/instagram",
  {
    headers: {
      "x-api-key": process.env.SOCIALFETCH_API_KEY,
    },
  }
);

const body = await response.json();

console.log(response.status, body);

Reference: Get Instagram profile.

YouTube channel lookup accepts handle, channel ID, or URL in one request:

Request
const params = new URLSearchParams({"handle":"mrbeast"});

const response = await fetch(
  `https://api.socialfetch.dev/v1/youtube/channel?${params.toString()}`,
  {
    headers: {
      "x-api-key": process.env.SOCIALFETCH_API_KEY,
    },
  }
);

const body = await response.json();

console.log(response.status, body);

Reference: Get YouTube channel.

Extract handles from bio text with boring string rules before fuzzy matching:

function extractHandles(bio: string | undefined): string[] {
  if (!bio) return [];
  const matches = bio.match(/@[\w.]+/g) ?? [];
  return [...new Set(matches.map((m) => m.slice(1).toLowerCase()))];
}

function extractUrls(bio: string | undefined): string[] {
  if (!bio) return [];
  return bio.match(/https?:\/\/[^\s]+/g) ?? [];
}

Feed extractUrls output into YouTube's url query param when you see youtube.com/@… or youtu.be/… links. For Linktree-style domains, fetch the profile once, store the raw bio, and let a human confirm — do not auto-merge on a landing-page redirect alone.

X/Twitter uses the same path-parameter pattern as Instagram:

curl -sS \
  -H "x-api-key: $SOCIALFETCH_API_KEY" \
  "https://api.socialfetch.dev/v1/twitter/profiles/sarahfit"

Reference: Get Twitter profile.

Phase 3: Fan out profile lookups

Once you have a handle list per platform (exact handle plus any bio-derived candidates), resolve them in parallel. Every platform returns the same top-level shape, so one loop handles all four.

Example
typescript
import { SocialFetchClient } from "@socialfetch/sdk";

const client = new SocialFetchClient({
  apiKey: process.env.SOCIALFETCH_API_KEY!,
});

const handle = "mrbeast";

const [tiktok, youtube, instagram] = await Promise.all([
  client.tiktok.getProfile({ handle }),
  client.youtube.getChannel({ handle }),
  client.instagram.getProfile({ handle }),
]);

for (const result of [tiktok, youtube, instagram]) {
  if (!result.ok) {
    console.error(result.error.code, result.error.requestId);
    continue;
  }
  console.log(result.value.data.lookupStatus, result.value.meta.creditsCharged);
}

Production version with per-platform handle map and status logging:

import { SocialFetchClient } from "@socialfetch/sdk";

const client = new SocialFetchClient({
  apiKey: process.env.SOCIALFETCH_API_KEY!,
});

type Platform = "tiktok" | "youtube" | "instagram" | "twitter";

const candidates: Record<Platform, string> = {
  tiktok: "sarahfit",
  youtube: "SarahFitOfficial", // from Instagram bio, not guessed
  instagram: "sarahfit",
  twitter: "sarahfit",
};

const lookups = await Promise.all([
  client.tiktok.getProfile({ handle: candidates.tiktok }),
  client.youtube.getChannel({ handle: candidates.youtube }),
  client.instagram.getProfile({ handle: candidates.instagram }),
  client.twitter.getProfile({ handle: candidates.twitter }),
]);

const platforms: Platform[] = ["tiktok", "youtube", "instagram", "twitter"];

for (let i = 0; i < lookups.length; i++) {
  const result = lookups[i];
  const platform = platforms[i];

  if (!result.ok) {
    console.error(platform, result.error.code, result.error.requestId);
    continue;
  }

  const { lookupStatus, metrics } = result.value.data;
  console.log(platform, lookupStatus, metrics, result.value.meta.creditsCharged);
}

Each result carries its own meta.creditsCharged. A four-platform pass is typically four credits when all complete, even if two return not_found.

Concurrency: metered routes have no published rate cap; your balance is the limit. For imports of 5,000 creators, batch Promise.all in chunks of 50–100 so a single failure does not stall the whole file.

Phase 4: Disambiguate when handles diverge

Exact-handle fan-out is step one, not the finish line. Use a short decision tree:

  1. Declared link in bio — if Instagram bio contains youtube.com/@SarahFitOfficial, that edge outranks guessing sarahfit on YouTube.
  2. Display name + avatar hash — weak signal alone; useful when handles differ by one character (sarahfit vs sarah.fit).
  3. Follower magnitude — a 2M TikTok paired with a 200-subscriber YouTube is probably wrong unless the bio says "new channel."
  4. Manual queue — anything below your confidence threshold gets matchMethod: "manual" and skips auto-merge.

Store unresolved platforms as lookupStatus: "not_found" on the child row instead of copying the anchor's metrics. Empty cells in a vetting UI are honest; duplicated follower totals are not.

Worked example — CRM says sarahfit everywhere, but Instagram bio says youtube.com/@SarahFitOfficial:

StepActionOutcome
1GET /v1/instagram/profiles/sarahfitfound, bio contains YouTube URL
2GET /v1/youtube/channel?handle=sarahfitnot_found — wrong handle
3GET /v1/youtube/channel?handle=SarahFitOfficialfound, 98k subscribers
4Merge with matchMethod: "declared_link"YouTube row linked to parent crt_8f2a

That middle not_found call still costs a credit. Parse bios before guessing identical handles — it is cheaper than fanning out blind and cleaner for your audit log.

Confidence scoring (optional, but teams ask for it):

function linkConfidence(signals: {
  declaredUrl: boolean;
  exactHandle: boolean;
  displayNameMatch: boolean;
  followerRatio: number; // max/min across platforms, 1 = identical
}): "high" | "medium" | "low" {
  if (signals.declaredUrl) return "high";
  if (signals.exactHandle && signals.displayNameMatch && signals.followerRatio < 5) {
    return "medium";
  }
  return "low";
}

Send low rows to a human queue. Auto-merge only high in production pipelines.

Phase 5: Normalize into one card

Map platform responses into one UI card without if (youtube) subscriberCount else followers scattered through React components:

type CreatorCard = {
  creatorId: string;
  displayName: string | null;
  totalReach: number;
  platforms: Array<{
    platform: Platform;
    handle: string;
    lookupStatus: string;
    followers: number | null;
    verified: boolean;
    capturedAt: string;
    requestId: string;
  }>;
};

function followersFromEnvelope(platform: Platform, data: Record<string, unknown>): number | null {
  const metrics = data.metrics as Record<string, number> | undefined;
  if (!metrics) return null;
  // YouTube subscribers live in the same metrics object
  return metrics.followers ?? metrics.subscribers ?? null;
}

Pull displayName from whichever found platform you trust most — often Instagram or TikTok. YouTube's channel.title can differ ("Sarah Fit - Workouts" vs @sarahfit); show per-platform names in a detail drawer if they disagree.

Example merged JSON your dashboard API might return:

{
  "creatorId": "crt_8f2a",
  "displayName": "Sarah Fit",
  "totalReach": 412000,
  "platforms": [
    {
      "platform": "tiktok",
      "handle": "sarahfit",
      "lookupStatus": "found",
      "followers": 284000,
      "verified": false,
      "capturedAt": "2026-06-30T14:00:00.000Z",
      "requestId": "req_01example"
    },
    {
      "platform": "youtube",
      "handle": "SarahFitOfficial",
      "lookupStatus": "found",
      "followers": 98000,
      "verified": true,
      "capturedAt": "2026-06-30T14:00:01.000Z",
      "requestId": "req_02example"
    },
    {
      "platform": "instagram",
      "handle": "sarahfit",
      "lookupStatus": "found",
      "followers": 30000,
      "verified": false,
      "capturedAt": "2026-06-30T14:00:02.000Z",
      "requestId": "req_03example"
    },
    {
      "platform": "twitter",
      "handle": "sarahfit",
      "lookupStatus": "not_found",
      "followers": null,
      "verified": false,
      "capturedAt": "2026-06-30T14:00:03.000Z",
      "requestId": "req_04example"
    }
  ]
}

totalReach is a business rule — sum only found platforms, or sum with a cap per network. Document the rule in code; the API will not pick for you.

Phase 6: Recent posts for engagement checks

Follower count gets you on a shortlist; median views tells you whether anyone watches. After profiles resolve, pull recent uploads for creators above your cutoff.

TikTok videos for the anchor handle:

curl -sS \
  -H "x-api-key: $SOCIALFETCH_API_KEY" \
  "https://api.socialfetch.dev/v1/tiktok/profiles/sarahfit/videos?sortBy=latest"

Instagram posts paginate the same way:

curl -sS \
  -H "x-api-key: $SOCIALFETCH_API_KEY" \
  "https://api.socialfetch.dev/v1/instagram/profiles/sarahfit/posts"

References: TikTok profile videos · Instagram profile posts.

Compute engagement in your warehouse — the API returns per-item metrics, not a black-box score:

const videos = result.value.data.videos ?? [];
const views = videos.map((v) => v.metrics?.views ?? 0).filter((n) => n > 0);
const medianViews = views.sort((a, b) => a - b)[Math.floor(views.length / 2)] ?? 0;

Call GET /v1/tiktok/profiles/{handle} before interpreting an empty video list; list routes do not always expose private the same way profile routes do. See the capability matrix for route-specific lookupStatus behavior.

For spoken-content classification, pair post captions with transcript routes when hashtags are thin.

TypeScript pagination for TikTok videos (same cursor pattern as Reddit search in our other guides):

const allVideos = [];
let cursor: string | undefined;

do {
  const result = await client.tiktok.getProfileVideos({
    handle: "sarahfit",
    sortBy: "latest",
    cursor,
  });

  if (!result.ok) break;

  allVideos.push(...(result.value.data.videos ?? []));
  cursor = result.value.data.page.nextCursor ?? undefined;
} while (cursor);

const avgViews =
  allVideos.reduce((sum, v) => sum + (v.metrics?.views ?? 0), 0) /
  (allVideos.length || 1);

Stop after two or three pages unless you are building a forensic report — most vetting workflows only need the last 12–20 posts.

Identity resolution assumes you already have a name. Discovery fills the top of the funnel — keyword search on TikTok users when you expand a vertical.

curl -sS \
  -H "x-api-key: $SOCIALFETCH_API_KEY" \
  -G "https://api.socialfetch.dev/v1/tiktok/users/search" \
  --data-urlencode "query=home workout"
const result = await client.tiktok.searchUsers({ query: "home workout" });

if (result.ok) {
  for (const user of result.value.data.users ?? []) {
    console.log(user.handle, user.metrics?.followers);
  }
}

Reference: Search TikTok users. Persist each handle with the query that found it so you can re-run the same list after a model refresh.

Filter client-side on follower floors before spending four credits per person on full cross-platform enrichment. A search page plus 200 profile fan-outs is 201 credits — fine for a targeted import, expensive as a default loop.

TikTok video search (GET /v1/tiktok/search) is an alternate seed when you care about content niche more than account name — pull handles from high-view videos, then run Phase 1–5 on the unique set. Deduplicate handles before enrichment; the same creator may appear in twenty search hits.

Store rows your team can audit

Flatten API responses into rows your ops team already uses (Airtable, Postgres, Google Sheets):

ColumnSourceWhy
matchMethodyour resolverExplains auto vs manual links
lookupStatusper platformDrives UI badges (private, not_found)
followersmetricsSort and filter
verifiedprofile.verifiedContract eligibility
requestIdmeta.requestIdSupport trail when a number looks wrong
creditsChargedmeta.creditsChargedClient-level cost allocation

Re-pull on a cron before QBRs or campaign reviews. Credits are prepaid and do not expire between quarterly bursts, so a quiet graph between agency reporting cycles does not run up a subscription.

Weekly refresh job pattern (same idea as the social listening guide, but per creator row):

const roster = await db.creators.findMany({ where: { active: true } });

for (const row of roster) {
  const handles = await db.creatorPlatforms.findMany({
    where: { creatorId: row.id },
  });

  for (const platform of handles) {
    // re-call the matching profile endpoint; upsert followers + lookupStatus
    await sleep(50); // modest spacing during large rosters
  }
}

Write meta.requestId on every upsert. When a client says "this number is wrong," you can trace the exact lookup without re-running the whole import.

Troubleshooting

YouTube not_found but the channel loads in a browser

  • Pass the full channel URL: ?url=https://www.youtube.com/@SarahFitOfficial instead of a shortened handle.
  • Handles are case-sensitive on some legacy channels — copy the @ from the address bar.
  • @handle and /c/ChannelName URLs refer to different identifiers; use what the creator's bio declares.

Instagram found but follower count is null

  • private accounts may return identity fields without public metrics. Check lookupStatus before showing zeros.
  • Re-fetch if the profile was public five minutes ago; creators flip privacy settings often.

TikTok handle exists; fan-out returns mixed results

  • Regional or banned accounts can return not_found on one network only. Log requestId and retry once before marking dead.
  • Strip @ from path params — /v1/tiktok/profiles/@sarahfit is wrong; use sarahfit.

Merged card shows impossible total reach

  • Dedupe by person, not by handle string. Two different people named alex on TikTok and YouTube should stay two parent rows until bio links confirm a merge.
  • Do not sum private or not_found platforms into totalReach unless your product spec explicitly says to.

lookup_failed or HTTP 503

  • Not charged. Retry with exponential backoff; pass the failed meta.requestId to support if it persists.
  • A burst of 10,000 parallel profile calls is unnecessary — chunk work.

Empty video/post lists on a found profile

  • Genuinely inactive creators exist. Compare against metrics.videos or post count on the profile object.
  • Paginate — first page may be empty for sort windows; check data.page.hasMore.

Rate limits

  • No hard rate cap on metered routes; your credit balance is the practical limit. Chunk large backfills (50–100 concurrent profile calls) so retries stay manageable.

SDK Result errors vs lookupStatus

  • result.ok === false means transport or auth failed — check result.error.code and result.error.requestId.
  • result.ok === true with lookupStatus: "not_found" is a successful, billable domain outcome. Do not retry those unless the handle string changed.

Billing and credit budgeting

Each profile lookup is typically 1 credit when the call completes, including not_found and private. You are not charged for validation errors, lookup_failed, or 503 temporarily_unavailable.

Rough math for planning:

StepCalls per creatorCredits (approx.)
Anchor TikTok profile11
Bio-driven Instagram + YouTube + X fan-out33
Recent TikTok videos (1 page)11
Recent Instagram posts (1 page)11
Enriched creator row66

Import 500 creators with full enrichment: ~3,000 credits. Discovery search adds 1 credit per results page before you filter.

Agency tip: log meta.creditsCharged per creatorId and bill clients per import job using your own ledger. Public data only — lawful use is on you under Terms. Full matrix: Credits.

Batch import spreadsheet (500 rows, enrich everyone):

StageCalculationCredits
Profile fan-out (4 networks × 500)2,0002,000
Skip not_found anchors (~8%)−160 × 3 remaining calls−480
Video/post page for top 200 by followers200 × 2400
Net estimate~1,920

Run anchor lookup first and short-circuit dead rows — most savings come from not fanning out when TikTok returns not_found on a typoed handle.

What you can build

  • Influencer vetting — reconcile CRM handles against live follower counts and flag private accounts before outreach.
  • Cross-platform leaderboards — rank by summed reach with per-platform drill-down when totals tie.
  • Agency reporting — one dashboard template; platform tabs read the same JSON parser.
  • Creator graph products — parent/child rows with matchMethod provenance for merge audits.
  • Engagement benchmarks — median views per post across TikTok and Instagram for the same resolved person.

For a product-level overview of discovery, scoring, and refresh cadence, see the creator intelligence use case.


Next steps: Playground · Creator intelligence use case · TikTok scraping guide · API reference · Pricing