# Douyin 抖音视频 Video Scraper - Metadata & No-Watermark URL (`lanternlab318/douyin-video`) Actor

抖音视频详情 for any public Douyin (抖音) post: description, publish time, length, likes, comments, shares, saves, hashtags, music, cover and the watermark-free play URL. Paste video links, /note/ album links, v.douyin.com share links or bare IDs. No login.

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

## Pricing

from $2.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 抖音视频 Video Scraper

Read the full public detail (抖音视频详情) of any Douyin (抖音) post: description, publish time, length, likes, comments, shares, saves, hashtags, music, cover image and the **watermark-free MP4 play URL** - exact numbers, not Douyin's rounded `9.8万`. Paste video links, `/note/` album links, `v.douyin.com` share links or bare IDs; no login, no cookies, no account of your own. **$2.99 per 1,000 videos, plus $0.005 per run.** See [Pricing](#pricing).

Input:

```json
{
  "videoUrls": [
    "https://www.douyin.com/video/7683413288402612731",
    "https://v.douyin.com/iJSMnqRm/"
  ]
}
```

Output - one row per video (`recordType: "video"`), plus one free row per input line and a `RUN_SUMMARY`. Sample from a real run; nickname partially masked, URLs shortened:

```json
{
  "recordType": "video",
  "platform": "douyin",
  "dataSource": "guest_web",
  "awemeId": "7683413288402612731",
  "url": "https://www.douyin.com/video/7683413288402612731",
  "postType": "video",
  "desc": "果树补钙用绿陇糖醇钙，移动性强，好吸收！#糖醇钙 #果树种植 #农资肥料 #钙肥",
  "createTime": 1788934062,
  "publishedAt": "2026-09-09T06:07:42+00:00",
  "durationSec": 38.4,
  "author": {
    "nickname": "绿陇***",
    "secUid": "MS4wLjABAAAA…",
    "uid": "1051…",
    "douyinId": "lvlong2018",
    "url": "https://www.douyin.com/user/MS4wLjABAAAA…",
    "verified": true
  },
  "diggCount": 41,
  "commentCount": 2,
  "shareCount": 3,
  "collectCount": 6,
  "playCount": null,
  "hashtags": ["糖醇钙", "果树种植", "农资肥料", "钙肥"],
  "musicTitle": "@绿陇生物创作的原声",
  "musicAuthor": "绿陇***",
  "coverUrl": "https://p3-pc-sign.douyinpic.com/…",
  "videoPlayUrl": "https://v3-web.douyinvod.com/…",
  "imageUrls": [],
  "location": null,
  "input": "https://www.douyin.com/video/7683413288402612731",
  "videoStatus": "available",
  "accessMode": "direct",
  "collectionStatus": "complete",
  "partial": false,
  "partialReason": null,
  "elapsedMs": 1842,
  "collectedAt": "2026-10-24T05:41:12+00:00"
}
```

Limits:

- **Public data only.** The Actor reads what Douyin shows a signed-out visitor and never logs in. No comments, no private or login-only posts, and nothing about the author beyond the public display name and IDs on the post.
- **No MP4 is downloaded.** `videoPlayUrl` is the watermark-free stream Douyin's own player uses, handed to you as a link. It is signed and short-lived, so fetch it soon after the run and send a `douyin.com` referer. Copying the bytes through Apify storage would bill you for video traffic you may not want.
- **Every row says how complete it is.** A post Douyin would not describe - taken down, private, withheld - is reported as such with the reason, never as a video with zero likes.
- **Your limits stop collection and appear in the row**: the `maxUpstreamRequests` request ceiling, the residential-fallback budget (`maxFallbackVideos`) and the maximum charge per run (see [Pricing](#pricing)).

### What it does

- Reads each post's detail the way Douyin's own video page shows it, and writes exactly one row per video - even for a post that could not be read.
- Accepts every link shape Douyin hands out: `/video/<id>`, `/note/<id>` and `/slides/<id>`, `v.douyin.com` share links (followed to the post), `?modal_id=<id>` links, and bare numeric IDs.
- Costs one Douyin request per video. A run of 100 videos is one browser session and 100 requests.
- Deduplicates inside a run: the same post asked for twice is fetched once and charged once, and the second line is reported as a `duplicate`.
- Handles 图文 (image-album) posts: their pictures come back in `imageUrls`, and they carry no `videoPlayUrl` because Douyin serves none.
- Marks a row `partial` with a reason when the post was not read: your request limit, your cost limit, or a refusal by Douyin.
- Runs without a proxy by default. If Douyin refuses the request for a post, that one post is retried through an Apify residential proxy exiting in China (limited per run, can be turned off). A post Douyin *does* answer about - gone, private, withheld - is never retried, because no exit changes that answer.
- Stops minting fresh visitor sessions once a run has seen two posts refused and none answered: Douyin withholds from every visitor, not from one session, so a third page load would only buy the same answer. Each post is still asked once, so each keeps its own evidence.
- Writes a `RUN_SUMMARY` - last row of the dataset and a key-value record - with the totals, the requests sent against the ceiling in force, and why the run stopped.

### What it does not do

- **No video files.** The play URL is handed over; nothing is downloaded or re-hosted, so you are never billed for video traffic.
- **No keyword search.** Bring links or IDs. To find videos by keyword first, use the [Douyin keyword search Actor](https://apify.com/lanternlab318/douyin-search).
- **No comments** - use the [Douyin comments Actor](https://apify.com/lanternlab318/douyin-comments). **No account profile numbers** - use the [Douyin profile Actor](https://apify.com/lanternlab318/douyin-profile).
- **No play counts.** Douyin's web API answers a constant `0` for plays whatever the real number, so `playCount` is `null` rather than a figure the platform never stated.
- **No private data.** Only what Douyin's own video page shows. No email, no phone, no personal name - just the author's public display name, their public IDs and the badge Douyin itself displays.
- No login-only or age-restricted posts, no live streams, no scheduling built in.

### Input

| Field | Default | Meaning |
| --- | --- | --- |
| `videoUrls` | - | Video links (`https://www.douyin.com/video/7...`), album links (`/note/7...`), share links (`https://v.douyin.com/...`), links carrying `modal_id=...`, or bare numeric video IDs. |
| `maxUpstreamRequests` | worked out from your input | Hard ceiling on Douyin requests for the whole run, retries included. Leave empty and the Actor derives one, so a run is never unbounded. |
| `proxyConfiguration` | no proxy | Standard Apify proxy setting for all requests. |
| `residentialFallback` | true | Retry a refused video through a residential proxy in China. |
| `maxFallbackVideos` | 3 | Videos per run that may use that fallback, at most 20. 0 turns it off. |
| `requestDelaySecs` | 1 | Minimum pause between two requests from one visitor session. |
| `maxIdentityRotations` | 6 | Fresh visitor sessions a run may mint after a refusal. |
| `msTokenWaitSecs` | 20 | How long a new visitor page may take to receive its security token. |

### Output

Three row kinds in the default dataset, so three different facts can never be mistaken for each other.

| `recordType` | One per | Charged |
| --- | --- | --- |
| `video` | video looked up | only when it carries data |
| `input_status` | input line - what it resolved to (`found`, `duplicate`, `invalid_url`, `not_collected`) | never |
| `RUN_SUMMARY` | run, as the last row (also in the key-value store) | never |

Fields on a `video` row:

| Field | Meaning |
| --- | --- |
| `awemeId` | Douyin's own post ID, as a string - it is past the JavaScript safe integer range. |
| `url`, `postType` | The post's page, and whether it is a `video` or an `image_album` (图文). |
| `desc` | The description exactly as Douyin shows it, hashtags included. Douyin has one description field; there is no separate title. |
| `createTime`, `publishedAt` | The author's publish time, as Unix epoch seconds and as UTC ISO 8601. |
| `durationSec` | Video length in seconds. `null` for an album. |
| `author` | `nickname`, `secUid` (the permanent `MS4w...` ID), numeric `uid`, `douyinId` (抖音号), profile `url`, `verified`. |
| `diggCount`, `commentCount`, `shareCount`, `collectCount` | Likes, comments, shares and saves - exact, not rounded to 万. |
| `playCount` | Always `null`; see [What it does not do](#what-it-does-not-do). |
| `hashtags` | Hashtags without the leading `#`, in Douyin's order. |
| `musicTitle`, `musicAuthor` | The track on the post, and who Douyin credits. |
| `coverUrl`, `coverUrls` | The cover image, and every cover mirror Douyin served to fail over between. |
| `videoPlayUrl` | The watermark-free MP4 stream. Signed and short-lived; expects a `douyin.com` referer. |
| `imageUrls` | Every picture of an album post, in page order. Empty for a video. |
| `location` | The place name on the post, when the author attached one. |
| `input`, `collectedAt`, `elapsedMs` | Your input line, when the row was collected (UTC, ISO 8601), and time spent on it. |
| `videoStatus` | `available`, `unavailable`, `deleted`, `private` or `unknown`. See below. |
| `accessMode` | `direct`, `custom_proxy` or `residential_fallback`. |
| `collectionStatus`, `partial`, `partialReason` | How complete the row is, and why not. |

A row never carries an invented value: a field Douyin stated nothing about is `null`, not `0` and not an empty string. A missing like count and a like count of zero are different facts.

#### Video status

| `videoStatus` | Meaning |
| --- | --- |
| `available` | The post was read. The only status that is charged. |
| `unavailable` | Douyin answered but would not describe the post - withdrawn, under review, or withheld. Not "an empty video". |
| `deleted`, `private` | Douyin said so explicitly. Whatever metadata it still returned is kept, flagged. |
| `unknown` | The request was refused, or the run stopped before sending it. |

#### What `partial` means

`partial` is true whenever `collectionStatus` is not `complete`, and `partialReason` says why:

| Reason | Meaning |
| --- | --- |
| `cap:maxUpstreamRequests=N` | The run's request ceiling stopped it - yours, or the one derived from your input. `RUN_SUMMARY.upstreamCapSource` says which. |
| `cap:maxTotalChargeUsd` | Your **Maximum cost per run** was reached; rows past it are not stored. |
| `cap:maxInputs=N` | More input lines than one run accepts; the rest are listed, not collected. |
| `blocked:<accessMode>:...` | Douyin refused the request on that exit, after fresh visitor sessions. The text keeps the classification and Douyin's own status code. |
| `detail:filtered:<Douyin's wording>` | Douyin answered about the post but would not describe it, in its own words. |
| `detail:is_delete`, `detail:private_status` | Douyin says the post is deleted or private. |
| `fallback_cap:maxFallbackVideos=N` | The post was refused and the run had used up its residential retries. |
| `input:no_aweme_id` | No Douyin post ID in that input line. |
| `input:duplicate_in_run` | The same post was already collected earlier in this run. |

`collectionStatus` is `complete` when the post was read, `blocked` when Douyin refused the request, and `not_collected` when nothing could be read or nothing was attempted.

### Pricing

Pay per event: **$2.99 per 1,000 videos, plus $0.005 for each run.** Apify platform usage, including the browser session and the residential fallback, is included; these two events are the whole price.

| Event | Price | Charged for |
| --- | --- | --- |
| `result` | $0.00299 ($2.99 per 1,000) | Each row that carries a video. |
| `run-start` | $0.005, once per run | The signed-out browser session every run opens before it reads any post. |

A row that only reports a failure - an input line with no post ID, a duplicate, a post Douyin would not describe, a post the run never reached - is written **free**. A run that reads no post at all still pays the $0.005 session.

The session is a flat fee per run, not per video, so a batch costs less per video than many small runs: 10 videos cost $0.035 ($0.0035 each), 100 videos cost $0.304 ($0.0030 each), 1,000 videos cost $2.995.

To cap a run, set **Maximum cost per run** in the run options. When the cap is reached the Actor stores no further rows, stops before the next video, marks what it cut short `partial` with `cap:maxTotalChargeUsd`, and records it in `RUN_SUMMARY`.

### Resources

- Runs with **1 GB memory** by default; 1-8 GB can be chosen in the run options. A visitor session is minted in a real browser, which needs 1 GB.
- A run opens a visitor session first (about 20-40 seconds), then takes a couple of seconds per video.

### Notes

- Respect Douyin's terms and local law, and only collect what you need. Nicknames and descriptions are personal data in many jurisdictions.
- Douyin changes its web client often. `RUN_SUMMARY` reports request outcomes per endpoint, so a change shows up there first.

# Actor input Schema

## `videoUrls` (type: `array`):

Public Douyin posts, one per line: a video link (https://www.douyin.com/video/7...), an image-album link (/note/7...), a share link (https://v.douyin.com/...), a link carrying modal_id=..., or the bare numeric video ID. Keyword search is not supported here - use the Douyin Search Scraper to find videos first.

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

Hard ceiling on requests sent to Douyin in this run, retries included. Leave it empty and the Actor works one out from the number of videos you gave it, so a run is never unbounded; 0 sends no request at all. RUN_SUMMARY reports which ceiling was in force.

## `proxyConfiguration` (type: `object`):

Proxy for all requests. Off by default: Douyin answers guests without a proxy.

## `residentialFallback` (type: `boolean`):

When Douyin refuses the request for a video (captcha, empty answer), retry that video once through an Apify residential proxy exiting in China. Residential traffic costs more than direct requests. A video Douyin does answer about - taken down, private, withheld - is never retried: no exit changes that answer.

## `maxFallbackVideos` (type: `integer`):

Upper limit per run on videos moved to the residential proxy. 0 turns the fallback off.

## `requestDelaySecs` (type: `number`):

Minimum pause between two requests from the same guest identity.

## `maxIdentityRotations` (type: `integer`):

How many times a refused request may be retried on a freshly minted guest identity before the video is reported as blocked.

## `msTokenWaitSecs` (type: `integer`):

How long a new guest page may take to receive its security token (msToken) before requests start. Longer waits make requests more likely to be answered.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.douyin.com/video/7683413288402612731",
    "https://v.douyin.com/iJSMnqRm/"
  ],
  "maxUpstreamRequests": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "residentialFallback": true,
  "maxFallbackVideos": 3,
  "requestDelaySecs": 1,
  "maxIdentityRotations": 6,
  "msTokenWaitSecs": 20
}
```

# Actor output Schema

## `videos` (type: `string`):

No description

## `inputs` (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 = {
    "videoUrls": [
        "https://www.douyin.com/video/7683413288402612731",
        "https://v.douyin.com/iJSMnqRm/"
    ],
    "maxUpstreamRequests": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/douyin-video").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 = {
    "videoUrls": [
        "https://www.douyin.com/video/7683413288402612731",
        "https://v.douyin.com/iJSMnqRm/",
    ],
    "maxUpstreamRequests": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/douyin-video").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 '{
  "videoUrls": [
    "https://www.douyin.com/video/7683413288402612731",
    "https://v.douyin.com/iJSMnqRm/"
  ],
  "maxUpstreamRequests": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call lanternlab318/douyin-video --silent --output-dataset

```

## MCP server setup

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

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/aHxTGxAP687zgwRgO/builds/wDxDt5r7n2cXzVFb8/openapi.json
