# Douyin Search Scraper (`lanternlab318/douyin-search`) Actor

Search Douyin (抖音, Chinese TikTok) videos by keyword, signed out. One row per video with caption, hashtags, likes, comments, collects, shares, publish time, duration, cover, author and a link, plus the related searches Douyin suggests. Each keyword says whether its results were complete.

- **URL**: https://apify.com/lanternlab318/douyin-search.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Social media
- **Stats:** 5 total users, 4 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$4.99 / 1,000 videos

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Douyin Search Scraper

A Lantern Lab Actor. Search Douyin (抖音, the Chinese TikTok) by keyword and get one row per video: caption, hashtags, likes, comments, collects, shares, publish time, duration, cover, the author and a link to the video. It also writes the searches Douyin suggests for each keyword ("大家还在搜", people also search). No Douyin login, no cookies, no account of your own.

Every keyword gets a summary row that says whether its search was complete. A keyword that could not be searched is marked `failed` with the reason - it is never reported as "0 results".

### Where the data comes from

- **Douyin web search, signed out** (`dataSource: "guest_web"`, the default). The Actor opens Douyin's mobile web search (`so.douyin.com`) the way a visitor without an account does, in a fresh browser session for each keyword, and reads the result list page after page. Only what any signed-out visitor can see is read.
- **Backup search API** (`dataSource: "tikhub"`). Used for a keyword only when you set a filter (web search has none) or when web search could not read that keyword at all. Its rows have the same fields, plus follower counts and IP location.

The results are Douyin's **web search ranking for a signed-out visitor**. They are not the same list the Douyin app shows a signed-in user, and not the same list the backup API returns; the order can also change between sessions and regions. Expect the same topic, not the same videos in the same order.

### What it does

- Searches each keyword until it reaches your limit or Douyin has no more results.
- Writes the related searches Douyin shows under the results (`includeRelatedSearches`, on by default, free).
- Optional sort and filters (publish time, video length, post type) through the backup API.
- One row per video. A video found by two of your keywords is written and charged once, under the first keyword. Douyin's result pages overlap a little; repeats are dropped.

### What it does not do

- No video files, no comments, no full author profiles. For comments, use our Douyin Comments Scraper.
- No follower counts or IP location in web search results: Douyin does not show them to signed-out visitors, so these fields are `null` (never 0).
- No play counts: Douyin does not show them publicly, so `playCount` is almost always `null`.
- No search volume for related searches, and no multi-level keyword expansion.
- No scheduling or monitoring built in; use Apify schedules.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `keywords` | - | One search per keyword. Required. |
| `maxResultsPerKeyword` | 100 | Search results to read per keyword (1-1000). Videos already written under an earlier keyword count toward the limit but are not written again. |
| `includeRelatedSearches` | `true` | Also write the searches Douyin suggests for each keyword. |
| `sort` | `general` | `general` (relevance, 综合排序), `latest` or `mostLiked`. |
| `publishedWithin` | `any` | `any`, `day`, `week` or `halfYear`. |
| `duration` | `any` | `any`, `under1min`, `from1to5min` or `over5min`. |
| `contentType` | `all` | `all`, `video` or `image` (图文). |
| `maxUpstreamRequests` | derived from your input | Hard ceiling on the requests this run may send to the backup data source. Counted in HTTP attempts, so an internal retry uses one up. `0` switches the backup data source off for the run. Leave it empty and the Actor uses `keywords x ceil(maxResultsPerKeyword / 10) x 2 + 2`, which is enough to finish the run you asked for; your own value always wins. |

Douyin web search has no sort or filters. If you set `sort`, `publishedWithin`, `duration` or `contentType` to anything other than the default, every keyword is read from the backup API. If the backup API is not available at the time, the run stops at once with a message - it does not return unfiltered results in their place. Nothing is charged.

Example:

```json
{
  "keywords": ["水溶肥", "雅苒"],
  "maxResultsPerKeyword": 100
}
```

An empty keyword list or an unknown option stops the run at once with a message that says what to fix. Nothing is searched or charged.

### Output

One dataset; `recordType` tells the rows apart. Three table views are predefined: Videos, Related searches and Keyword summaries. A `RUN_SUMMARY` record in the key-value store has the run totals and the status of each keyword.

Video rows (`recordType: "video"`):

| Field | Meaning |
| --- | --- |
| `dataSource` | `guest_web` (signed-out web search) or `tikhub` (backup API). |
| `keyword`, `rank` | The keyword that found the video and its position in the results (from 1). |
| `videoId` | Douyin video (aweme) ID, as a string. |
| `videoUrl` | `https://www.douyin.com/video/<videoId>`, or `https://www.douyin.com/note/<videoId>` for image posts. |
| `postType` | `video` or `image` (图文). |
| `description` | The caption, with hashtags as written. |
| `hashtags` | Hashtags in the caption, without `#`. |
| `likeCount`, `commentCount`, `collectCount`, `shareCount` | Counts at collection time. |
| `playCount` | Usually `null` (Douyin does not show it). |
| `publishedAt` | When the post was published (UTC). |
| `durationSeconds` | Video length; `null` for image posts. |
| `coverUrl` | Cover image link. Douyin signs these links; they expire after some days. |
| `authorId`, `authorSecUid`, `authorNickname`, `authorDouyinId` | The author as shown in the search result. `authorSecUid` opens the profile at `https://www.douyin.com/user/<authorSecUid>`; `authorDouyinId` is the Douyin ID (抖音号). |
| `authorFollowerCount` | Followers. `null` for `guest_web` rows (not shown to signed-out visitors). |
| `ipLocation` | IP location (IP 属地). `null` for `guest_web` rows; for `tikhub` rows, when Douyin includes it. |
| `collectedAt` | When we collected the row (UTC). |

Related-search rows (`recordType: "related_search"`):

| Field | Meaning |
| --- | --- |
| `keyword` | Your keyword. |
| `relatedKeyword` | A search Douyin suggests for it. |
| `rank` | Position in the suggestions, in the order they appeared (from 1). |
| `source`, `sourceLabel` | `people_also_search`, shown on Douyin under the heading 大家还在搜. |
| `sourceCard` | Which suggestion card on the result pages (from 1). Douyin shows several cards as you scroll. |
| `collectionStatus` | `complete`. |
| `dataSource`, `collectedAt` | As for videos. |

Related searches are Douyin's own suggestions for the keyword. They come with no search volume, and Douyin may suggest different words to different visitors, regions and times. They are free.

Keyword summary rows (`recordType: "keyword_summary"`), one per keyword:

| Field | Meaning |
| --- | --- |
| `dataSource` | The source that read the keyword. |
| `collectionStatus` | `complete`: Douyin had no more results. `partial`: stopped before the end, see `stopReason`. `empty`: no results for this keyword and filters. `failed`: nothing could be read - not the same as empty. |
| `stopReason` | `end_of_results`, `limit_reached` (your `maxResultsPerKeyword`), `refused` (Douyin refused signed-out search on every fresh session tried), `upstream_error`, `pagination_stopped` (Douyin said it had more but the next page did not load), `charge_limit` (your maximum charge for the run), `data_source_unavailable` or `not_searched` (the run did not reach this keyword). |
| `statusReason` | Why the keyword is not complete, in words. |
| `videosCollected` | Video rows written and charged for this keyword. |
| `foundUnderEarlierKeyword` | Results already written under an earlier keyword. |
| `resultsSeen`, `pagesRead` | Distinct results read and result pages read. |
| `relatedSearchStatus` | `complete`, `failed` (none could be read; `relatedSearchReason` says why - this does not mean there are none) or `not_requested`. |
| `relatedSearchesCollected` | Related-search rows written. |
| `guestSessions`, `guestRefusals` | Fresh signed-out sessions used, and how many Douyin refused. |
| `fallbackFrom`, `fallbackReason` | When web search failed: why, and whether the backup API read the keyword instead. |
| `sort`, `publishedWithin`, `durationFilter`, `contentTypeFilter` | The options used. |

Counts are what Douyin showed at `collectedAt` and change over time.

#### Was the backup data source used? (`RUN_SUMMARY`)

Three fields in `RUN_SUMMARY` answer that, and a program should read them in this order:

| Field | Meaning |
| --- | --- |
| `backupConfigured` | `true` when this Actor has a backup data source at all. When it is `false` there is nothing to be unavailable: stop here. |
| `backupUnavailableKind` | `null` when the backup source was available. Otherwise `not_configured`, `cap_zero` (you set `maxUpstreamRequests` to 0), `no_credential` (the backup account was rejected or out of credit) or `upstream_error`. The first two are configuration, the last two mean collection that needed the backup source is short. |
| `backupUnavailable` | The same reason in a sentence, for a person to read. The wording can change between builds; do not match on it. |

A non-empty `backupUnavailable` is not a failure by itself. A run that reads every keyword
through signed-out web search reports `backupConfigured: false`,
`backupUnavailableKind: "not_configured"` and still has `allComplete: true`. Whether a keyword is
short is told by its own `collectionStatus` and `stopReason`, not by these three fields.

### Pricing

Pay per event, no start fee:

| Event | Price | Charged for |
| --- | --- | --- |
| `video` | $4.99 per 1,000 videos ($0.00499 each) | Each video row written. |

Related-search rows and summary rows are free. A video found by several keywords is charged once. Set a maximum charge for the run and the Actor stops before going over it; the keyword's summary says `charge_limit`.

### Limits

- Douyin decides how many results a search returns. A search can end before your limit; the summary then says `end_of_results`.
- Web search depth varies by keyword and session. If Douyin stops loading more pages, the keyword is `partial` with `pagination_stopped` and keeps what was read.
- Douyin sometimes asks a signed-out session for a verification check. The Actor then tries a fresh session (at most three per keyword); if every one is refused, the keyword falls back to the backup API, or is marked `failed` with `refused`.
- The backup API has its own quota. When it is used up, keywords that need it are marked `partial` or `failed` with `data_source_unavailable`, the status message says so, and keywords that web search can read still run.
- `maxUpstreamRequests` is a hard ceiling, not a target: the run sends at most that many requests to the backup data source, counted in HTTP attempts, so an internal retry of a request that timed out uses one up. Reaching it gives you **incomplete results**: collection stops where it is, every row already written is kept, and the keyword's summary is `partial` (when it had results) or `failed` (when it had none) with `stopReason` `upstream_request_cap` and a `statusReason` that says which ceiling stopped it, not Douyin.
- **Every run has this ceiling, including one that does not ask for it.** When you leave `maxUpstreamRequests` empty, the Actor works it out from the rest of your input: `keywords x ceil(maxResultsPerKeyword / 10) x 2 + 2`. That is the pages the run has to read (the backup source returns at least 10 results per page), doubled so a retried request does not cut collection short, plus two spare - so the default is enough to finish the run you asked for, and a single run cannot send an unbounded number of backup requests. One keyword and the default 100 results give 22. A value you pass yourself always wins, whether it is larger or smaller. `RUN_SUMMARY` reports which of the two was used in `backupCapSource` (`input` or `derived`) and the number itself in `backupCap`.
- `maxUpstreamRequests: 0` switches the backup data source off for the whole run. Keywords that signed-out web search cannot read are then `failed` and say the backup source was off because you set the limit to 0. Runs that need filters are refused before anything is searched, and no request leaves the Actor. `RUN_SUMMARY` then says `backupUnavailableKind: "cap_zero"`, which is your input, not a broken source.
- The ceiling applies to the whole run, so with several keywords the last ones can be the incomplete ones. `RUN_SUMMARY` reports `backupAttempts` (HTTP attempts, the number the ceiling counts), `backupRequests` (attempts the backup source answered), `backupCap`, `backupCapSource` and `backupCapped`.
- The ceiling covers every request to the backup data source except one: a run that uses filters checks the backup account once before it starts, so it can refuse the filters instead of failing on the first page. That check reads no search results and is never billed, and it is skipped when `maxUpstreamRequests` is 0, so a run capped at 0 sends no request at all.
- Results are what Douyin returned at the time of the run.

### Feedback

Open an issue on this Actor with the keyword, the options you used and the output you expected.

# Actor input Schema

## `keywords` (type: `array`):

One search per keyword, in Chinese or English, as you would type it on Douyin.

## `maxResultsPerKeyword` (type: `integer`):

Stop each keyword after this many search results. Douyin may end a search earlier; the keyword's summary row says which happened.

## `includeRelatedSearches` (type: `boolean`):

Also write the searches Douyin suggests for each keyword ("大家还在搜", people also search) as rows with recordType "related_search". Free: only videos are charged.

## `sort` (type: `string`):

Order of the search results, as in the Douyin app. Douyin web search has no filters: any value other than the default uses a backup data source, and the run stops at once with a message if that source is not available.

## `publishedWithin` (type: `string`):

Only posts published in this period. Douyin offers no other ranges. Douyin web search has no filters: any value other than the default uses a backup data source, and the run stops at once with a message if that source is not available.

## `duration` (type: `string`):

Only videos of this length. Douyin web search has no filters: any value other than the default uses a backup data source, and the run stops at once with a message if that source is not available.

## `contentType` (type: `string`):

Only videos, only image posts (图文), or both. Douyin web search has no filters: any value other than the default uses a backup data source, and the run stops at once with a message if that source is not available.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the requests this run may send to the backup data source. Counted in HTTP attempts, so an internal retry uses one up. The run never sends more than this, whatever the keywords or filters ask for; when the ceiling is reached, collection stops there and the keyword's summary row is partial or failed with stopReason "upstream_request_cap", so results can be incomplete. 0 switches the backup data source off for the whole run: keywords that signed-out web search cannot read are then reported as failed, and filtered runs are refused before anything is searched. Leave empty and the Actor works the ceiling out from the rest of your input - keywords x ceil(maxResultsPerKeyword / 10) x 2 + 2, which is 22 for one keyword and 100 results - so a run always has an upper bound; RUN_SUMMARY reports the number used in backupCap and where it came from in backupCapSource.

## Actor input object example

```json
{
  "keywords": [
    "水溶肥"
  ],
  "maxResultsPerKeyword": 100,
  "includeRelatedSearches": true,
  "sort": "general",
  "publishedWithin": "any",
  "duration": "any",
  "contentType": "all"
}
```

# Actor output Schema

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

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "keywords": [
        "水溶肥"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/douyin-search").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 = { "keywords": ["水溶肥"] }

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/douyin-search").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 '{
  "keywords": [
    "水溶肥"
  ]
}' |
apify call lanternlab318/douyin-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lanternlab318/douyin-search"
        }
    }
}
```

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/Wa6RK41p5eMbL0ex1/builds/eYqqv2Ffwx4LOaQYd/openapi.json
