Docs · Hashtag videos
Hashtag videos
Videos posted with a hashtag, plus how big the hashtag is: total posts and views.
https://api.lurkapi.com/v1/tiktok/search/hashtagWhen to use this
Use it to research a trend, a niche or a campaign hashtag: which creators post under it, what performs, and how big it is (hashtag.videoCount and hashtag.viewCount, exact). Videos come in the order tiktok.com's hashtag page shows them: neither newest nor most viewed first, and often months old.
Pagination. Up to 30 videos per page. Pass the response's cursor back as cursor (with the same hashtag) for more; has_more is false at the end. A few videos can repeat across pages, so dedupe by id. At times TikTok shows logged-out visitors only a handful of a hashtag's videos (with has_more: false), even for a big one; it serves full pages again later.
Unknown hashtags return 404 not_found.
Parameters
All parameters go in the query string.
| Parameter | Description |
|---|---|
hashtagstringrequired | The hashtag, with or without # (e.g. skincare), or its tiktok.com/tag/… URL.
|
cursorstringoptional | The cursor from the previous response, to get the next page. |
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/search/hashtag?hashtag=smallbusiness" \
-H "x-api-key: YOUR_API_KEY"const params = new URLSearchParams({
hashtag: "smallbusiness",
});
const res = await fetch(`https://api.lurkapi.com/v1/tiktok/search/hashtag?${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.videos);import os
import requests
res = requests.get(
"https://api.lurkapi.com/v1/tiktok/search/hashtag",
params={
"hashtag": "smallbusiness",
},
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["videos"])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 (5 KB)Hide
{
"success": true,
"credits_remaining": 99956,
"credits_charged": 1,
"hashtag": {
"id": "31667995",
"title": "SmallBusiness",
"desc": "Join the #SmallBusiness community",
"videoCount": 44173142,
"viewCount": 252781323890,
"isCommerce": false
},
"videos": [
{
"id": "7451403653912677678",
"url": "https://www.tiktok.com/@franchisejon/video/7451403653912677678",
"desc": "How to own a Nothing Bundt Cakes franchise? #franchise #franchiseopportu…",
"createTime": 1734915113,
"textLanguage": "en",
"locationCreated": null,
"isAd": false,
"author": {
"id": "6609105852464021510",
"uniqueId": "franchisejon",
"nickname": "franchisejon",
"secUid": "MS4wLjABAAAA-ko4urvbwco3xX8NTQjMzh3stLu7nG-SccyZN5Dohp3w70QvgmUIjsqFAn8N-EPm",
"verified": false,
"privateAccount": false,
"avatarThumb": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/92014e7…",
"avatarLarger": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/92014e7…"
},
"stats": {
"playCount": 620900,
"diggCount": 13000,
"commentCount": 663,
"shareCount": 1814,
"collectCount": 1112,
"repostCount": 0
},
"video": {
"duration": 55,
"width": 576,
"height": 1024,
"definition": "540p",
"cover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/oodootgaf…",
"dynamicCover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/oodootgaf…",
"playUrl": "https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=177b34e2f668411f…",
"expires_at": 1790947674
},
"music": {
"id": "7451403593065925422",
"title": "original sound",
"authorName": "franchisejon",
"original": true,
"duration": 55,
"playUrl": "https://v16m.tiktokcdn-us.com/bc91a83fbd76f761a46bd585d6c256b4/6abd62ba/…",
"cover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/92014e7…"
},
"hashtags": [
"franchise",
"franchiseopportunities",
"business",
"businesscheck",
"businesstips",
"tiktokbusiness",
"fyp",
"careeradvice",
"careerchange",
"beyourownboss",
"entrepreneur",
"entrepreneurship",
"franchisee",
"tiktokbusinesscampaign",
"smallbusiness",
"businessowner",
"businesstiktok"
],
"mentions": [],
"images": []
},
{
"id": "7449732446209232170",
"url": "https://www.tiktok.com/@rusty_exotics/video/7449732446209232170",
"desc": "The birthplace of orchids - RustyExotics #SmallBusiness #TikTokBusinessC…",
"createTime": 1734526007,
"textLanguage": "en",
"locationCreated": null,
"isAd": false,
"author": {
"id": "7352872043438359595",
"uniqueId": "rusty_exotics",
"nickname": "RustyExotics",
"secUid": "MS4wLjABAAAALLqyyXChFpFfg4oaT9FON0smbQ5pHXc29absfjMy-w6HyKDNkr5O29ocVuIV2QmB",
"verified": false,
"privateAccount": false,
"avatarThumb": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/68e2af6…",
"avatarLarger": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-avt-0068-tx/68e2af6…"
},
"stats": {
"playCount": 209200,
"diggCount": 17500,
"commentCount": 159,
"shareCount": 971,
"collectCount": 1255,
"repostCount": 0
},
"video": {
"duration": 88,
"width": 576,
"height": 1024,
"definition": "540p",
"cover": "https://p19-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/oMxhD6fDS…",
"dynamicCover": "https://p16-common-sign.tiktokcdn-us.com/tos-useast5-p-0068-tx/oMxhD6fDS…",
"playUrl": "https://www.tiktok.com/aweme/v1/play/?faid=1988&file_id=d49684dac1cd4a3e…",
"expires_at": 1790947707
},
"music": {
"id": "7380329705533573136",
"title": "growth",
"authorName": "Gede Yudis",
"original": false,
"duration": 215,
"playUrl": null,
"cover": "https://p19-common-sign.tiktokcdn-us.com/tos-alisg-v-2774/oYgBEoKYkBBMQE…"
},
"hashtags": [
"smallbusiness",
"tiktokbusinesscampaign",
"biology",
"science",
"orchid",
"plants"
],
"mentions": [],
"images": []
}
],
"has_more": true,
"cursor": "55"
}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). |
| hashtag | object | The hashtag. |
| hashtag.id | string | TikTok's id for the hashtag. |
| hashtag.title | string | The hashtag, without #. |
| hashtag.desc | string | TikTok's description of the hashtag; empty for most. |
| hashtag.videoCount | number | Posts using the hashtag, exact. |
| hashtag.viewCount | number | Total views of those posts, exact. |
| hashtag.isCommerce | boolean | A branded hashtag (a sponsored hashtag challenge). |
| videos | object[] | Posts, in the order tiktok.com shows them (not newest first). |
| videos[].id | string | Video id, e.g. 7671325437229845774. |
| videos[].url | string | The post on tiktok.com. |
| videos[].desc | string | Caption, hashtags included. |
| videos[].createTime | number | Posted at, Unix seconds. |
| videos[].textLanguage | stringnullable | Caption language as TikTok detects it, e.g. en; null if unknown. |
| videos[].locationCreated | stringnullable | Country the post was made in, as a 2-letter code; null if unknown. |
| videos[].isAd | boolean | A paid ad (Spark Ad or promoted post). |
| videos[].author | object | Who posted it. |
| videos[].author.id | string | Numeric user id. |
| videos[].author.uniqueId | string | Username (handle), without @. |
| videos[].author.nickname | string | Display name. |
| videos[].author.secUid | string | TikTok's long, stable user id; other tools ask for it. |
| videos[].author.verified | boolean | Has the verified badge. |
| videos[].author.privateAccount | boolean | The account is private. |
| videos[].author.avatarThumb | stringnullable | Small avatar URL (expires after a few days); null if TikTok has none. |
| videos[].author.avatarLarger | stringnullable | Large avatar URL (expires after a few days); null if TikTok has none. |
| videos[].stats | object | Engagement when fetched. Exact numbers, not rounded. |
| videos[].stats.playCount | number | Views. |
| videos[].stats.diggCount | number | Likes. |
| videos[].stats.commentCount | number | Comments. |
| videos[].stats.shareCount | number | Shares. |
| videos[].stats.collectCount | numbernullable | Saves (favorites); null where TikTok doesn't show it. |
| videos[].stats.repostCount | numbernullable | Reposts; null where TikTok doesn't show it. |
| videos[].video | object | The video file. For photo posts see images. |
| videos[].video.duration | number | Length in seconds; 0 for photo posts. |
| videos[].video.width | number | Width in pixels; 0 for photo posts. |
| videos[].video.height | number | Height in pixels; 0 for photo posts. |
| videos[].video.definition | stringnullable | Quality of the default rendition, e.g. 720p. |
| videos[].video.cover | stringnullable | Cover image URL (expires after a few days); null if TikTok has none. |
| videos[].video.dynamicCover | stringnullable | Animated cover URL (expires after a few days); null if TikTok has none. |
| videos[].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. |
| videos[].video.expires_at | numbernullable | When playUrl stops working, Unix seconds; null if unknown. |
| videos[].music | objectnullable | The sound; null if the post has none. |
| videos[].music.id | string | Sound id; pass it to song endpoints. |
| videos[].music.title | string | Sound title, e.g. original sound. |
| videos[].music.authorName | stringnullable | Artist, or the creator for original sounds. |
| videos[].music.original | boolean | An original sound made with this post rather than a track. |
| videos[].music.duration | numbernullable | Length in seconds. |
| videos[].music.playUrl | stringnullable | Audio URL (expires after a few hours); null if TikTok has none. |
| videos[].music.cover | stringnullable | Sound cover image URL; null if TikTok has none. |
| videos[].hashtags | string[] | Hashtags in the caption, without #. |
| videos[].mentions | string[] | Usernames @mentioned in the caption. |
| videos[].images | object[] | The slides of a photo post, in order. Empty for videos. |
| videos[].images[].url | string | Image URL (expires after a few days). |
| videos[].images[].width | number | Width in pixels. |
| videos[].images[].height | number | Height in pixels. |
| has_more | boolean | There are more posts after this page. |
| cursor | stringnullable | Pass as cursor for the next page. null on the last page. |
Pagination
Pass the response's cursor back as cursor, with the other parameters unchanged, for the next page. It's null on the last page. Each page costs 1 credit.
let cursor = null;
for (let page = 1; page <= 5; page++) {
const params = new URLSearchParams({ hashtag: "smallbusiness" });
if (cursor) params.set("cursor", cursor);
const res = await fetch(`https://api.lurkapi.com/v1/tiktok/search/hashtag?${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.videos);
cursor = data.cursor;
if (!cursor) break; // last page
}Caching and freshness
Responses are cached for up to 10 minutes, 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_hashtag_videos tool. Ask in plain English, for example:
Use LurkAPI's tiktok_hashtag_videos with hashtag "smallbusiness" and summarize what you find.Claude calls tiktok_hashtag_videos with arguments like these, and each call costs 1 credit:
{
"hashtag": "smallbusiness"
}Tool results skip nulls and empty lists to save tokens.