Creator rosters usually have a handle per network. The hard part 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.
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. This guide uses a fictional fitness creator (sarahfit) as the running example.
You'll need an API key and the TypeScript SDK or curl. Try a live lookup in the Playground.
Why same handle ≠ same person
Platforms do not share a global user ID you can join on.
| Pitfall | What goes wrong | What to do instead |
|---|---|---|
| Exact-handle fan-out | @nova on TikTok is a dancer; @nova on X is a game studio | Treat each platform lookup as a candidate edge, not ground truth |
| Ignoring bio links | CRM says sarahfit everywhere; her YouTube is SarahFitOfficial | Parse profile.bio and channel.description for declared URLs first |
| HTTP 200 = data | Private Instagram returns 200 with lookupStatus: "private" | Branch on lookupStatus, not status code |
| Stale CSV imports | Follower count from last quarter wins the sort | Store 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 field names every month.
| DIY per platform | Social Fetch |
|---|---|
| Four proxy stacks, four breakage schedules | One maintained API surface |
Field names differ (followers vs subscriberCount) | Normalized metrics + profile objects |
| Auth and rate-limit logic duplicated | One x-api-key header on metered routes |
The response envelope
Every profile route returns the same top-level keys:
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
Define the creator row
Decide what one person looks like in your database before the first API call. A flat table works for agencies; graph products often use a parent creators row and child creator_platforms rows.
| Column | Example | Source |
|---|---|---|
creatorId | crt_8f2a | your UUID |
platform | tiktok | enum per network |
handle | sarahfit | path param you queried |
lookupStatus | found | data.lookupStatus from that call |
followers | 284000 | metrics.followers or subscribers |
capturedAt | ISO timestamp | your clock at ingest |
requestId | req_01… | meta.requestId for support |
Also reserve matchMethod (declared_link, exact_handle, fuzzy_name, manual) so analysts know why two handles were linked.
Anchor, mine bios, and fan out
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.
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:
If the anchor is not_found, try common variants (sarah.fit, sarah_fit) before parallelizing the other networks. Each attempt is another credit.
2. Mine declared links from bios. Instagram and YouTube bios are where creators paste their "real" handles.
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:
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 before fuzzy matching:
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:
Reference: Get Twitter profile.
3. Fan out profile lookups. Resolve your handle list per platform (exact handle plus bio-derived candidates) in parallel. One loop handles all four because every platform returns the same top-level shape.
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:
Each result carries its own meta.creditsCharged. A four-platform pass is typically four credits when all complete, even if two return not_found.
For imports of 5,000 creators, batch Promise.all in chunks of 50–100. Metered routes have no published rate cap; your balance is the limit.
Disambiguate when handles diverge
Exact-handle fan-out is step one, not the finish line:
- Declared link in bio — if Instagram bio contains
youtube.com/@SarahFitOfficial, that edge outranks guessingsarahfiton YouTube. - Display name + avatar hash — weak signal alone; useful when handles differ by one character (
sarahfitvssarah.fit). - Follower magnitude — a 2M TikTok paired with a 200-subscriber YouTube is probably wrong unless the bio says "new channel."
- 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.
Worked example — CRM says sarahfit everywhere, but Instagram bio says youtube.com/@SarahFitOfficial:
| Step | Action | Outcome |
|---|---|---|
| 1 | GET /v1/instagram/profiles/sarahfit | found, bio contains YouTube URL |
| 2 | GET /v1/youtube/channel?handle=sarahfit | not_found — wrong handle |
| 3 | GET /v1/youtube/channel?handle=SarahFitOfficial | found, 98k subscribers |
| 4 | Merge 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.
Confidence scoring:
Send low rows to a human queue. Auto-merge only high in production pipelines.
Normalize into one card
Map platform responses into one UI card without if (youtube) subscriberCount else followers scattered through React components:
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:
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.
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:
Instagram posts paginate the same way:
References: TikTok profile videos · Instagram profile posts.
Compute engagement in your warehouse — the API returns per-item metrics, not a black-box score:
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:
Stop after two or three pages unless you are building a forensic report — most vetting workflows only need the last 12–20 posts.
Seed lists with discovery search
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.
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 the anchor-and-fan-out flow 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):
| Column | Source | Why |
|---|---|---|
matchMethod | your resolver | Explains auto vs manual links |
lookupStatus | per platform | Drives UI badges (private, not_found) |
followers | metrics | Sort and filter |
verified | profile.verified | Contract eligibility |
requestId | meta.requestId | Support trail when a number looks wrong |
creditsCharged | meta.creditsCharged | Client-level cost allocation |
Re-pull on a cron before QBRs or campaign reviews.
Weekly refresh job pattern (same idea as the social listening guide, but per creator row):
Write meta.requestId on every upsert.
Troubleshooting
YouTube not_found but the channel loads in a browser
- Pass the full channel URL:
?url=https://www.youtube.com/@SarahFitOfficialinstead of a shortened handle. - Handles are case-sensitive on some legacy channels — copy the
@from the address bar. @handleand/c/ChannelNameURLs refer to different identifiers; use what the creator's bio declares.
Instagram found but follower count is null
privateaccounts may return identity fields without public metrics. ChecklookupStatusbefore 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_foundon one network only. LogrequestIdand retry once before marking dead. - Strip
@from path params —/v1/tiktok/profiles/@sarahfitis wrong; usesarahfit.
Merged card shows impossible total reach
- Dedupe by person, not by handle string. Two different people named
alexon TikTok and YouTube should stay two parent rows until bio links confirm a merge. - Do not sum
privateornot_foundplatforms intototalReachunless your product spec explicitly says to.
lookup_failed or HTTP 503
- Not charged. Retry with exponential backoff; pass the failed
meta.requestIdto 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.videosor 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 === falsemeans transport or auth failed — checkresult.error.codeandresult.error.requestId.result.ok === truewithlookupStatus: "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:
| Step | Calls per creator | Credits (approx.) |
|---|---|---|
| Anchor TikTok profile | 1 | 1 |
| Bio-driven Instagram + YouTube + X fan-out | 3 | 3 |
| Recent TikTok videos (1 page) | 1 | 1 |
| Recent Instagram posts (1 page) | 1 | 1 |
| Enriched creator row | 6 | 6 |
Import 500 creators with full enrichment: ~3,000 credits. Discovery search adds 1 credit per results page before you filter.
Log meta.creditsCharged per creatorId for client-level cost allocation. Public data only — lawful use is on you under Terms. Full matrix: Credits.
Batch import spreadsheet (500 rows, enrich everyone):
| Stage | Calculation | Credits |
|---|---|---|
| Profile fan-out (4 networks × 500) | 2,000 | 2,000 |
Skip not_found anchors (~8%) | −160 × 3 remaining calls | −480 |
| Video/post page for top 200 by followers | 200 × 2 | 400 |
| 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
privateaccounts 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
matchMethodprovenance 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