Docs · Public stories
Public stories
A creator's active public stories, with captions, media and expiration times.
https://api.lurkapi.com/v1/tiktok/user/storiesWhen to use this
See the temporary content a public creator shares alongside regular posts. Each story includes the usual post fields and its expires_at time. Returns up to 20 stories, oldest first; has_more warns if TikTok reports additional stories beyond this page. This endpoint does not paginate. No active stories returns an empty list. Only stories TikTok exposes to logged-out visitors are available; private stories are excluded. Unknown accounts return 404 not_found.
Parameters
All parameters go in the query string.
| Parameter | Description |
|---|---|
handlestringrequired | TikTok username, with or without @ (e.g. nike), or the profile URL.
|
Example request
Replace YOUR_API_KEY with your key, or set LURKAPI_KEY for the code. Get a free key.
curl "https://api.lurkapi.com/v1/tiktok/user/stories?handle=nono95118" \
-H "x-api-key: YOUR_API_KEY"const params = new URLSearchParams({
handle: "nono95118",
});
const res = await fetch(`https://api.lurkapi.com/v1/tiktok/user/stories?${params}`, {
headers: { "x-api-key": process.env.LURKAPI_KEY },
});
const data = await res.json();
if (!data.success) throw new Error(`${data.code}: ${data.error}`);
console.log(data.stories);import os
import requests
res = requests.get(
"https://api.lurkapi.com/v1/tiktok/user/stories",
params={
"handle": "nono95118",
},
headers={"x-api-key": os.environ["LURKAPI_KEY"]},
timeout=60,
)
data = res.json()
if not data["success"]:
raise RuntimeError(f"{data['code']}: {data['error']}")
print(data["stories"])Example response
A real response from this endpoint, captured from the live API and trimmed to a couple of items. Strings over 96 characters (mostly signed media URLs) are cut short and end in ….
Show the example response (4 KB)Hide
{
"success": true,
"credits_remaining": 999956,
"credits_charged": 1,
"user": {
"id": "7322921924349690912",
"uniqueId": "nono95118",
"nickname": "n.o4m'",
"avatarThumb": "https://p19-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/2eefcd56d9d…",
"verified": false
},
"stories": [
{
"id": "7691030223965506849",
"url": "https://www.tiktok.com/@nono95118/video/7691030223965506849",
"desc": "",
"createTime": 1790707521,
"textLanguage": "un",
"locationCreated": null,
"isAd": false,
"author": {
"id": "7322921924349690912",
"uniqueId": "nono95118",
"nickname": "n.o4m'",
"secUid": "MS4wLjABAAAAm4M5uYboGN9cOhADsXDQe_AIUrXvvalv-7c1nV7AUhaiinF_Sk75ktDi8SRKZjfv",
"verified": false,
"privateAccount": false,
"avatarThumb": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/2eefcd56d9d…",
"avatarLarger": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/2eefcd56d9d…"
},
"stats": {
"playCount": 69,
"diggCount": 2,
"commentCount": 0,
"shareCount": 0,
"collectCount": 0,
"repostCount": 0
},
"video": {
"duration": 24,
"width": 576,
"height": 1024,
"definition": "540p",
"cover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast2a-p-0037-euttp/oY5z1…",
"dynamicCover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast2a-p-0037-euttp/osB5Q…",
"playUrl": "https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=a11cc2e02b9b4450…",
"expires_at": 1790952038
},
"music": {
"id": "6716518209370982401",
"title": "Hymn for the Weekend",
"authorName": "Coldplay",
"original": false,
"duration": 60,
"playUrl": "https://sf19.tiktokcdn-us.com/obj/tos-alisg-ve-2774/7b32380bc8be43028a40d287cc6e4489",
"cover": "https://p16-common.tiktokcdn-us.com/tos-alisg-v-2774/9ed8c123c47c4eac897…"
},
"hashtags": [],
"mentions": [],
"images": [],
"expires_at": 1790793921
},
{
"id": "7691227743198940448",
"url": "https://www.tiktok.com/@nono95118/video/7691227743198940448",
"desc": "",
"createTime": 1790753511,
"textLanguage": "un",
"locationCreated": null,
"isAd": false,
"author": {
"id": "7322921924349690912",
"uniqueId": "nono95118",
"nickname": "n.o4m'",
"secUid": "MS4wLjABAAAAm4M5uYboGN9cOhADsXDQe_AIUrXvvalv-7c1nV7AUhaiinF_Sk75ktDi8SRKZjfv",
"verified": false,
"privateAccount": false,
"avatarThumb": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/2eefcd56d9d…",
"avatarLarger": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/2eefcd56d9d…"
},
"stats": {
"playCount": 36,
"diggCount": 0,
"commentCount": 0,
"shareCount": 0,
"collectCount": 0,
"repostCount": 0
},
"video": {
"duration": 10,
"width": 576,
"height": 1024,
"definition": "540p",
"cover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast2a-p-0037-euttp/okrAZ…",
"dynamicCover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast2a-p-0037-euttp/oUfdd…",
"playUrl": "https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=667c80c4e20f4a8d…",
"expires_at": 1790952024
},
"music": {
"id": "7683828213017316118",
"title": "sunet original",
"authorName": "Kashif",
"original": true,
"duration": 14,
"playUrl": "https://v16-webapp-prime.us.tiktok.com/video/tos/no1a/tos-no1a-v-2370-no…",
"cover": "https://p16-common-sign.tiktokcdn-us.com/tos-maliva-avt-0068/bb05916d5c1…"
},
"hashtags": [],
"mentions": [],
"images": [],
"expires_at": 1790839911
}
],
"total_count": 3,
"has_more": false
}Response fields
Every field of a successful response. [] marks a list: a[].b is the b of each item in a. nullable fields can be null; optional fields can be missing.
Show all 59 fieldsHide
| Field | Type | Description |
|---|---|---|
| success | true | Always true here; errors have success: false. |
| credits_remaining | number | Your balance after this call. On anonymous playground calls: free tries left today. |
| credits_charged | number | Credits this call cost; 0 on free endpoints. On anonymous playground calls: tries used (1). |
| user | object | The creator. |
| user.id | string | Numeric user id. |
| user.uniqueId | string | Username (handle), without @. |
| user.nickname | string | Display name. |
| user.avatarThumb | stringnullable | Small avatar URL (expires after a few days); null if TikTok has none. |
| user.verified | boolean | Has the verified badge. |
| stories | object[] | Active public stories, oldest first, up to 20. |
| stories[].id | string | Video id, e.g. 7671325437229845774. |
| stories[].url | string | The post on tiktok.com. |
| stories[].desc | string | Caption, hashtags included. |
| stories[].createTime | number | Posted at, Unix seconds. |
| stories[].textLanguage | stringnullable | Caption language as TikTok detects it, e.g. en; null if unknown. |
| stories[].locationCreated | stringnullable | Country the post was made in, as a 2-letter code; null if unknown. |
| stories[].isAd | boolean | A paid ad (Spark Ad or promoted post). |
| stories[].author | object | Who posted it. |
| stories[].author.id | string | Numeric user id. |
| stories[].author.uniqueId | string | Username (handle), without @. |
| stories[].author.nickname | string | Display name. |
| stories[].author.secUid | string | TikTok's long, stable user id; other tools ask for it. |
| stories[].author.verified | boolean | Has the verified badge. |
| stories[].author.privateAccount | boolean | The account is private. |
| stories[].author.avatarThumb | stringnullable | Small avatar URL (expires after a few days); null if TikTok has none. |
| stories[].author.avatarLarger | stringnullable | Large avatar URL (expires after a few days); null if TikTok has none. |
| stories[].stats | object | Engagement when fetched. Exact numbers, not rounded. |
| stories[].stats.playCount | number | Views. |
| stories[].stats.diggCount | number | Likes. |
| stories[].stats.commentCount | number | Comments. |
| stories[].stats.shareCount | number | Shares. |
| stories[].stats.collectCount | numbernullable | Saves (favorites); null where TikTok doesn't show it. |
| stories[].stats.repostCount | numbernullable | Reposts; null where TikTok doesn't show it. |
| stories[].video | object | The video file. For photo posts see images. |
| stories[].video.duration | number | Length in seconds; 0 for photo posts. |
| stories[].video.width | number | Width in pixels; 0 for photo posts. |
| stories[].video.height | number | Height in pixels; 0 for photo posts. |
| stories[].video.definition | stringnullable | Quality of the default rendition, e.g. 720p. |
| stories[].video.cover | stringnullable | Cover image URL (expires after a few days); null if TikTok has none. |
| stories[].video.dynamicCover | stringnullable | Animated cover URL (expires after a few days); null if TikTok has none. |
| stories[].video.playUrl | stringnullable | MP4 URL that plays without cookies (h264, best quality). It redirects to TikTok's CDN and expires at expires_at; null if TikTok has none. |
| stories[].video.expires_at | numbernullable | When playUrl stops working, Unix seconds; null if unknown. |
| stories[].music | objectnullable | The sound; null if the post has none. |
| stories[].music.id | string | Sound id; pass it to song endpoints. |
| stories[].music.title | string | Sound title, e.g. original sound. |
| stories[].music.authorName | stringnullable | Artist, or the creator for original sounds. |
| stories[].music.original | boolean | An original sound made with this post rather than a track. |
| stories[].music.duration | numbernullable | Length in seconds. |
| stories[].music.playUrl | stringnullable | Audio URL (expires after a few hours); null if TikTok has none. |
| stories[].music.cover | stringnullable | Sound cover image URL; null if TikTok has none. |
| stories[].hashtags | string[] | Hashtags in the caption, without #. |
| stories[].mentions | string[] | Usernames @mentioned in the caption. |
| stories[].images | object[] | The slides of a photo post, in order. Empty for videos. |
| stories[].images[].url | string | Image URL (expires after a few days). |
| stories[].images[].width | number | Width in pixels. |
| stories[].images[].height | number | Height in pixels. |
| stories[].expires_at | number | When this story expires, Unix seconds. |
| total_count | number | Total stories TikTok reported when fetched; may exceed the returned page. |
| has_more | boolean | TikTok reports stories beyond this page; this endpoint does not paginate. |
Caching and freshness
Responses are cached for up to 1 minute, so a repeat call within that window can return the same data. It still costs 1 credit.
Errors
Errors return { success: false, error, code, docs } with the HTTP status below. Validation errors, 401/402/429 rejections and 5xx failures are free; not_found is charged (how charging works). All error codes.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key | No API key. Send it in the x-api-key header. |
| 401 | invalid_api_key | The key is unknown or was revoked. |
| 402 | insufficient_credits | Not enough credits for this call. Buy a pack or wait for tomorrow's top-up. |
| 404 | not_found | The endpoint, or the thing you asked for (ad, post, subreddit), doesn't exist. Charged when the platform was checked. |
| 405 | method_not_allowed | Endpoints take GET with query params. |
| 429 | rate_limited | Too many calls at once. Wait for the retry-after seconds, then retry. |
| 500 | internal_error | Something broke on our side. Retry; 5xx errors are free. |
| 502 | upstream_error | The platform didn't give a usable answer. Retry; 5xx errors are free. |
| 503 | upstream_busy | All our connections to the platform are busy. Retry in a few seconds; 5xx errors are free. |
| 504 | upstream_timeout | The platform took over 30 seconds to answer. Retry; timeouts are free. |
Use it in Claude
Once LurkAPI is connected to Claude, this endpoint is the tiktok_stories tool. Ask in plain English, for example:
Use LurkAPI's tiktok_stories with handle "nono95118" and summarize what you find.Claude calls tiktok_stories with arguments like these, and each call costs 1 credit:
{
"handle": "nono95118"
}Tool results skip nulls and empty lists to save tokens.