mcpField
S
StoriesFly: Instagram and TikTok data

StoriesFly: Instagram and TikTok data

Remote
storiesfly.comSocial Media0Updated 4d ago

About StoriesFly: Instagram and TikTok data

StoriesFly provides a TypeScript client for retrieving public Instagram and TikTok data through its API, for developers building data workflows. Its typed methods cover profiles, media, social lists, search, and downloads without requiring platform logins.

Key features

  1. 01Fetch Instagram profiles and content
  2. 02Retrieve TikTok profiles and videos
  3. 03Read comments, followers, and lists
  4. 04Search hashtags and places
  5. 05Download media files
  6. 06Paginate list results

Use cases

  1. 01Inspect public Instagram profiles and posts
  2. 02Retrieve TikTok video and account data
  3. 03Collect follower or following pages
  4. 04Download media from post links

MCP support

The same Instagram and TikTok data is exposed to AI assistants through the StoriesFly MCP server at `https://storiesfly.com/mcp`. ChatGPT and Claude connect there, with Telegram sign-in.

Configuration

Remote servers need no local install. Pick your client, then copy the config or install in one click.

Endpointhttps://storiesfly.com/mcp
Copy install command
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.

Tools

Details

From the project README

storiesfly

CI npm version

TypeScript client for the StoriesFly API. It reads public Instagram and TikTok data over HTTP. For Instagram: profiles, stories, highlights, posts, reels, comments, followers, hashtags, places and search. For TikTok: profiles, videos, stories, followers and following lists, reposts, and video download without the watermark. You do not log in to Instagram or TikTok and you do not handle cookies or sessions. You send an API key and pay per request. The same data works inside ChatGPT and Claude through the StoriesFly MCP server: see Use it from ChatGPT and Claude (MCP).

The package has no runtime dependencies. It needs Node 18 or newer (it uses the global fetch), it is ESM only, and it ships its own types. The 46 data calls and the free account call each have a method, and every answer is typed.

Install

npm install storiesfly

Get a key

  1. Sign in with Telegram at https://storiesfly.com/developers/dashboard.
  2. Create a key there. A new account gets 100 free requests, so you can try every call before you pay.

Keep the key on your server. Anyone who has it can spend your balance.

Quick start

The stories an account has up right now:

import { StoriesFly } from "storiesfly";

const client = new StoriesFly({ apiKey: process.env.STORIESFLY_API_KEY! });

const { user, stories } = await client.user.stories({ username: "instagram" });

console.log(`${stories.length} stories up from account ${user.id}`);
for (const story of stories) {
  console.log(story.media_type, story.taken_at_iso, story.media_url);
}

Things to know:

  • An account is named by { username } or { id }, never both. A post is named by { url }, { code } or { id }. The types enforce this.
  • Ids are strings. Instagram ids pass 2^53, and a JSON number would round them.
  • Field names are snake_case, as the API sends them. A value the answer does not carry is null. It is never 0 or "".
  • Media links in answers are signed Instagram CDN links. They expire within days, so download what you need soon.

A post, with the links to its pictures and video:

const post = await client.post.get({ url: "https://www.instagram.com/p/DcyLOLIEdy7/" });

console.log(post.caption, post.likes);
for (const item of post.media) {
  console.log(item.type, item.video_url ?? item.image_url); // one item per slide of a carousel
}

A TikTok video without the watermark, and a TikTok account's followers:

const video = await client.tiktok.video({ url: "https://www.tiktok.com/@khaby.lame/video/7697307919943060767" });
console.log(video.caption, video.views, video.video_url); // video_url has no watermark

const profile = await client.tiktok.user.profile({ username: "khaby.lame" });
console.log(profile.followers);

for await (const page of client.paginate(client.tiktok.user.followers, { username: "khaby.lame" }, { maxPages: 2 })) {
  for (const follower of page.users) console.log(follower.username);
}
Options
new StoriesFly({
  apiKey: "...",       // required, sent as X-Api-Key
  baseUrl: "...",      // default https://storiesfly.com/api/v1
  fetch: myFetch,      // default: the global fetch
  timeoutMs: 60_000,   // per attempt; 0 turns the timeout off
  maxRetries: 2,       // for rate_limited answers; 0 turns retries off
});

Every call also takes a last argument with signal (an AbortSignal), timeoutMs, maxRetries and withMeta.

Paging

A call that returns a list returns one page, with next_cursor and has_more. client.paginate follows the list for you. It is an async iterator that calls the method, yields the page, then calls it again with the next_cursor of the page before, until has_more is false:

for await (const page of client.paginate(client.user.followers, { username: "instagram" }, { maxPages: 4 })) {
  for (const follower of page.users) console.log(follower.username);
}
  • Every page is one call and is charged as one. A large account has many pages, so set maxPages unless you mean to read the whole list.
  • paginate takes the method itself (client.user.followers), its parameters, and options: maxPages, signal and withMeta. A cursor in the parameters starts the walk from a page you already have.
  • Only methods that take cursor can be paginated, and the compiler refuses the rest. The Paged column of the methods table marks them, and ENDPOINTS (exported) has a paged flag on each.
  • To page by hand, pass next_cursor back as cursor and keep every other parameter the same. A cursor is opaque. A cursor the API did not return gives bad_request.
  • Page size follows Instagram and varies by endpoint. The spec fixes it for three: user.posts returns up to 9 items a page, user.followers and user.following up to 25. The Paged column of the methods table shows the size wherever the spec gives one.

TikTok

TikTok calls live under client.tiktok. An account is named by { username }.

await client.tiktok.video({ url });                          // one video or photo post, short links too
await client.tiktok.user.profile({ username });              // followers, following, likes, video count
await client.tiktok.user.videos({ username });               // newest first, paged
await client.tiktok.user.stories({ username });              // stories up now
await client.tiktok.user.followers({ username });            // paged
await client.tiktok.user.following({ username });            // paged
await client.tiktok.user.reposts({ username });              // paged
  • videos, followers, following and reposts take cursor and return next_cursor, so client.paginate follows them. Page size follows TikTok and varies by endpoint.
  • An account that hides its followers or following list answers an empty page with hidden: true.
  • A number is null when it is not exact. File links are temporary: save the files right away.
  • On a later page of tiktok.user.videos, video_url is null. To get the file of one video, call tiktok.video or download with its url.

Download

client.download({ url }) takes the link of an Instagram post, reel or IGTV, or of a TikTok video or photo post (short links too), and returns the file links: the video (TikTok's without the watermark), every picture of a carousel or photo post, the cover, and for TikTok the soundtrack. It returns files only; for the numbers use post.get or tiktok.video. The price is fixed per call: $0.50 per 1,000 for an Instagram link and $0.20 per 1,000 for a TikTok link.

const { platform, media } = await client.download({ url: "https://www.instagram.com/reel/DdY3MUJCfby/" });
for (const file of media) console.log(file.type, file.url);

File links are temporary, so download them right away. client.account() also shows the download prices as download_price_usd.

Use it from ChatGPT and Claude (MCP)

The same data is available to AI assistants at https://storiesfly.com/mcp, with no code. Details: https://storiesfly.com/developers/mcp.

  • In Claude Code: claude mcp add --transport http storiesfly https://storiesfly.com/mcp
  • ChatGPT and Claude connect to the same address and sign in with Telegram.

What a call cost: response meta

A call resolves to the parsed body. To see the billing headers too, pass { withMeta: true } as the last argument and the call resolves to { data, meta }:

const { data, meta } = await client.user.profile({ username: "instagram" }, { withMeta: true });

console.log(data.followers);
console.log(meta.unitsCharged, meta.balanceRemaining, meta.freeUnitsRemaining);
meta fieldHeaderMeaning
statusThe HTTP status.
requestIdX-Request-IdThe id of the call. Quote it when you write to support.
unitsChargedX-Units-ChargedUnits this call charged. 0 when it was free or was refunded.
balanceRemainingX-Balance-RemainingYour balance after the call, in US dollars with four decimals. A string, so no float rounds it.
freeUnitsRemainingX-Free-Units-RemainingFree units you have left.
rateLimitLimitX-RateLimit-LimitRequests per second your account may send.
rateLimitRemainingX-RateLimit-RemainingRequests left in the current second.

A header the answer did not carry is null. The meta is returned per call and not kept on the client (there is no lastResponse), so calls that run at the same time cannot overwrite each other's numbers. With paginate, { withMeta: true } yields { data, meta } for each page.

Errors and what is charged

Every call that does not return data throws StoriesFlyError. Test code, not message:

import { StoriesFly, StoriesFlyError } from "storiesfly";

try {
  await client.user.stories({ username: "some_private_account" });
} catch (err) {
  if (!(err instanceof StoriesFlyError)) throw err;

  console.log(err.status);       // HTTP status; 0 when no answer came
  console.log(err.code);         // "private_account"
  console.log(err.requestId);    // quote it to support
  console.log(err.retryAfterMs); // from Retry-After, when the API sent one
  console.log(err.meta?.unitsCharged); // what this failed call was charged
}

Other fields: fields (with bad_request, the names of the refused parameters), linkUrl (with telegram_required, the link that links your account to Telegram), url (the URL called; it holds no key) and body (the parsed error body).

You are charged for answers: a 200, not_found and private_account. A wrong parameter, a rate limit and an empty balance cost nothing, and a failure on our side is refunded. err.meta.unitsCharged and X-Units-Charged say what a call took.

StatuscodeChargedWhen
200yesThe data.
404not_foundyesNothing was found for this request.
403private_accountyesThe call reached a private account.
400bad_requestnoA parameter is missing or invalid. fields names it.
401unauthorizednoThe key is missing or wrong.
402credits_exhaustednoNo credits left. Top up your balance.
403telegram_requirednoThe account behind the key is not linked to Telegram. linkUrl has the link.
404unknown_endpointnoNo such endpoint.
429rate_limitednoToo many requests per second.
500internal_errorrefundedA fault on our side.
503upstream_unavailablerefundedThe data is not available right now. Try again shortly.

The client adds three codes of its own, with status 0 when no answer came: timeout (no answer within timeoutMs), network_error (fetch failed; the error has the cause) and invalid_response (an answer that is not in the documented shape, such as a proxy's error page; the status and the body are kept).

Retries
  • A 429 is retried up to maxRetries times (2 by default). The client waits for the Retry-After the API sent, or half a second and then longer when it sent none. A Retry-After longer than 30 seconds is not waited out: the error is thrown at once, with retryAfterMs set.
  • A 402 is never retried: another call would not change it.
  • Other errors, timeouts and network errors are not retried. A 500 or 503 is refunded, so you can retry it yourself.

Pricing

From $1.50 per 1,000 requests, prepaid. See https://storiesfly.com/developers/pricing.

One unit is one request. Every call takes 1 unit, a page of a list included. Resolving a share link takes 2, and account takes none. download is sold at a fixed price per call (see Download) and uses one free unit when you have any. Free units are spent before your balance. Requests per second are limited per account.

Methods

Each method is a call of API v1. This table is generated from the OpenAPI spec (openapi.json) and checked by the tests.

CallEndpointUnitsPagedReturns
client.user.profile()GET /user/profile1Profile with verified badge, bio, link and counts
client.user.about()GET /user/about1Country, join month and former usernames
client.user.posts()GET /user/posts1yes, up to 9Posts with likes, comments, views, shares, saves and reposts
client.user.reels()GET /user/reels1yesReels with captions, video and play counts
client.user.tagged()GET /user/tagged1yesPosts the account is tagged in
client.user.pinned()GET /user/pinned1Posts pinned to the top of the profile
client.user.stories()GET /user/stories1Stories live now, with the length of each video
client.user.highlights()GET /user/highlights1yesHighlight covers and titles
client.user.followers()GET /user/followers1yes, up to 25Followers, page by page
client.user.following()GET /user/following1yes, up to 25Accounts followed, page by page
client.user.followersSearch()GET /user/followers/search1Find someone among the followers
client.user.followingSearch()GET /user/following/search1Find someone among the accounts followed
client.user.reposts()GET /user/reposts1yesPosts and reels the account reposted
client.user.suggested()GET /user/suggested1Similar accounts Instagram suggests
client.post.get()GET /post1One post: media links, caption, views, likes, comments, shares, saves and reposts
client.post.comments()GET /post/comments1yesComments with likes and reply counts
client.post.commentReplies()GET /post/comment-replies1yesReplies to a comment
client.post.likers()GET /post/likers1Accounts that liked the post
client.post.usertags()GET /post/usertags1People tagged in the post
client.post.music()GET /post/music1The track used in a reel
client.comment.likers()GET /comment/likers1yesAccounts that liked a comment
client.story.get()GET /story1One story by link or id
client.highlight.get()GET /highlight1Every story in a highlight, by link or id
client.hashtag.get()GET /hashtag1A hashtag with its post count
client.hashtag.posts()GET /hashtag/posts1yesPosts under a hashtag: top, recent or reels
client.location.get()GET /location1Place name, address, city and coordinates
client.location.posts()GET /location/posts1yesPosts tagged at a place: top or recent
client.location.search()GET /location/search1Find places by name or around a point
client.search.users()GET /search/users1yesFind accounts by name or username
client.search.hashtags()GET /search/hashtags1yesFind hashtags, with their post counts
client.search.places()GET /search/places1Find places by name
client.search.music()GET /search/music1yesFind tracks by title or artist
client.search.reels()GET /search/reels1yesFind reels for a text
client.search.top()GET /search/top1yesThe top posts for a text
client.track.get()GET /track1Track title, artist, length and audio link
client.resolve()GET /resolve1, or 2 for share linksTurn a post, reel or share link into data
client.shop.storefront()GET /shop/storefront1An account's shop: collections and first products
client.shop.products()GET /shop/products1yesProducts in a shop collection
client.tiktok.video()GET /tiktok/video1One TikTok video without the watermark, with views, likes, comments and shares
client.tiktok.user.profile()GET /tiktok/user/profile1TikTok profile with followers, following, likes and video count
client.tiktok.user.videos()GET /tiktok/user/videos1yesA TikTok account's videos with numbers, page by page; file links on page one
client.tiktok.user.stories()GET /tiktok/user/stories1TikTok stories live now
client.tiktok.user.followers()GET /tiktok/user/followers1yesTikTok followers, page by page; a hidden list answers an empty page
client.tiktok.user.following()GET /tiktok/user/following1yesTikTok accounts followed, page by page; a hidden list answers an empty page
client.tiktok.user.reposts()GET /tiktok/user/reposts1yesVideos a TikTok account reposted, page by page, with their authors
client.download()GET /download1Files of an Instagram post or reel, or a TikTok video, by link
client.account()GET /account0Your plan, balance, free units and use

Parameters, in short:

  • TikTok calls (tiktok.user.*) take { username }, and tiktok.video and download take { url }. The paged TikTok calls also take cursor.
  • Account calls (user.*, shop.storefront) take { username } or { id }. The calls with a cursor also take cursor. user.followersSearch and user.followingSearch also take q.
  • Post calls (post.*) take { url }, { code } (the part of the link after /p/ or /reel/) or { id }. post.comments takes cursor. post.commentReplies also takes comment_id.
  • story.get and highlight.get take { url } or { id }.
  • hashtag.posts takes name, sort (top, recent or reels; keep it the same from page to page) and cursor. location.posts takes id, sort (top or recent) and cursor.
  • location.search takes q, or lat and lng. The search.* calls take q, 1 to 100 characters.
  • post.get returns the whole post: media (the links to its pictures and video, one entry per slide of a carousel), caption, owner, time, and the numbers. It is the same shape as an item of user.posts, user.reels, hashtag.posts and the other lists of posts, and resolve returns it for a post link.

Docs

Migrating from 0.x

1.0 is a rewrite for API v1. The old methods are gone, the base URL changed, and the old plans and daily quotas do not exist in v1.

0.x1.0
getStories(username)user.stories({ username }). It returns { user, stories }. The profile and the highlights are separate calls (user.profile, user.highlights), and you pay for each.
getProfile(username)user.profile({ username }). It also gives id, bio, verified and external_url.
getHighlights(id)highlight.get({ id }), with every story in the highlight. user.highlights lists the highlights of an account.
getMedia(url)post.get({ url }). It returns the whole post: media holds the picture and video links (image_url, video_url, one entry per slide of a carousel), with the caption, owner, time and the counts.
download(url)download({ url }) in 1.1 returns the file links of a post, reel or TikTok video. post.get also gives the media links.
getTrackerEvents, listTrackedAccounts, addTrackedAccount, removeTrackedAccount, checkShadowban, getOutboundEngagementNot part of API v1, so not in 1.0.

What else changed:

  • baseUrl now defaults to https://storiesfly.com/api/v1. The bearerToken option is gone: v1 takes the key in X-Api-Key and nowhere else.
  • Methods are grouped by resource: client.user.profile(), client.post.get(), client.search.users(). Every method takes one object of parameters.
  • Answers use the API's own snake_case names, and null for a value that is missing: fullName is now full_name, profilePicUrl is profile_pic_url, isPrivate is is_private, takenAt is taken_at.
  • StoriesFlyError has code, requestId and retryAfterMs. The 0.x fields rateLimit, retryAfterSec, requiresUpgrade, isRateLimited and method are gone. Compare err.code with rate_limited, credits_exhausted and the rest. A 429 is now retried for you.
  • The old plans (DEV, Starter, Growth, Scale, and the Tracker plans), their daily quotas and the 60 requests a minute burst limit are gone. v1 bills from a prepaid balance, per request, with 100 free requests for a new account. See Pricing.
  • A key from before works on v1 once the account behind it is linked to Telegram. Until then v1 answers telegram_required, and err.linkUrl has the link.
  • The tracker, shadow ban and outbound analytics endpoints still answer over HTTP, but 1.0 does not wrap them. To keep using them, stay on storiesfly@0 or call them yourself. The older /api/stories, /api/profile, /api/highlights, /api/media and /api/download endpoints that 0.x calls keep working for existing free keys until 2026-12-31. Details: https://storiesfly.com/developers/docs/legacy.

FAQ

Does it need an Instagram or TikTok login? No. You send a StoriesFly API key and nothing else. There are no cookies or sessions to manage.

Can I get a TikTok account's followers list? Yes. client.tiktok.user.followers({ username }) returns it page by page, and client.paginate follows the pages. An account that hides the list answers an empty page with hidden: true.

Can I download a TikTok video without the watermark? Yes. client.tiktok.video({ url }) returns video_url without the watermark, and client.download({ url }) returns the file links for an Instagram or TikTok link.

Can I use it from ChatGPT or Claude? Yes, through the MCP server at https://storiesfly.com/mcp. See Use it from ChatGPT and Claude (MCP).

How is it priced? Per request, from prepaid balance. A new account gets 100 free requests, once. See Pricing.

Not affiliated

This is a client for the StoriesFly API. It reads public data only, and a private account gives private_account. It is not an Instagram or Meta product and is not affiliated with or endorsed by them.

Development

npm install
npm test               # clean, build, compile the tests and run them
npm run openapi:check  # validate openapi.json and openapi.yaml, and check that they match
npm run readme:check   # check the method table above against the spec

To update the spec: replace openapi.json with the new file, run npm run openapi:sync and npm run readme:sync, and run the tests.

License

MIT. See LICENSE.

Comments

Markdown supported

New to MCP?

From what the protocol is to installing your first server, these answers cover most first questions.

An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.