TikTok Stories Scraper
Pricing
from $4.00 / 1,000 profiles
TikTok Stories Scraper
Scrape active public TikTok Stories from up to 200 profiles in one run. Get video and photo media URLs, Story IDs, author data, music, engagement metrics, and clear outcomes for profiles with no active Stories. No TikTok login required.
Pricing
from $4.00 / 1,000 profiles
Rating
0.0
(0)
Developer
LanceAPI
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Collect the currently active public Stories of TikTok profiles — video and photo — in one run, as a clean structured Dataset.
Give it usernames, @usernames or full profile URLs. You get one row per Story with media URLs,
engagement, music and author data, plus one explicit outcome row for every profile that had no
Story, so a quiet profile never looks like a broken run.
- 🎬 Video and photo Stories in the same Dataset, each row labelled with a stable
storyType. - ⚡ Batch collection — up to 200 distinct profiles per run, validated, normalized and deduplicated.
- ✅ A clear outcome for every profile —
success,no_active_stories,private,not_found,invalidorfailed. One profile failing never stops the batch. - 🔀 Newest or oldest first, plus an optional per-profile Story limit — both applied locally, with no extra requests.
- 💰 Predictable, per-profile pricing — you pay for profiles checked, never for the number of Stories a creator happened to post. See Pricing.
TikTok Stories disappear after roughly a day, so a run that returns nothing is a normal answer, not an error. No TikTok login, cookie or session is ever used.
📌 At a glance
| What it collects | Active public TikTok Stories (video and photo) |
| Input | TikTok usernames, @usernames or profile URLs |
| Output | JSON rows in the default Dataset: one per Story, plus one outcome row per profile with no Story |
| Batch size | 1–200 distinct profiles per run |
| Duplicate inputs | Merged — processed once |
| Login required | No |
| Options | Story order (newest/oldest), optional per-profile Story limit |
| Pricing | Per-profile pricing + run-start fee; see Pricing |
| Best for | Monitoring, campaign verification, creator research, data pipelines |
🎯 What this Actor does
| Capability | Detail |
|---|---|
| Active Story collection | Reads the Stories a public profile has live right now, before they expire. |
| Video Stories | Returns a playable videoUrl, cover, duration and engagement for video Stories. |
| Photo Stories | Returns every image of a photo Story in media.images, plus a top-level imageUrl. |
| Multiple Stories per profile | One row per Story. A profile with five live Stories returns five rows. |
| Batch processing | Check up to 200 profiles together for a single monitoring snapshot. |
| Input normalization | name, @name, profile URL and video URL forms all resolve to the same profile. |
| De-duplication | Equivalent inputs collapse to one profile for cleaner batch results. |
| Explicit outcomes | Profiles without Stories, private profiles, unknown profiles and rejected inputs each get a labelled row. |
| Fault isolation | A failing profile is reported as failed; the rest of the batch continues. |
| Story ordering | Newest first or oldest first, applied to the list already fetched. |
| Per-profile limit | Optional cap on how many Story rows a profile returns. |
| Automation ready | Plain JSON, stable IDs, one Dataset per run — drops into n8n, Make, Zapier, warehouses and LLM pipelines. |
🚀 Quick start
- Open the Input tab.
- Add one or more profiles to TikTok profile usernames.
- Leave the options at their defaults for a first run.
- Click Start.
- Open the Dataset tab to see the Stories.
{"usernames": ["tiktok", "@nasa", "https://www.tiktok.com/@netflix"]}
Every accepted profile produces at least one row. A profile with no active Story gets a
profile_status row with status: "no_active_stories".
📦 What you get
One row per delivered Story gives you media links, engagement and creator context for your own reporting or analysis:
{"rowType": "story","profileUsername": "mercadolivre","storyId": "7698748937192279316","storyType": "photo","status": "success","createdAt": "2026-10-20T13:57:57.000Z","imageUrl": "https://<temporary-media-url>","views": 13315,"likes": 113,"comments": 0,"shares": 1,"saves": 0,"author": {"userId": "6926981670042436614","username": "mercadolivre","nickname": "Mercado Livre","verified": true,"followers": 5015608},"media": { "type": "photo", "images": ["https://<temporary-media-url>"] }}
Illustrative output example; temporary media URLs are placeholders. Field availability varies.
Three Dataset views are provided:
| View | Shows |
|---|---|
| Stories | username, Story ID, type, created time, views, likes, comments, media links |
| Media | video, cover and image URLs, plus any expiry stated by the URL |
| Analytics | views, likes, comments, shares, saves, created time, row status |
📋 Output reference
Story rows (rowType: "story")
| Field | Type | Nullable | Meaning |
|---|---|---|---|
profileUsername | string | no | Canonical (lowercased) TikTok username the Story belongs to. |
storyId | string | no | TikTok Story id. Always a string, never a number. |
storyType | string | no | video or photo. A Story with at least one image is a photo Story. |
status | string | no | success for a delivered Story. |
createdAt | ISO 8601 string | yes | When the Story was published. |
videoUrl | string | yes | Playable video URL. null for photo Stories. |
imageUrl | string | yes | First photo image URL. null for video Stories. |
coverUrl | string | yes | Cover image URL. |
caption | string | yes | Story caption or description when the source provides one. |
views / likes | integer | yes | Counters exactly as reported. |
comments | integer | yes | Comment count. |
shares / saves | integer | yes | Share and save counters. |
url | string | yes | Story page URL when available. |
mediaUrlExpiresAt | ISO 8601 string | yes | Expiry parsed from the media URL, when the URL itself states one. |
author | object | no | userId, username, nickname, avatarUrl, verified, followers… |
media | object | no | type, durationMs, coverUrl, videoUrl, images, hasWatermark, sizeBytes |
engagement | object | no | views, likes, comments, shares, saves, downloads |
music | object | no | id, title, author, album, playUrl, coverUrl, durationMs, original |
commercial | object | no | isAd, brandedContentType |
mentions | object | no | users, hashtags, anchors |
rowType | string | no | story. |
inputUsername | string | yes | First submitted reference for this profile, with surrounding whitespace trimmed. |
canonicalKey | string | yes | tiktok:<username>:<storyId> — use with storyId to match Stories across snapshots. |
scrapedAt | ISO 8601 string | no | When the row was written. |
Unknown scalar values are null; missing lists are empty arrays. Nested objects remain present
with nullable fields, so your pipeline can distinguish unknown data from a reported zero. Zero counters
stay 0; impossible physical values (a zero duration or byte size) are treated as unknown and become
null.
TikTok identifiers (storyId, author.userId) are always strings, so a large id is never rounded by
JavaScript number handling.
Nested scalar fields can be null, and mention/image lists can be empty. Durations (durationMs)
are in milliseconds; sizeBytes is in bytes. Counters are counts, and timestamps use ISO 8601 UTC.
author.userId lets you join creator records; storyId helps deduplicate repeated snapshots.
Profile outcome rows (rowType: "profile_status")
Every accepted profile that delivered no Story gets exactly one row:
{"rowType": "profile_status","inputUsername": "@nasa","profileUsername": "nasa","canonicalKey": "tiktok:nasa","status": "no_active_stories","outcome": "no_active_stories","error": null,"storiesFound": 0,"scrapedAt": "2026-10-21T00:22:27.245Z"}
Status values
status | On row | Meaning | Next step |
|---|---|---|---|
success | story | Story collected. | Use the returned data. |
no_active_stories | profile_status | Profile exists, no active public Story right now. | Check again on your next scheduled run. |
private | profile_status | Profile is private; no content returned. | Use a public profile. |
not_found | profile_status | Profile could not be found. | Verify the username. |
invalid | profile_status | Input is not a usable TikTok profile reference. | Correct the input. |
failed | profile_status | Data could not be read or the free allowance blocked the check. | Check error; retry temporary failures later. |
See Pricing for the Profile Check rules for these outcomes.
error is null unless something actually went wrong, so filter on status for business outcomes.
Error codes
| Code | With status | Meaning |
|---|---|---|
INVALID_INPUT | invalid | The value is not a usable TikTok profile reference. |
TEMPORARY_SOURCE_ERROR | failed | Data could not be read right now. Retry the run later. |
FREE_USAGE_LIMIT_EXCEEDED | failed | The included free profile allowance was reached. |
FREE_USAGE_CHECK_FAILED | failed | The allowance could not be verified. Retry the run later. |
⚙️ Input reference
| Field | Type | Required | Default | Limits |
|---|---|---|---|---|
usernames | array of string | yes | — | 1–200 input entries |
sortStories | string | no | newest | newest or oldest |
maxStoriesPerProfile | integer | no | — | ≥ 1 |
Prefill is not a default. The Input form pre-fills usernames with
https://www.tiktok.com/@tiktok; API callers must supply their own non-empty array.
Leaving maxStoriesPerProfile unset returns all available active Stories.
What counts as a profile reference. All of these resolve to the same profile:
tiktok@tiktokhttps://www.tiktok.com/@tiktokhttps://www.tiktok.com/@tiktok/video/7300000000000000000
Values are trimmed and lowercased, non-profile links are rejected, and equivalent forms collapse to
one profile. Duplicates are processed once, keeping repeated references out of your results. Rejected values still produce an invalid row so nothing disappears silently.
About the 200-profile limit
The Input schema accepts 1–200 entries, including duplicates. Within that list, equivalent entries are merged before processing. The runtime also rejects lists with more than 200 distinct profiles instead of silently dropping the extra profiles. Keep each submitted list within 200 entries.
Optional features
🔧 Two options change what a run returns. Neither costs an extra Actor charge and neither causes an additional request to the data source — both work on the Story list that was already collected.
🔀 sortStories — newest or oldest first
What it does — orders the Stories returned for each profile by creation time.
newest(default): the most recent Story appears first. Best when you want to know what a creator posted most recently, or when you combine it with a limit of 1.oldest: the earliest Story that is still online appears first. Best when you are walking a creator's current Story sequence in the order it was published.
Stories without a known creation time are placed after dated ones, keeping their original order.
Cost — none. This is a local sort of data already in memory: no extra request, no extra billing.
✂️ maxStoriesPerProfile — cap the rows per profile
What it does — keeps only the first N Stories of the sorted list. For example
sortStories: "newest" with maxStoriesPerProfile: 1 returns only the latest active Story of each
profile, which is the usual choice for a daily monitoring job.
Cost — no extra Actor charge. The full active list is read, then trimmed locally; this does not reduce the Profile Check charge. It reduces Dataset rows, response size and downstream processing.
Leave it empty to keep every active Story.
🔄 How it works
- Your input list is validated, normalized and deduplicated.
- Each accepted profile is checked for currently active public Stories.
- Stories are ordered per your
sortStorieschoice and trimmed to yourmaxStoriesPerProfilelimit, locally. - One Dataset row per Story is written, and one outcome row per profile that delivered nothing.
- Results are ready in the Dataset, the API, or any automation you connect.
Private profiles are reported as private and never bypassed. Only public data is read.
💡 Use cases
- Creator monitoring — track which creators are posting Stories today, and capture the media URLs.
- Brand and campaign monitoring — verify that a sponsored Story went live, and when.
- Competitive research — use returned timestamps and counters to compare Story activity and engagement across accounts in your own tools.
- Snapshot archives — run on a schedule and keep one Dataset per run as a record of what was live.
- Social data pipelines — feed a warehouse or dashboard with structured Story records.
- AI and analytics input — use the returned captions, hashtags, counters and media URLs as input to your own models; the Actor delivers data, it does not run analysis for you.
- Agency reporting — collect a client's creator list in one run and use the structured results in your campaign reports.
🤖 Automation and downstream use
The Dataset is plain JSON, so it drops into n8n, Make, Zapier, a database, a dashboard or an LLM pipeline for further processing. Two practical patterns:
- Scheduled snapshot. Run every few hours; each run is its own Dataset you can diff.
- Latest-only monitor. Set
sortStories: "newest"andmaxStoriesPerProfile: 1to keep the output to one row per active creator per run.
🧑💻 API usage
JavaScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('lance_api/tiktok-stories-scraper').call({usernames: ['tiktok', '@nasa'],sortStories: 'newest',maxStoriesPerProfile: 1,});const { items } = await client.dataset(run.defaultDatasetId).listItems();const stories = items.filter((row) => row.rowType === 'story');const outcomes = items.filter((row) => row.rowType === 'profile_status');console.log(`${stories.length} Stories, ${outcomes.length} profiles without a Story`);
Python
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run = client.actor("lance_api/tiktok-stories-scraper").call(run_input={"usernames": ["tiktok", "@nasa"],"sortStories": "newest","maxStoriesPerProfile": 1,})items = client.dataset(run["defaultDatasetId"]).list_items().itemsstories = [row for row in items if row["rowType"] == "story"]print(len(stories), "Stories")
cURL
curl -X POST "https://api.apify.com/v2/acts/lance_api~tiktok-stories-scraper/runs" \-H "Authorization: Bearer $APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"usernames":["tiktok","@nasa"]}'
This starts a run asynchronously. Wait until the run succeeds by checking
https://api.apify.com/v2/actor-runs/<runId>, then read the items from
https://api.apify.com/v2/datasets/<defaultDatasetId>/items with the same Authorization header.
Pricing
TikTok Stories Scraper uses per-profile pricing: a run-start fee plus one Profile Check charge for each unique, chargeable profile checked. You are not charged per Story.
| Charge | Price (USD) |
|---|---|
| Actor start | $0.099 per run |
| Profile check | From $4.00 per 1,000 profiles |
Plan-specific rates vary by Apify subscription. See the Actor's Pricing tab for the current rate on your plan.
What is charged and what is not
| Situation | Profile Check charge |
|---|---|
| Valid profile checked, Stories returned | Yes — one unit |
| Valid profile checked, no active Story | Yes — one unit |
| Private profile checked | Yes — one unit |
| Invalid input | No |
| Profile not found | No |
| Technical failure | No |
| Duplicate input in the same run | No additional charge |
The Actor start fee is separate from these Profile Check rules. A Profile Check is chargeable only after its Story rows or profile outcome row have been written to the Dataset.
One profile is one billing unit, regardless of how many active Stories it returns. This makes recurring monitoring easier to budget.
📎 Good to know
- Only active public Stories. Highlights, archived Stories and expired Stories are out of scope.
- Public access only. Private content and access restrictions are not bypassed.
- Data for your own analysis. Use returned fields in your reporting or AI tools; the Actor does not generate summaries, sentiment analysis or creator scores.
- Stories are short-lived. No Stories is a normal answer at any given moment.
- Media URLs are temporary. They may expire or stop working. The Actor returns URLs, it does not host files — save what you need with your own tooling before they expire.
- Video and photo Stories differ. A video Story has
videoUrl; a photo Story hasimageUrlandmedia.images, and no video URL — photo Stories can carry background audio, which is not a video. storyIdanduserIdare strings. TikTok identifiers are large; they are never converted to numbers, so they stay exact.- One failure stays one failure. A profile that fails is reported as
failed; the rest of the batch continues.
❓ FAQ
Can I collect TikTok Stories without logging in? Yes. The Actor reads public data only and never asks for a TikTok account, cookie or session.
Does it work with private accounts?
Private profiles are reported with status: "private". Their content is not accessible and is not
returned.
What happens if a profile has no active Stories?
You get one profile_status row with status: "no_active_stories" and storiesFound: 0. That is a
normal outcome, not an error.
Am I charged per Story? No. Pricing is per unique, chargeable profile checked, not per Story returned. See Pricing.
Am I charged for a profile with no active Stories? Yes. A completed check that finds no active Stories incurs one Profile Check charge. See Pricing for the other outcomes and the separate run-start fee.
Can I download the video or image files?
The Actor gives you the media URLs in videoUrl, imageUrl and media.images. It does not download
or host the files; fetching them is up to your own pipeline.
Are duplicate usernames processed twice?
No. Inputs are normalized and deduplicated, so tiktok, @TikTok and the profile URL are one
profile, processed once and counted once.
Can I process multiple profiles in one run? Yes, up to 200 distinct profiles per run.
What happens if I submit more than 200 profiles? Keep the input array within 200 entries, as required by the Input schema. The runtime also rejects more than 200 distinct profiles with a clear message; extra profiles are not silently dropped.
Why are some fields null?
Fields the source did not report are null instead of a made-up value. That is why a photo Story has
no videoUrl. Music fields can be null inside the music object, and missing lists are empty arrays.
How long do Story media URLs remain available?
They are temporary. Some URLs state their own expiry, which is exposed as mediaUrlExpiresAt, but
they can stop working sooner.
What happens if one profile fails?
A temporary data-source failure produces a failed row with TEMPORARY_SOURCE_ERROR for that
profile. Other profiles continue. Allowance-related failures have their own codes listed above.
Can I limit how many Stories come back per profile?
Yes — set maxStoriesPerProfile. Combined with sortStories: "newest" and a value of 1, you get only
the latest active Story per profile.
How does the free allowance work?
If your account includes a free allowance for this Actor, it is counted per profile, not per Story.
Duplicate inputs and retries within the same run do not consume an additional unit. An exhausted
allowance is reported as FREE_USAGE_LIMIT_EXCEEDED for affected profiles.
Can I schedule it? Yes. Create a Task or schedule on the Actor and each run writes its own Dataset you can compare.
🧠 Summary for AI agents and developers
{"actor": "lance_api/tiktok-stories-scraper","platform": "TikTok","collects": "active public TikTok Stories for given profiles","storyTypes": ["video", "photo"],"input": {"usernames": "array of username | @username | profile URL","sortStories": "newest | oldest, default newest","maxStoriesPerProfile": "integer, optional"},"output": {"datasetRows": ["story", "profile_status"],"idsAreStrings": true,"missingValues": "unknown scalars are null; missing lists are empty arrays","media": "URLs only; the Actor does not host files"},"limits": {"maxInputEntries": 200,"maxProfilesPerRun": 200,"duplicatesCharged": false},"optionalFeatures": {"sortStories": "local sort only, no extra request, no extra charge","maxStoriesPerProfile": "local slice only, no extra request, no extra charge"},"auth": "no TikTok login required; Apify token only for API access","pricing": {"unit": "profile","runStartFee": true,"chargedPerStory": false,"seePricingTabForCurrentRates": true}}
⚖️ Responsible use
Collect only public data you are permitted to collect, and follow applicable law, TikTok's terms and the privacy and copyright of the people whose content you store. Do not use the output for spam, harassment or to build profiles of people in ways they would not expect.
🛠️ Support
Email lanceapi2026@hotmail.com or open an issue on the Actor's page and include:
- the run ID,
- the input you submitted (without secrets),
- the status and error code you saw,
- what you expected versus what you got.
Never send your Apify token, API keys, cookies, session data, passwords, authorization headers or proxy credentials.