# TikTok Hashtag Scraper - Videos, Views & Tag Analytics (`s-r/tiktok-hashtag-scraper`) Actor

Scrape TikTok hashtags: every video posted under a tag with its caption, author, views, likes, comments and shares, plus the tag's own total views and video count. Filter by date and likes, sort, and download the videos and covers.

- **URL**: https://apify.com/s-r/tiktok-hashtag-scraper.md
- **Developed by:** [SR](https://apify.com/s-r) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## TikTok Hashtag Scraper: videos, views and tag analytics

Give it hashtags and get two things back. One row per video posted under each
tag, with the caption, the author and their follower count, the views, likes,
comments and shares, the sound and the post date. And one row per hashtag
carrying its total view count, which is the number that tells you whether a tag
is worth posting under at all.

No login, no API key, no TikTok developer account.

### What you get

**Per video**: `video_id`, `url`, `description`, `created_at`, `handle`,
`nickname`, `author_id`, `author_verified`, `follower_count`,
`author_video_count`, `play_count`, `like_count`, `comment_count`,
`share_count`, `collect_count`, `duration_seconds`, `width`, `height`,
`cover_url`, `play_url`, `download_url`, `music_title`, `music_author`,
`music_id`, `hashtags`, `is_ad`, and the `hashtag` that produced the row.

Counts arrive as integers, not as the `1.2M` strings the site shows.

**Per hashtag**: `hashtag`, `hashtag_id`, `url`, `description`, `view_count`,
`is_commerce`.

**Optional**: eight extra author fields (bio, avatar, region, following count,
lifetime likes), and downloads of the videos, thumbnails, avatars, sound
artwork and photo-post slideshow images into the run's key-value store.

### Why a hashtag is worth measuring

A hashtag is the only free distribution lever on TikTok. Which tag a post
carries decides which feed it is eligible for, and the difference between a
tag with 27 billion views and one with 200 billion is the difference between a
niche and a firehose.

Two questions this answers and the app does not. Which accounts actually get
reach under a tag, which is the video rows grouped by handle and sorted by
views. And whether a tag is still alive, which is the post dates on the rows:
a tag whose top results are two years old is a tag nobody is posting under now.

### The one thing to know before you run it

**The videos come from TikTok's search, not from the hashtag page's own feed,
because that feed is closed.** The endpoint the hashtag page uses to list its
videos answers with an empty response. That is not a limitation of this Actor:
TikTok's own hashtag page, loaded in a real browser with every parameter it
normally sends, gets the same empty response and renders zero videos.

So a tag's videos are fetched by searching for the tag, which does work, and
the run summary reports `videosFromSearch` rather than implying a feed that
does not exist. In practice the result is the same set of videos you would see
on the tag page, ranked TikTok's way.

**`video_count` on a hashtag row is empty on purpose.** TikTok still sends the
field and it reads `0` on every tag tested, including `#booktok` at 551.1
billion views and `#fyp` at 128 trillion. Publishing that zero would put a
sortable column in your export claiming the biggest tags on the platform have
no videos in them. `view_count` is live and correct, and that is the figure the
hashtag row is for.

### Input

| Field | Type | Notes |
|---|---|---|
| `hashtags` | array | One per line. `#coffee`, `coffee` and the full tag link are the same tag. Required. |
| `maxVideosPerHashtag` | integer | Default 60. Set to 0 for reach figures only. |
| `includeHashtagStats` | boolean | Default on. One extra request per tag. Free. |
| `resultsPerPage` | integer | 1 to 30, default 30. |
| `sortBy` | select | `relevance`, `most_liked`, `most_viewed`, `most_commented`, `most_shared`, `newest`, `oldest`. |
| `oldestPostDate` | string | `YYYY-MM-DD`, an ISO timestamp, or `7 days` / `3 months`. |
| `newestPostDate` | string | Same formats. |
| `leastDiggs` | integer | Minimum likes. |
| `mostDiggs` | integer | Maximum likes. Useful for finding small accounts rather than the same viral posts. |
| `excludeAds` | boolean | Drop rows TikTok marks as advertising. |
| `scrapeAdditionalAuthorMeta` | boolean | Eight extra author fields. Free. |
| `shouldDownloadVideos` | boolean | Store the MP4 and put a working link on the row. |
| `shouldDownloadCovers` | boolean | Store the thumbnail. |
| `shouldDownloadAvatars` | boolean | Store the author avatar. |
| `shouldDownloadMusicCovers` | boolean | Store the sound artwork. |
| `shouldDownloadSlideshowImages` | boolean | Store each image of a photo post. |
| `maxMediaDownloads` | integer | Ceiling across every download option. Default 50. |
| `country` | string | Two-letter code. Results are regional. |
| `language` | string | Two-letter code. |
| `proxyCountryCode` | string | Where the request should appear to come from. |

### Filters cost pages, and the summary says so

Every filter is applied to the rows the run collected. TikTok's search endpoint
accepts a sort and a date parameter and then ignores both, which was measured
rather than assumed: the same tag returned an identical result ordering across
every value of each.

So a narrow date window or a high like threshold reads **more** pages and
returns fewer rows, which is the opposite of how a server-side filter behaves.
The summary carries `videosScanned` next to `videosRejectedByFilters` and
`pagesRead`, so a filter that matched little reads as a filter rather than as
a tag that has gone quiet.

### Speed

One tag, three pages: **99 videos in 5.4 seconds**, all 99 carrying a view
count, 89 carrying a playable media address. The other ten are photo posts,
which have no video at all and come back with their slideshow images instead.

The tag's reach figures are one extra request and took 0.3 seconds.

### Media links that actually work

A raw media address from TikTok is not a link you can use. Paste one into a
browser, a CMS or a colleague's message and it returns 403. Turn on a download
option and the file is stored in the run's key-value store with a public link
on the row, which works anywhere.

Photo posts have no video. Those rows carry `slideshow_images` and the
slideshow download option stores each image.

### What people use this for

**Finding creators to work with.** Video rows grouped by handle, sorted by
views, filtered to accounts under a follower ceiling. That is a shortlist of
people getting reach in a niche without the follower count to price
accordingly.

**Content research.** Sort by most liked within a date window and you have the
formats that are working under a tag this month, not the ones that worked in
2024\.

**Tag selection.** Run twenty candidate tags with `maxVideosPerHashtag` at 0
and you get twenty reach figures in a few seconds, for the price of twenty
requests.

**Brand monitoring.** A branded tag run daily, with the post-date filter set to
the last day, is every new post carrying your tag.

### Notes

View, like and comment counts are live and change through the day. A run is a
snapshot.

A tag that does not exist is reported by name with the reason, rather than
returning zero rows that look like a tag nobody uses.

Free Apify plans are capped at 10 results per run. The run stops collecting at
that point rather than doing the whole crawl and discarding it.

# Actor input Schema

## `hashtags` (type: `array`):

One per line. The leading # is optional and a full https://www.tiktok.com/tag/... link works too, so 'coffee', '#coffee' and the link are the same tag.

## `maxVideosPerHashtag` (type: `integer`):

TikTok serves roughly 200 unique videos per tag before it stops, so asking for more returns what exists. Set it to 0 to collect only the reach figures.

## `includeHashtagStats` (type: `boolean`):

Adds one row per tag with its total views and how many videos carry it. Free: it is a single extra request per tag.

## `resultsPerPage` (type: `integer`):

How many videos to ask for per request, up to 30. Lower means more requests for the same total.

## `sortBy` (type: `string`):

Applied to the rows this run collected, not to TikTok's feed order. 'Relevance' keeps TikTok's own ordering untouched.

## `oldestPostDate` (type: `string`):

YYYY-MM-DD, a full ISO timestamp, or a relative window such as '7 days' or '3 months'. Filtering happens after the fetch, so a narrow window reads more pages and returns fewer rows.

## `newestPostDate` (type: `string`):

Same formats as the field above.

## `leastDiggs` (type: `integer`):

Drop videos with fewer likes than this.

## `mostDiggs` (type: `integer`):

Drop videos with more likes than this. Useful for finding small accounts rather than the same viral posts.

## `excludeAds` (type: `boolean`):

Drop rows TikTok marks as advertising.

## `scrapeAdditionalAuthorMeta` (type: `boolean`):

Adds the author's bio, avatar, region, following count and total likes. Free: the fields are already in the response.

## `shouldDownloadVideos` (type: `boolean`):

Stores the MP4 in the run's key-value store and puts a public link on the row. TikTok's raw media links return 403 to anyone who pastes them elsewhere, which is why downloading is the only way to get a link that works.

## `shouldDownloadCovers` (type: `boolean`):

Stores each video's thumbnail in the key-value store and puts a public link on the row.

## `shouldDownloadAvatars` (type: `boolean`):

Stores the author's or account's profile picture and puts a public link on the row.

## `shouldDownloadMusicCovers` (type: `boolean`):

Stores the artwork of the sound used in the video.

## `shouldDownloadSlideshowImages` (type: `boolean`):

Photo posts have no video at all. This stores each image instead.

## `maxMediaDownloads` (type: `integer`):

A ceiling across every download option, so a large search cannot turn into a large bill by accident.

## `country` (type: `string`):

Two-letter country code sent with the request. TikTok serves follower lists regionally, so this can change the ordering.

## `language` (type: `string`):

Two-letter language code for the search.

## `proxyCountryCode` (type: `string`):

Two-letter code for where the request should appear to come from. Leave empty to use the default route. Setting the country above changes what TikTok ranks; setting this changes where the request arrives from, and the two are worth matching.

## Actor input object example

```json
{
  "hashtags": [
    "cottagecore",
    "bookrecommendations"
  ],
  "maxVideosPerHashtag": 60,
  "includeHashtagStats": true,
  "resultsPerPage": 30,
  "sortBy": "relevance",
  "oldestPostDate": "2026-01-01",
  "newestPostDate": "2026-06-30",
  "excludeAds": false,
  "scrapeAdditionalAuthorMeta": false,
  "shouldDownloadVideos": false,
  "shouldDownloadCovers": false,
  "shouldDownloadAvatars": false,
  "shouldDownloadMusicCovers": false,
  "shouldDownloadSlideshowImages": false,
  "maxMediaDownloads": 50,
  "country": "us",
  "language": "en",
  "proxyCountryCode": "gb"
}
```

# Actor output Schema

## `results` (type: `string`):

One row per hashtag and one per video.

## `summary` (type: `string`):

What was asked for, what came back and how many pages it took.

## `errors` (type: `string`):

Targets that returned nothing, with the reason.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hashtags": [
        "cottagecore"
    ],
    "country": "us",
    "language": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/tiktok-hashtag-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "hashtags": ["cottagecore"],
    "country": "us",
    "language": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/tiktok-hashtag-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hashtags": [
    "cottagecore"
  ],
  "country": "us",
  "language": "en"
}' |
apify call s-r/tiktok-hashtag-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/tiktok-hashtag-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/q6kmiVt5BkKh69PMC/builds/8xoPOdsEbbv5xGKyn/openapi.json
