{
  "data": {
    "id": "75caFFr2NTE3Pf7ul",
    "actId": "BvUQENdTgrozjghWJ",
    "userId": "IYgBDiXtHOijAaUel",
    "startedAt": "2026-11-01T13:31:55.620Z",
    "finishedAt": "2026-11-01T13:32:34.247Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 38469,
      "runTimeSecs": 38.469,
      "computeUnits": 0.042743333333333335,
      "imageSizeBytes": 3149595648
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "createdByOrganizationMemberUserId": "QYo53EZkJk0prePWY",
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"TikTok Explore/Trending Scraper Input\",\n  \"description\": \"Collect the public TikTok Explore feed with no login or cookies. All=120 is the default; 123 is accepted as a legacy alias. Optional country selection, filters, only-new monitoring, transcript text, post-detail enrichment and Key-Value Store media rehosting are additive.\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"categories\": {\n      \"title\": \"Categories\",\n      \"type\": \"array\",\n      \"default\": [\n        \"All\"\n      ],\n      \"prefill\": [\n        \"All\"\n      ],\n      \"editor\": \"json\",\n      \"description\": \"Categories to collect. Accepts names, numeric IDs, or legacy {id,name} objects. All=120; topics 100-119: Anime & Comics, Shows, Beauty Care, Games, Comedy, Daily Life, Family, Relationship, Drama, Outfit, Lipsync, Food, Sports, Animals, Society, Cars, Education, Fitness & Health, Technology, Singing & Dancing. 123 is accepted as a legacy alias for All. 121/122 (empty upstream) and 124+ (invalid upstream) are skipped with a warning.\"\n    },\n    \"maxItemsPerCategory\": {\n      \"title\": \"Max Items Per Category\",\n      \"type\": \"integer\",\n      \"default\": 3,\n      \"minimum\": 1,\n      \"maximum\": 1000,\n      \"description\": \"Hard cap on emitted posts per category. The cap is applied before any page is pushed, so a 30-item page never overshoots. The default is 3 because media rehosting is on by default and adds one browser page plus several HTTP media fetches per emitted post.\",\n      \"prefill\": 3\n    },\n    \"maxItems\": {\n      \"title\": \"Max Items (legacy alias)\",\n      \"type\": \"integer\",\n      \"minimum\": 1,\n      \"maximum\": 1000,\n      \"description\": \"Legacy alias for maxItemsPerCategory. It is used when maxItemsPerCategory is absent or exactly the default 3; an explicitly non-default maxItemsPerCategory wins.\"\n    },\n    \"categoryId\": {\n      \"title\": \"Category ID (legacy shorthand)\",\n      \"type\": \"integer\",\n      \"minimum\": 1,\n      \"maximum\": 100000,\n      \"description\": \"Legacy shorthand for one category. It replaces the categories list. 100-120 are accepted; 123 maps to 120; other values are skipped with a warning.\"\n    },\n    \"autoDiscoverCategories\": {\n      \"title\": \"Auto-Discover All Categories\",\n      \"type\": \"boolean\",\n      \"default\": false,\n      \"prefill\": false,\n      \"description\": \"When true, an empty or All-only selection expands to all 21 verified Explore categories. If specific categories are selected, the missing categories are appended.\"\n    },\n    \"countries\": {\n      \"title\": \"Countries / Markets\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\",\n        \"pattern\": \"^[A-Za-z]{2}$\"\n      },\n      \"maxItems\": 10,\n      \"default\": [],\n      \"prefill\": [],\n      \"editor\": \"stringList\",\n      \"description\": \"Optional ISO 3166-1 alpha-2 markets. Empty means the actor's current egress and no proxy. Each selected country uses a per-country Apify proxy (AUTO first, residential only after a proven block). The feed follows the egress IP, not a query parameter.\"\n    },\n    \"country\": {\n      \"title\": \"Country (shorthand)\",\n      \"type\": \"string\",\n      \"pattern\": \"^([A-Za-z]{2})?$\",\n      \"default\": \"\",\n      \"editor\": \"textfield\",\n      \"description\": \"Single-country shorthand. Ignored when countries is non-empty.\"\n    },\n    \"postedWithin\": {\n      \"title\": \"Posted Within\",\n      \"type\": \"string\",\n      \"enum\": [\n        \"any\",\n        \"24h\",\n        \"7d\",\n        \"30d\",\n        \"90d\"\n      ],\n      \"default\": \"any\",\n      \"prefill\": \"any\",\n      \"description\": \"Client-side filter on createTime relative to the run start.\"\n    },\n    \"minViews\": {\n      \"title\": \"Minimum Views\",\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 1000000000000,\n      \"default\": 0,\n      \"prefill\": 0,\n      \"description\": \"Drop posts with fewer playCount views. Missing stats fail a positive threshold.\"\n    },\n    \"minLikes\": {\n      \"title\": \"Minimum Likes\",\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 1000000000000,\n      \"default\": 0,\n      \"prefill\": 0,\n      \"description\": \"Drop posts with fewer likes. Missing stats fail a positive threshold.\"\n    },\n    \"minComments\": {\n      \"title\": \"Minimum Comments\",\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 1000000000000,\n      \"default\": 0,\n      \"prefill\": 0,\n      \"description\": \"Drop posts with fewer comments. Missing stats fail a positive threshold.\"\n    },\n    \"minShares\": {\n      \"title\": \"Minimum Shares\",\n      \"type\": \"integer\",\n      \"minimum\": 0,\n      \"maximum\": 1000000000000,\n      \"default\": 0,\n      \"prefill\": 0,\n      \"description\": \"Drop posts with fewer shares. Missing stats fail a positive threshold.\"\n    },\n    \"onlyNew\": {\n      \"title\": \"Only New Posts\",\n      \"type\": \"boolean\",\n      \"default\": false,\n      \"prefill\": false,\n      \"description\": \"Emit only posts that were not present in the named state store. State is read and written only when this is true.\"\n    },\n    \"stateKey\": {\n      \"title\": \"Only-New State Key\",\n      \"type\": \"string\",\n      \"maxLength\": 200,\n      \"default\": \"\",\n      \"editor\": \"textfield\",\n      \"description\": \"Optional key-value store key for onlyNew. When empty, a deterministic key is derived from categories, countries and filters.\"\n    },\n    \"includeTranscripts\": {\n      \"title\": \"Include Transcript Text\",\n      \"type\": \"boolean\",\n      \"default\": false,\n      \"prefill\": false,\n      \"description\": \"Fetch subtitle WebVTT URLs and add subtitles[].text. Subtitle metadata is always returned when available; text is opt-in and fail-soft.\"\n    },\n    \"includePostDetails\": {\n      \"title\": \"Include Post Details\",\n      \"type\": \"boolean\",\n      \"default\": false,\n      \"prefill\": false,\n      \"description\": \"Fetch the public post detail page for each selected post (bounded to the emitted maxItems) and merge locationCreated plus poi/imagePost when the feed omitted them. Detail requests are fail-soft: a 403, stub page or parse failure keeps the original post. Adds one page request per emitted row.\"\n    },\n    \"rehostVideo\": {\n      \"title\": \"Rehost Media in Key-Value Store\",\n      \"type\": \"boolean\",\n      \"default\": true,\n      \"prefill\": false,\n      \"description\": \"Open each emitted post with Playwright to capture the real video/mp4 response, then HTTP-fetch the signed cover, originCover, dynamicCover, zoomCover, avatar, music, slideshow image and subtitle files. Every captured payload is stored in the run's default key-value store and the original signed URL is kept as a <field>Original value plus in mediaSources. video.playUrl and video.downloadUrl use tiktok-video-{postId}.mp4; other media use typed tiktok-{kind}-{postId}-... keys. Rehosting is fail-soft per field and never drops a post. It adds one browser page plus several media fetches per emitted record and increases run time and KV storage use; keep maxItemsPerCategory small.\"\n    }\n  },\n  \"required\": [\n    \"categories\"\n  ]\n}",
    "readme": "# TikTok Explore Feed Trending Post Scraper\n\nCollect the public TikTok Explore feed by category with no login, cookies or API keys. Choose All or any of the 20 topic feeds, target one or more country markets, filter by post age and engagement, and get one row per post with author stats, music, hashtags, mentions, subtitles, POI data and derived performance metrics. Signed TikTok media URLs can be copied to stable key-value store URLs in the same run, covering video, covers, avatars, music, slideshow images and subtitle files.\n\n## What this actor does\n\n- **21 Explore categories:** All (`120`) plus the 20 topic feeds; accepts category names, numeric IDs and legacy `{id,name}` objects.\n- **Market and filter controls:** optional ISO country markets, post age, and minimum views, likes, comments and shares.\n- **Rich post records:** rank, engagement rates, author stats, music, hashtags, mentions, POI, ad/AI flags and subtitle metadata.\n- **Stable media:** with `rehostVideo`, TikTok video, covers, avatars, music, slideshow images and subtitle files are stored in the run's key-value store and emitted as durable public URLs.\n- **Empty fields are omitted** — records contain no `null`, empty string, empty list or empty object.\n\n## Output per post\n\nOne row per Explore post. `feedType` is always `explore` and `recordType` is `explorePost`. Empty values are removed, so a field that is not listed is absent for that post rather than `null`.\n\n### Identity and position\n\n| Field | Description |\n|---|---|\n| `postId` | TikTok post/video ID (string) |\n| `postType` | `video` or `slideshow` |\n| `postUrl`, `sourceUrl` | Public post URL; `sourceUrl` mirrors `postUrl` |\n| `feedType` | Always `explore` |\n| `categoryId`, `categoryName` | Requested Explore category; All is `120` |\n| `itemCategoryId`, `itemCategoryName` | The post's own TikTok category, which can differ from the requested category |\n| `rank`, `position` | 1-based rank and 0-based position within the market/category after filters and dedupe |\n| `marketRegion` | Selected ISO country, or the region read from Explore page state when no country was selected; omitted rather than guessed |\n\n### Content, engagement and flags\n\n| Field | Description |\n|---|---|\n| `caption`, `textLanguage` | Post caption and detected caption language |\n| `createTime`, `createdAt` | Original Unix timestamp and ISO 8601 UTC creation time |\n| `locationCreated` | Emitted only when post-detail enrichment supplies it; the Explore feed payload does not contain it |\n| `hashtags[]` | `name`, `id` and `isCommerce` |\n| `mentions[]` | `username`, `start`, `end` and `awemeId` |\n| `playCount`, `likeCount`, `commentCount`, `shareCount`, `collectCount`, `repostCount` | Raw TikTok counters |\n| `engagementRate` | `(likes + comments + shares + collects) / plays`, 6 decimals; omitted when plays is zero |\n| `ageHours` | Hours between post creation and run start, 2 decimals |\n| `playsPerHour` | `playCount / ageHours`, 1 decimal; omitted when age is not positive |\n| `stitchEnabled`, `duetEnabled`, `shareEnabled` | Post interaction settings |\n| `isAd` | Whether the post is marked as an ad |\n| `adFlags` | `adAuthorization`, `brandOrganicType`, `adLabelVersion`, `aigcLabelType`, `moderationAigcLabelType` and `isAiGenerated` |\n\n### Author, music, video, place and subtitles\n\n| Field | Description |\n|---|---|\n| `author` | Profile snapshot: `id`, `secUid`, `username`, `displayName`, `bio`, `verified`, `privateAccount`, `roomId`, `avatarUrl` (plus `avatarUrlOriginal` after rehosting), `followerCount`, `followingCount`, `heartCount`, `videoCount`, and `commentSetting`, `duetSetting`, `stitchSetting`, `downloadSetting`, `isEmbedBanned` when present |\n| `music` | `id`, `title`, `authorName`, `original`, `duration`, `album`, `playUrl` (plus `playUrlOriginal`), `coverUrl` (plus `coverUrlOriginal`) |\n| `video` | Metadata `width`, `height`, `duration`, `ratio`, `definition`, `bitrate`; URLs `playUrl`, `downloadUrl`, `sourcePlayUrl`, `sourceDownloadUrl`; images `cover`, `originCover`, `dynamicCover` and resolution-keyed `zoomCover`; after rehosting, the signed sources are kept in `playUrlOriginal`, `downloadUrlOriginal`, `coverOriginal`, `originCoverOriginal`, `dynamicCoverOriginal` and `zoomCoverOriginal` |\n| `place` | POI data: `id`, `name`, `address`, `city`, `province`, `country`, `countryCode`, `category` and `typeName` |\n| `images[]` | Slideshow images: `ordinal`, `width`, `height`, `urlList`, and `urlListOriginal` after rehosting |\n| `subtitles[]` | `languageCode`, `format`, `url` (plus `urlOriginal` after rehosting), `expiresAt`, `sizeBytes`, `source`, `version`, and `text` when `includeTranscripts=true` |\n| `mediaExpiresAt` | Per-asset source expiry: `video`, `cover`, `avatar`, `music`, `subtitles` |\n| `mediaUrlExpiresAt` | Earliest of the media expiries |\n\n### Media rehosting status\n\n| Field | Description |\n|---|---|\n| `mediaRehosted` | `true` when the video was stored, or for a post without video when at least one other media field was stored |\n| `mediaRehostedAll` | `true` when every attempted media field was stored |\n| `rehostStatus` | Record outcome: `ok`, `partial`, or a fail-soft reason such as `http_403`, `no_video_response`, `not_mp4`, `not_vtt`, `network_error` or `kv_error` |\n| `rehostStatusByField` | Per-field status keyed by output path, for example `video.cover`, `music.playUrl`, `author.avatarUrl`, `images[0].urlList` and `subtitles[0].url` |\n| `mediaRehostedFields` | Boolean per-field success map derived from `rehostStatusByField` |\n| `mediaSources` | Original signed URL or URL list for every rehosted field |\n\n### Monitoring and provenance\n\n| Field | Description |\n|---|---|\n| `isNew`, `firstSeen`, `lastSeen` | Present only when `onlyNew=true`: whether the post is new in this run, plus its state-store timestamps |\n| `scrapedAt` | ISO 8601 UTC run-start time |\n| `recordType` | Always `explorePost` |\n\n## Input\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `categories` | array | `[\"All\"]` | Required. Names, numeric IDs or legacy `{id,name}` objects. Prefill is `[\"All\"]`. |\n| `maxItemsPerCategory` | integer 1–1,000 | `3` | Hard cap per category, checked before a page is emitted. Prefill is `3`. |\n| `maxItems` | integer 1–1,000 | – | Legacy alias used only when `maxItemsPerCategory` is absent or exactly `3`; an explicit non-default `maxItemsPerCategory` wins. |\n| `categoryId` | integer 1–100,000 | – | Legacy single-category shorthand. It replaces `categories`; `123` maps to `120`. |\n| `autoDiscoverCategories` | boolean | `false` | Expand an empty or All-only selection to all 21 categories, or append missing categories to a partial selection. |\n| `countries` | array of ISO 3166-1 alpha-2 | `[]` | Up to 10 markets. Empty means the current connection and no proxy. Each selected market needs its own egress. |\n| `country` | string | `\"\"` | Single-country shorthand; ignored when `countries` is non-empty. |\n| `postedWithin` | string | `any` | `any`, `24h`, `7d`, `30d` or `90d`, applied to `createTime`. |\n| `minViews` | integer | `0` | Drop posts below this play count. |\n| `minLikes` | integer | `0` | Drop posts below this like count. |\n| `minComments` | integer | `0` | Drop posts below this comment count. |\n| `minShares` | integer | `0` | Drop posts below this share count. |\n| `onlyNew` | boolean | `false` | Emit only post IDs that are not already in the state store. |\n| `stateKey` | string | derived | Key inside the `tiktok-explore-only-new` state store; blank derives one from your categories, markets and filters. |\n| `includeTranscripts` | boolean | `false` | Fetch subtitle WebVTT files and add `subtitles[].text`. Subtitle metadata is returned even when this is off. |\n| `includePostDetails` | boolean | `false` | Fetch one public post-detail page per emitted post to add `locationCreated` and missing POI/slideshow data. Fail-soft. |\n| `rehostVideo` | boolean | `true` | Copy every signed media field into the key-value store and emit stable public URLs. Default `true`; the form prefills `false` for a lighter first run. |\n\nCategory IDs: 100 Anime & Comics, 101 Shows, 102 Beauty Care, 103 Games, 104 Comedy, 105 Daily Life, 106 Family, 107 Relationship, 108 Drama, 109 Outfit, 110 Lipsync, 111 Food, 112 Sports, 113 Animals, 114 Society, 115 Cars, 116 Education, 117 Fitness & Health, 118 Technology, 119 Singing & Dancing, 120 All. `121` and `122` are empty upstream and `124+` are invalid upstream, so they are skipped with a warning. No TikTok account, cookies, API keys or user proxy credentials are needed.\n\n### Example: default metadata run\n\n```json\n{\n  \"categories\": [\"All\"],\n  \"maxItemsPerCategory\": 3,\n  \"rehostVideo\": false\n}\n```\n\n### Example: rehost every media field\n\n```json\n{\n  \"categories\": [\"All\"],\n  \"maxItemsPerCategory\": 10,\n  \"rehostVideo\": true\n}\n```\n\n### Example: filtered multi-market run\n\n```json\n{\n  \"categories\": [\"All\", \"Sports\"],\n  \"countries\": [\"PK\", \"TH\"],\n  \"maxItemsPerCategory\": 100,\n  \"postedWithin\": \"7d\",\n  \"minViews\": 10000,\n  \"minLikes\": 500,\n  \"rehostVideo\": false\n}\n```\n\n### Example: only-new monitoring\n\n```json\n{\n  \"categories\": [\"Technology\"],\n  \"maxItemsPerCategory\": 100,\n  \"onlyNew\": true,\n  \"stateKey\": \"tech-daily\",\n  \"rehostVideo\": false\n}\n```\n\n## Use cases\n\n- **Social media marketing** — see which formats, sounds and captions are trending across Explore categories and markets.\n- **Creator and agency research** — benchmark author follower counts, engagement rates and post performance against category peers.\n- **Content strategy** — compare hooks, hashtags and mentions across markets before planning a campaign.\n- **Brand and ad monitoring** — filter ad-flagged posts and AI/AIGC labels within a category or country.\n- **Dataset building** — export metadata-rich rows for recommendation models, trend dashboards and research datasets.\n- **Localization research** — collect the same category through different country egresses to compare regional feeds.\n\n## FAQ\n\n### Do I need a TikTok login, cookies, or an API key?\n\nNo. The actor reads publicly available Explore data and needs no account, cookies, tokens or user proxy credentials.\n\n### How many posts can I get per category?\n\nUp to 1,000 per category. TikTok returns at most 30 items per request, and Explore has no cursor, so the actor repeats requests and removes duplicates until it reaches your cap or the feed stops producing new posts. A category can run short when TikTok rotates the feed; the run status message reports the actual count and stop reason.\n\n### What is the difference between `categoryId` and `itemCategoryId`?\n\n`categoryId` is the Explore feed you requested. `itemCategoryId` is the post's own TikTok category. Explore feeds are mixed, so a Sports request can contain posts classified as Comedy or Society. All is `120`; `123` is accepted as a legacy alias for All, while `121` and `122` are empty upstream and `124+` are invalid.\n\n### Why can the same post appear more than once, and is rank stable?\n\nEach selected country is a separate collection. Dedupe is per market and post ID, so a post appears once per market but can appear once for each requested country. `rank` and `position` are run-local emission order after filters and dedupe, not a permanent TikTok ranking; the Explore feed changes between calls, so ranks can differ in a later run.\n\n### How do country and market targets work?\n\nThe TikTok feed follows the network egress, not a query parameter. With `countries` empty the actor uses the current connection and no proxy. Each selected country uses a per-country Apify proxy, starting with the automatic pool and escalating to residential only after a proven block. If a country is unavailable, it is skipped with a status message instead of silently using another market.\n\n### Do TikTok media URLs expire?\n\nYes. Signed video, cover and avatar URLs usually expire in about 48 hours; music is often about 24 hours, and the fallback play carrier can be shorter. `mediaExpiresAt` exposes per-asset expiry and `mediaUrlExpiresAt` gives the earliest. With `rehostVideo=true`, the emitted media URLs point at the key-value store instead.\n\n### What does media rehosting store?\n\nFor every emitted post it stores the video plus cover, origin cover, dynamic cover, all zoom-cover sizes, author avatar, music audio and cover, slideshow images, and subtitle VTT files in the run's key-value store. The main field becomes a stable public key-value store URL, while the signed source stays in `<field>Original` and in `mediaSources`. Rehosting is fail-soft per field: a failed cover or subtitle keeps the original URL and never drops the post, and the status fields tell you exactly what succeeded. Rehosted files last for the lifetime of the store record, not forever; the URLs are public, need no Apify token, and store retention settings still apply. Rehosting adds one post-page load and several media downloads per record plus storage, so keep `maxItemsPerCategory` small or set `rehostVideo=false` for metadata-only runs.\n\n### Why are some fields missing?\n\nEmpty values are omitted, so a record contains only fields TikTok returned. `locationCreated` is not in the Explore feed payload and appears only with `includePostDetails=true` when the post detail page supplies it. Explore rarely exposes slideshows, so `images` is usually absent; detail enrichment can recover it for a known photo post. Subtitle metadata is source-dependent.\n\n### What does `onlyNew` do?\n\nIt keeps a named set of post IDs in the `tiktok-explore-only-new` state store and emits only IDs that were not seen before. `stateKey` names the set; leave it blank to derive one from your categories, markets and filters. State is pruned to 90 days and capped at 200,000 IDs.\n\n### Is this affiliated with TikTok?\n\nNo. This is an independent third-party actor that reads publicly available Explore data. It is not affiliated with or endorsed by TikTok.\n",
    "actorDefinition": {
      "actorSpecification": 1,
      "name": "tiktok-explore-feed-trending-post-scraper",
      "title": "TikTok Explore Feed Trending Post Scraper",
      "description": "Collect TikTok Explore posts by category without cookies: HTTP-first polling, 21 categories, country egress, filters, transcripts, post-detail enrichment and stable Key-Value Store media rehosting.",
      "version": "1.2",
      "buildTag": "latest",
      "readme": "# TikTok Explore Feed Trending Post Scraper\n\nCollect the public TikTok Explore feed by category with no login, cookies or API keys. Choose All or any of the 20 topic feeds, target one or more country markets, filter by post age and engagement, and get one row per post with author stats, music, hashtags, mentions, subtitles, POI data and derived performance metrics. Signed TikTok media URLs can be copied to stable key-value store URLs in the same run, covering video, covers, avatars, music, slideshow images and subtitle files.\n\n## What this actor does\n\n- **21 Explore categories:** All (`120`) plus the 20 topic feeds; accepts category names, numeric IDs and legacy `{id,name}` objects.\n- **Market and filter controls:** optional ISO country markets, post age, and minimum views, likes, comments and shares.\n- **Rich post records:** rank, engagement rates, author stats, music, hashtags, mentions, POI, ad/AI flags and subtitle metadata.\n- **Stable media:** with `rehostVideo`, TikTok video, covers, avatars, music, slideshow images and subtitle files are stored in the run's key-value store and emitted as durable public URLs.\n- **Empty fields are omitted** — records contain no `null`, empty string, empty list or empty object.\n\n## Output per post\n\nOne row per Explore post. `feedType` is always `explore` and `recordType` is `explorePost`. Empty values are removed, so a field that is not listed is absent for that post rather than `null`.\n\n### Identity and position\n\n| Field | Description |\n|---|---|\n| `postId` | TikTok post/video ID (string) |\n| `postType` | `video` or `slideshow` |\n| `postUrl`, `sourceUrl` | Public post URL; `sourceUrl` mirrors `postUrl` |\n| `feedType` | Always `explore` |\n| `categoryId`, `categoryName` | Requested Explore category; All is `120` |\n| `itemCategoryId`, `itemCategoryName` | The post's own TikTok category, which can differ from the requested category |\n| `rank`, `position` | 1-based rank and 0-based position within the market/category after filters and dedupe |\n| `marketRegion` | Selected ISO country, or the region read from Explore page state when no country was selected; omitted rather than guessed |\n\n### Content, engagement and flags\n\n| Field | Description |\n|---|---|\n| `caption`, `textLanguage` | Post caption and detected caption language |\n| `createTime`, `createdAt` | Original Unix timestamp and ISO 8601 UTC creation time |\n| `locationCreated` | Emitted only when post-detail enrichment supplies it; the Explore feed payload does not contain it |\n| `hashtags[]` | `name`, `id` and `isCommerce` |\n| `mentions[]` | `username`, `start`, `end` and `awemeId` |\n| `playCount`, `likeCount`, `commentCount`, `shareCount`, `collectCount`, `repostCount` | Raw TikTok counters |\n| `engagementRate` | `(likes + comments + shares + collects) / plays`, 6 decimals; omitted when plays is zero |\n| `ageHours` | Hours between post creation and run start, 2 decimals |\n| `playsPerHour` | `playCount / ageHours`, 1 decimal; omitted when age is not positive |\n| `stitchEnabled`, `duetEnabled`, `shareEnabled` | Post interaction settings |\n| `isAd` | Whether the post is marked as an ad |\n| `adFlags` | `adAuthorization`, `brandOrganicType`, `adLabelVersion`, `aigcLabelType`, `moderationAigcLabelType` and `isAiGenerated` |\n\n### Author, music, video, place and subtitles\n\n| Field | Description |\n|---|---|\n| `author` | Profile snapshot: `id`, `secUid`, `username`, `displayName`, `bio`, `verified`, `privateAccount`, `roomId`, `avatarUrl` (plus `avatarUrlOriginal` after rehosting), `followerCount`, `followingCount`, `heartCount`, `videoCount`, and `commentSetting`, `duetSetting`, `stitchSetting`, `downloadSetting`, `isEmbedBanned` when present |\n| `music` | `id`, `title`, `authorName`, `original`, `duration`, `album`, `playUrl` (plus `playUrlOriginal`), `coverUrl` (plus `coverUrlOriginal`) |\n| `video` | Metadata `width`, `height`, `duration`, `ratio`, `definition`, `bitrate`; URLs `playUrl`, `downloadUrl`, `sourcePlayUrl`, `sourceDownloadUrl`; images `cover`, `originCover`, `dynamicCover` and resolution-keyed `zoomCover`; after rehosting, the signed sources are kept in `playUrlOriginal`, `downloadUrlOriginal`, `coverOriginal`, `originCoverOriginal`, `dynamicCoverOriginal` and `zoomCoverOriginal` |\n| `place` | POI data: `id`, `name`, `address`, `city`, `province`, `country`, `countryCode`, `category` and `typeName` |\n| `images[]` | Slideshow images: `ordinal`, `width`, `height`, `urlList`, and `urlListOriginal` after rehosting |\n| `subtitles[]` | `languageCode`, `format`, `url` (plus `urlOriginal` after rehosting), `expiresAt`, `sizeBytes`, `source`, `version`, and `text` when `includeTranscripts=true` |\n| `mediaExpiresAt` | Per-asset source expiry: `video`, `cover`, `avatar`, `music`, `subtitles` |\n| `mediaUrlExpiresAt` | Earliest of the media expiries |\n\n### Media rehosting status\n\n| Field | Description |\n|---|---|\n| `mediaRehosted` | `true` when the video was stored, or for a post without video when at least one other media field was stored |\n| `mediaRehostedAll` | `true` when every attempted media field was stored |\n| `rehostStatus` | Record outcome: `ok`, `partial`, or a fail-soft reason such as `http_403`, `no_video_response`, `not_mp4`, `not_vtt`, `network_error` or `kv_error` |\n| `rehostStatusByField` | Per-field status keyed by output path, for example `video.cover`, `music.playUrl`, `author.avatarUrl`, `images[0].urlList` and `subtitles[0].url` |\n| `mediaRehostedFields` | Boolean per-field success map derived from `rehostStatusByField` |\n| `mediaSources` | Original signed URL or URL list for every rehosted field |\n\n### Monitoring and provenance\n\n| Field | Description |\n|---|---|\n| `isNew`, `firstSeen`, `lastSeen` | Present only when `onlyNew=true`: whether the post is new in this run, plus its state-store timestamps |\n| `scrapedAt` | ISO 8601 UTC run-start time |\n| `recordType` | Always `explorePost` |\n\n## Input\n\n| Field | Type | Default | Description |\n|---|---|---|---|\n| `categories` | array | `[\"All\"]` | Required. Names, numeric IDs or legacy `{id,name}` objects. Prefill is `[\"All\"]`. |\n| `maxItemsPerCategory` | integer 1–1,000 | `3` | Hard cap per category, checked before a page is emitted. Prefill is `3`. |\n| `maxItems` | integer 1–1,000 | – | Legacy alias used only when `maxItemsPerCategory` is absent or exactly `3`; an explicit non-default `maxItemsPerCategory` wins. |\n| `categoryId` | integer 1–100,000 | – | Legacy single-category shorthand. It replaces `categories`; `123` maps to `120`. |\n| `autoDiscoverCategories` | boolean | `false` | Expand an empty or All-only selection to all 21 categories, or append missing categories to a partial selection. |\n| `countries` | array of ISO 3166-1 alpha-2 | `[]` | Up to 10 markets. Empty means the current connection and no proxy. Each selected market needs its own egress. |\n| `country` | string | `\"\"` | Single-country shorthand; ignored when `countries` is non-empty. |\n| `postedWithin` | string | `any` | `any`, `24h`, `7d`, `30d` or `90d`, applied to `createTime`. |\n| `minViews` | integer | `0` | Drop posts below this play count. |\n| `minLikes` | integer | `0` | Drop posts below this like count. |\n| `minComments` | integer | `0` | Drop posts below this comment count. |\n| `minShares` | integer | `0` | Drop posts below this share count. |\n| `onlyNew` | boolean | `false` | Emit only post IDs that are not already in the state store. |\n| `stateKey` | string | derived | Key inside the `tiktok-explore-only-new` state store; blank derives one from your categories, markets and filters. |\n| `includeTranscripts` | boolean | `false` | Fetch subtitle WebVTT files and add `subtitles[].text`. Subtitle metadata is returned even when this is off. |\n| `includePostDetails` | boolean | `false` | Fetch one public post-detail page per emitted post to add `locationCreated` and missing POI/slideshow data. Fail-soft. |\n| `rehostVideo` | boolean | `true` | Copy every signed media field into the key-value store and emit stable public URLs. Default `true`; the form prefills `false` for a lighter first run. |\n\nCategory IDs: 100 Anime & Comics, 101 Shows, 102 Beauty Care, 103 Games, 104 Comedy, 105 Daily Life, 106 Family, 107 Relationship, 108 Drama, 109 Outfit, 110 Lipsync, 111 Food, 112 Sports, 113 Animals, 114 Society, 115 Cars, 116 Education, 117 Fitness & Health, 118 Technology, 119 Singing & Dancing, 120 All. `121` and `122` are empty upstream and `124+` are invalid upstream, so they are skipped with a warning. No TikTok account, cookies, API keys or user proxy credentials are needed.\n\n### Example: default metadata run\n\n```json\n{\n  \"categories\": [\"All\"],\n  \"maxItemsPerCategory\": 3,\n  \"rehostVideo\": false\n}\n```\n\n### Example: rehost every media field\n\n```json\n{\n  \"categories\": [\"All\"],\n  \"maxItemsPerCategory\": 10,\n  \"rehostVideo\": true\n}\n```\n\n### Example: filtered multi-market run\n\n```json\n{\n  \"categories\": [\"All\", \"Sports\"],\n  \"countries\": [\"PK\", \"TH\"],\n  \"maxItemsPerCategory\": 100,\n  \"postedWithin\": \"7d\",\n  \"minViews\": 10000,\n  \"minLikes\": 500,\n  \"rehostVideo\": false\n}\n```\n\n### Example: only-new monitoring\n\n```json\n{\n  \"categories\": [\"Technology\"],\n  \"maxItemsPerCategory\": 100,\n  \"onlyNew\": true,\n  \"stateKey\": \"tech-daily\",\n  \"rehostVideo\": false\n}\n```\n\n## Use cases\n\n- **Social media marketing** — see which formats, sounds and captions are trending across Explore categories and markets.\n- **Creator and agency research** — benchmark author follower counts, engagement rates and post performance against category peers.\n- **Content strategy** — compare hooks, hashtags and mentions across markets before planning a campaign.\n- **Brand and ad monitoring** — filter ad-flagged posts and AI/AIGC labels within a category or country.\n- **Dataset building** — export metadata-rich rows for recommendation models, trend dashboards and research datasets.\n- **Localization research** — collect the same category through different country egresses to compare regional feeds.\n\n## FAQ\n\n### Do I need a TikTok login, cookies, or an API key?\n\nNo. The actor reads publicly available Explore data and needs no account, cookies, tokens or user proxy credentials.\n\n### How many posts can I get per category?\n\nUp to 1,000 per category. TikTok returns at most 30 items per request, and Explore has no cursor, so the actor repeats requests and removes duplicates until it reaches your cap or the feed stops producing new posts. A category can run short when TikTok rotates the feed; the run status message reports the actual count and stop reason.\n\n### What is the difference between `categoryId` and `itemCategoryId`?\n\n`categoryId` is the Explore feed you requested. `itemCategoryId` is the post's own TikTok category. Explore feeds are mixed, so a Sports request can contain posts classified as Comedy or Society. All is `120`; `123` is accepted as a legacy alias for All, while `121` and `122` are empty upstream and `124+` are invalid.\n\n### Why can the same post appear more than once, and is rank stable?\n\nEach selected country is a separate collection. Dedupe is per market and post ID, so a post appears once per market but can appear once for each requested country. `rank` and `position` are run-local emission order after filters and dedupe, not a permanent TikTok ranking; the Explore feed changes between calls, so ranks can differ in a later run.\n\n### How do country and market targets work?\n\nThe TikTok feed follows the network egress, not a query parameter. With `countries` empty the actor uses the current connection and no proxy. Each selected country uses a per-country Apify proxy, starting with the automatic pool and escalating to residential only after a proven block. If a country is unavailable, it is skipped with a status message instead of silently using another market.\n\n### Do TikTok media URLs expire?\n\nYes. Signed video, cover and avatar URLs usually expire in about 48 hours; music is often about 24 hours, and the fallback play carrier can be shorter. `mediaExpiresAt` exposes per-asset expiry and `mediaUrlExpiresAt` gives the earliest. With `rehostVideo=true`, the emitted media URLs point at the key-value store instead.\n\n### What does media rehosting store?\n\nFor every emitted post it stores the video plus cover, origin cover, dynamic cover, all zoom-cover sizes, author avatar, music audio and cover, slideshow images, and subtitle VTT files in the run's key-value store. The main field becomes a stable public key-value store URL, while the signed source stays in `<field>Original` and in `mediaSources`. Rehosting is fail-soft per field: a failed cover or subtitle keeps the original URL and never drops the post, and the status fields tell you exactly what succeeded. Rehosted files last for the lifetime of the store record, not forever; the URLs are public, need no Apify token, and store retention settings still apply. Rehosting adds one post-page load and several media downloads per record plus storage, so keep `maxItemsPerCategory` small or set `rehostVideo=false` for metadata-only runs.\n\n### Why are some fields missing?\n\nEmpty values are omitted, so a record contains only fields TikTok returned. `locationCreated` is not in the Explore feed payload and appears only with `includePostDetails=true` when the post detail page supplies it. Explore rarely exposes slideshows, so `images` is usually absent; detail enrichment can recover it for a known photo post. Subtitle metadata is source-dependent.\n\n### What does `onlyNew` do?\n\nIt keeps a named set of post IDs in the `tiktok-explore-only-new` state store and emits only IDs that were not seen before. `stateKey` names the set; leave it blank to derive one from your categories, markets and filters. State is pruned to 90 days and capped at 200,000 IDs.\n\n### Is this affiliated with TikTok?\n\nNo. This is an independent third-party actor that reads publicly available Explore data. It is not affiliated with or endorsed by TikTok.\n",
      "input": {
        "title": "TikTok Explore/Trending Scraper Input",
        "description": "Collect the public TikTok Explore feed with no login or cookies. All=120 is the default; 123 is accepted as a legacy alias. Optional country selection, filters, only-new monitoring, transcript text, post-detail enrichment and Key-Value Store media rehosting are additive.",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "categories": {
            "title": "Categories",
            "type": "array",
            "default": [
              "All"
            ],
            "prefill": [
              "All"
            ],
            "editor": "json",
            "description": "Categories to collect. Accepts names, numeric IDs, or legacy {id,name} objects. All=120; topics 100-119: Anime & Comics, Shows, Beauty Care, Games, Comedy, Daily Life, Family, Relationship, Drama, Outfit, Lipsync, Food, Sports, Animals, Society, Cars, Education, Fitness & Health, Technology, Singing & Dancing. 123 is accepted as a legacy alias for All. 121/122 (empty upstream) and 124+ (invalid upstream) are skipped with a warning."
          },
          "maxItemsPerCategory": {
            "title": "Max Items Per Category",
            "type": "integer",
            "default": 3,
            "minimum": 1,
            "maximum": 1000,
            "description": "Hard cap on emitted posts per category. The cap is applied before any page is pushed, so a 30-item page never overshoots. The default is 3 because media rehosting is on by default and adds one browser page plus several HTTP media fetches per emitted post.",
            "prefill": 3
          },
          "maxItems": {
            "title": "Max Items (legacy alias)",
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "description": "Legacy alias for maxItemsPerCategory. It is used when maxItemsPerCategory is absent or exactly the default 3; an explicitly non-default maxItemsPerCategory wins."
          },
          "categoryId": {
            "title": "Category ID (legacy shorthand)",
            "type": "integer",
            "minimum": 1,
            "maximum": 100000,
            "description": "Legacy shorthand for one category. It replaces the categories list. 100-120 are accepted; 123 maps to 120; other values are skipped with a warning."
          },
          "autoDiscoverCategories": {
            "title": "Auto-Discover All Categories",
            "type": "boolean",
            "default": false,
            "prefill": false,
            "description": "When true, an empty or All-only selection expands to all 21 verified Explore categories. If specific categories are selected, the missing categories are appended."
          },
          "countries": {
            "title": "Countries / Markets",
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-Za-z]{2}$"
            },
            "maxItems": 10,
            "default": [],
            "prefill": [],
            "editor": "stringList",
            "description": "Optional ISO 3166-1 alpha-2 markets. Empty means the actor's current egress and no proxy. Each selected country uses a per-country Apify proxy (AUTO first, residential only after a proven block). The feed follows the egress IP, not a query parameter."
          },
          "country": {
            "title": "Country (shorthand)",
            "type": "string",
            "pattern": "^([A-Za-z]{2})?$",
            "default": "",
            "editor": "textfield",
            "description": "Single-country shorthand. Ignored when countries is non-empty."
          },
          "postedWithin": {
            "title": "Posted Within",
            "type": "string",
            "enum": [
              "any",
              "24h",
              "7d",
              "30d",
              "90d"
            ],
            "default": "any",
            "prefill": "any",
            "description": "Client-side filter on createTime relative to the run start."
          },
          "minViews": {
            "title": "Minimum Views",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000,
            "default": 0,
            "prefill": 0,
            "description": "Drop posts with fewer playCount views. Missing stats fail a positive threshold."
          },
          "minLikes": {
            "title": "Minimum Likes",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000,
            "default": 0,
            "prefill": 0,
            "description": "Drop posts with fewer likes. Missing stats fail a positive threshold."
          },
          "minComments": {
            "title": "Minimum Comments",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000,
            "default": 0,
            "prefill": 0,
            "description": "Drop posts with fewer comments. Missing stats fail a positive threshold."
          },
          "minShares": {
            "title": "Minimum Shares",
            "type": "integer",
            "minimum": 0,
            "maximum": 1000000000000,
            "default": 0,
            "prefill": 0,
            "description": "Drop posts with fewer shares. Missing stats fail a positive threshold."
          },
          "onlyNew": {
            "title": "Only New Posts",
            "type": "boolean",
            "default": false,
            "prefill": false,
            "description": "Emit only posts that were not present in the named state store. State is read and written only when this is true."
          },
          "stateKey": {
            "title": "Only-New State Key",
            "type": "string",
            "maxLength": 200,
            "default": "",
            "editor": "textfield",
            "description": "Optional key-value store key for onlyNew. When empty, a deterministic key is derived from categories, countries and filters."
          },
          "includeTranscripts": {
            "title": "Include Transcript Text",
            "type": "boolean",
            "default": false,
            "prefill": false,
            "description": "Fetch subtitle WebVTT URLs and add subtitles[].text. Subtitle metadata is always returned when available; text is opt-in and fail-soft."
          },
          "includePostDetails": {
            "title": "Include Post Details",
            "type": "boolean",
            "default": false,
            "prefill": false,
            "description": "Fetch the public post detail page for each selected post (bounded to the emitted maxItems) and merge locationCreated plus poi/imagePost when the feed omitted them. Detail requests are fail-soft: a 403, stub page or parse failure keeps the original post. Adds one page request per emitted row."
          },
          "rehostVideo": {
            "title": "Rehost Media in Key-Value Store",
            "type": "boolean",
            "default": true,
            "prefill": false,
            "description": "Open each emitted post with Playwright to capture the real video/mp4 response, then HTTP-fetch the signed cover, originCover, dynamicCover, zoomCover, avatar, music, slideshow image and subtitle files. Every captured payload is stored in the run's default key-value store and the original signed URL is kept as a <field>Original value plus in mediaSources. video.playUrl and video.downloadUrl use tiktok-video-{postId}.mp4; other media use typed tiktok-{kind}-{postId}-... keys. Rehosting is fail-soft per field and never drops a post. It adds one browser page plus several media fetches per emitted record and increases run time and KV storage use; keep maxItemsPerCategory small."
          }
        },
        "required": [
          "categories"
        ]
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "TikTok Explore Feed Trending Post Scraper Output",
        "properties": {
          "data": {
            "type": "string",
            "title": "Trending Post Data",
            "description": "Dataset of Explore posts. Each row preserves the legacy feedType/categoryId/categoryName identity and adds rank, position, marketRegion, itemCategoryId/itemCategoryName, derived engagement metrics, place, adFlags, subtitles, mediaExpiresAt, provenance fields and optional KVS media rehosting fields (mediaRehosted, mediaRehostedAll, rehostStatus, rehostStatusByField, mediaRehostedFields, mediaSources, and per-field <field>Original values).",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
          }
        }
      },
      "dockerfile": "./Dockerfile",
      "categories": [
        "SOCIAL_MEDIA"
      ],
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "title": "TikTok Explore/Trending Scraper Dataset",
          "views": {
            "overview": {
              "title": "Overview",
              "transformation": {
                "fields": [
                  "rank",
                  "marketRegion",
                  "categoryName",
                  "categoryId",
                  "itemCategoryName",
                  "feedType",
                  "postId",
                  "caption",
                  "playCount",
                  "likeCount",
                  "engagementRate",
                  "ageHours",
                  "playsPerHour",
                  "createdAt",
                  "postUrl",
                  "mediaRehosted",
                  "mediaRehostedAll",
                  "rehostStatus"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "rank": {
                    "label": "Rank",
                    "format": "number"
                  },
                  "marketRegion": {
                    "label": "Market",
                    "format": "text"
                  },
                  "categoryName": {
                    "label": "Category",
                    "format": "text"
                  },
                  "categoryId": {
                    "label": "Category ID",
                    "format": "number"
                  },
                  "itemCategoryName": {
                    "label": "Item Category",
                    "format": "text"
                  },
                  "feedType": {
                    "label": "Feed Type",
                    "format": "text"
                  },
                  "postId": {
                    "label": "Post ID",
                    "format": "text"
                  },
                  "caption": {
                    "label": "Caption",
                    "format": "text"
                  },
                  "playCount": {
                    "label": "Plays",
                    "format": "number"
                  },
                  "likeCount": {
                    "label": "Likes",
                    "format": "number"
                  },
                  "engagementRate": {
                    "label": "Engagement Rate",
                    "format": "number"
                  },
                  "ageHours": {
                    "label": "Age (hours)",
                    "format": "number"
                  },
                  "playsPerHour": {
                    "label": "Plays / Hour",
                    "format": "number"
                  },
                  "createdAt": {
                    "label": "Created At",
                    "format": "text"
                  },
                  "postUrl": {
                    "label": "Post URL",
                    "format": "link"
                  },
                  "mediaRehosted": {
                    "label": "Media Rehosted",
                    "format": "text"
                  },
                  "mediaRehostedAll": {
                    "label": "All Media Rehosted",
                    "format": "text"
                  },
                  "rehostStatus": {
                    "label": "Rehost Status",
                    "format": "text"
                  }
                }
              }
            }
          },
          "fields": {
            "feedType": {
              "type": "string",
              "title": "Feed Type",
              "description": "Always 'explore' for rows from this actor"
            },
            "categoryId": {
              "type": "integer",
              "title": "Category ID",
              "description": "Requested category ID. All is 120; legacy 123 inputs are normalized to 120."
            },
            "categoryName": {
              "type": "string",
              "title": "Category Name",
              "description": "Canonical requested category name (e.g. All, Sports, Technology)"
            },
            "itemCategoryId": {
              "type": "integer",
              "title": "Item Category ID",
              "description": "The item's own CategoryType. Explore feeds are mixed, so this can differ from categoryId."
            },
            "itemCategoryName": {
              "type": "string",
              "title": "Item Category Name",
              "description": "Canonical name for itemCategoryId"
            },
            "rank": {
              "type": "integer",
              "title": "Rank",
              "description": "1-based emitted order within the market/category after filters and dedupe"
            },
            "position": {
              "type": "integer",
              "title": "Position",
              "description": "0-based emitted order within the market/category after filters and dedupe"
            },
            "marketRegion": {
              "type": "string",
              "title": "Market Region",
              "description": "Selected ISO country, or the actual region detected from the Explore page state when no country was requested. Never guessed."
            },
            "postId": {
              "type": "string",
              "title": "Post ID",
              "description": "Unique TikTok post/video ID (string)"
            },
            "postType": {
              "type": "string",
              "title": "Post Type",
              "description": "Either 'video' or 'slideshow'"
            },
            "postUrl": {
              "type": "string",
              "title": "Post URL",
              "description": "Direct URL to the TikTok post"
            },
            "sourceUrl": {
              "type": "string",
              "title": "Source URL",
              "description": "Provenance alias for postUrl"
            },
            "caption": {
              "type": "string",
              "title": "Caption",
              "description": "Post caption/description text"
            },
            "textLanguage": {
              "type": "string",
              "title": "Text Language",
              "description": "Language code of the caption"
            },
            "createTime": {
              "type": "integer",
              "title": "Create Time",
              "description": "Unix timestamp when the post was created"
            },
            "createdAt": {
              "type": "string",
              "title": "Created At",
              "description": "ISO 8601 UTC timestamp of post creation"
            },
            "scrapedAt": {
              "type": "string",
              "title": "Scraped At",
              "description": "ISO 8601 UTC timestamp of the run start"
            },
            "recordType": {
              "type": "string",
              "title": "Record Type",
              "description": "Always 'explorePost'"
            },
            "playCount": {
              "type": "integer",
              "title": "Play Count",
              "description": "Number of video plays"
            },
            "likeCount": {
              "type": "integer",
              "title": "Like Count",
              "description": "Number of likes (from statsV2)"
            },
            "commentCount": {
              "type": "integer",
              "title": "Comment Count",
              "description": "Number of comments"
            },
            "shareCount": {
              "type": "integer",
              "title": "Share Count",
              "description": "Number of shares"
            },
            "collectCount": {
              "type": "integer",
              "title": "Collect Count",
              "description": "Number of times post was saved/collected"
            },
            "repostCount": {
              "type": "integer",
              "title": "Repost Count",
              "description": "Number of reposts"
            },
            "engagementRate": {
              "type": "number",
              "title": "Engagement Rate",
              "description": "(likes + comments + shares + collects) / plays, 6 decimals. Omitted when playCount is zero or missing."
            },
            "ageHours": {
              "type": "number",
              "title": "Age Hours",
              "description": "(run start - createTime) / 3600, 2 decimals"
            },
            "playsPerHour": {
              "type": "number",
              "title": "Plays Per Hour",
              "description": "playCount / ageHours, 1 decimal. Omitted when ageHours is not positive."
            },
            "stitchEnabled": {
              "type": "boolean",
              "title": "Stitch Enabled",
              "description": "Whether stitching is allowed on this post"
            },
            "duetEnabled": {
              "type": "boolean",
              "title": "Duet Enabled",
              "description": "Whether duets are allowed on this post"
            },
            "shareEnabled": {
              "type": "boolean",
              "title": "Share Enabled",
              "description": "Whether sharing is enabled on this post"
            },
            "isAd": {
              "type": "boolean",
              "title": "Is Ad",
              "description": "Whether the post is marked as an advertisement"
            },
            "adFlags": {
              "type": "object",
              "title": "Ad / AI Flags",
              "description": "adAuthorization, brandOrganicType, adLabelVersion, aigcLabelType, moderationAigcLabelType and isAiGenerated; empty subkeys are omitted"
            },
            "author": {
              "type": "object",
              "title": "Author",
              "description": "Author profile snapshot; includes commentSetting, duetSetting, stitchSetting, downloadSetting and isEmbedBanned when present. avatarUrl uses a KV URL after rehosting and avatarUrlOriginal keeps the signed source."
            },
            "music": {
              "type": "object",
              "title": "Music",
              "description": "Sound/music used in the post (id, title, authorName, duration, album, playUrl, coverUrl). After rehosting, playUrl and coverUrl use KV URLs and playUrlOriginal/coverUrlOriginal keep the signed sources."
            },
            "video": {
              "type": "object",
              "title": "Video",
              "description": "Video metadata. With rehostVideo=true, playUrl, downloadUrl, cover, originCover, dynamicCover and zoomCover are stable Key-Value Store URLs, each with a matching <field>Original value and a mediaSources entry. sourcePlayUrl keeps the original signed play URL, and sourceDownloadUrl keeps a distinct signed download URL. Without rehosting, playUrl prefers the working www.tiktok.com/aweme/v1/play/ carrier and downloadUrl falls back to it."
            },
            "mediaRehosted": {
              "type": "boolean",
              "title": "Media Rehosted",
              "description": "True when the video bytes were stored in the default Key-Value Store, or, for a post without video, when at least one other signed media field was stored. False when no media field succeeded; the original signed URLs remain."
            },
            "mediaRehostedAll": {
              "type": "boolean",
              "title": "All Media Rehosted",
              "description": "True when every attempted media field was stored in the Key-Value Store. False when any field failed or was skipped."
            },
            "rehostStatus": {
              "type": "string",
              "title": "Rehost Status",
              "description": "Record-level outcome: ok, partial, no_media_fields, no_post_url, missing_post_id, no_video_response, navigation_failed, playwright_unavailable, browser_unavailable, body_unavailable, empty_body, invalid_content_type, not_mp4, not_vtt, http_<status>, network_error, capture_error, kv_error, or kv_url_error. Failures keep the original URLs and never drop the record."
            },
            "rehostStatusByField": {
              "type": "object",
              "title": "Rehost Status By Field",
              "description": "Per-field status keyed by output path, for example video.playUrl, video.cover, video.zoomCover.240, music.playUrl, author.avatarUrl, images[0].urlList and subtitles[0].url. Values are ok or a fail-soft status."
            },
            "mediaRehostedFields": {
              "type": "object",
              "title": "Media Rehosted Fields",
              "description": "Boolean per-field success map derived from rehostStatusByField."
            },
            "mediaSources": {
              "type": "object",
              "title": "Media Sources",
              "description": "Original signed URL or URL list for every rehosted field. Rehosted fields also keep a matching <field>Original value."
            },
            "place": {
              "type": "object",
              "title": "Place / POI",
              "description": "POI object when present: id, name, address, city, province, country, countryCode, category, typeName. No latitude/longitude is invented."
            },
            "subtitles": {
              "type": "array",
              "title": "Subtitles",
              "description": "Subtitle metadata from subtitleInfos, or video.claInfo.captionInfos when subtitleInfos is absent. text is added only with includeTranscripts=true. With rehosting, url is a KV VTT URL and urlOriginal keeps the signed source."
            },
            "hashtags": {
              "type": "array",
              "title": "Hashtags",
              "description": "Hashtags used in the post"
            },
            "mentions": {
              "type": "array",
              "title": "Mentions",
              "description": "@mentions in the caption"
            },
            "images": {
              "type": "array",
              "title": "Images",
              "description": "Slideshow images for photo posts (postType='slideshow'). With rehosting, urlList becomes a single KV image URL and urlListOriginal keeps the signed candidate list."
            },
            "locationCreated": {
              "type": "string",
              "title": "Location Created (legacy)",
              "description": "Declared for backward compatibility. The Explore item payload does not contain it; it is emitted only when includePostDetails=true and the post detail page supplies a value."
            },
            "mediaUrlExpiresAt": {
              "type": "string",
              "title": "Media URL Expires At",
              "description": "Earliest ISO 8601 UTC expiry across the original signed video, cover, avatar, music and subtitle URLs. It remains the source expiry after rehosting; KV URLs do not use it."
            },
            "mediaExpiresAt": {
              "type": "object",
              "title": "Media Expiries",
              "description": "Per-asset ISO 8601 expiries: video, cover, avatar, music, subtitles"
            },
            "isNew": {
              "type": "boolean",
              "title": "Is New",
              "description": "Present only for onlyNew runs; true for posts emitted during this run"
            },
            "firstSeen": {
              "type": "string",
              "title": "First Seen",
              "description": "Present only for onlyNew runs: ISO timestamp when the post first entered the state store"
            },
            "lastSeen": {
              "type": "string",
              "title": "Last Seen",
              "description": "Present only for onlyNew runs: ISO timestamp for this observation"
            }
          }
        }
      }
    },
    "buildNumber": "1.2.1",
    "imageDigest": "afc6f74ab58b8bfcb4872dd9162889cd5f06a6a05cf3ea4c061594ba673763bc",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "1.2"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.042743333333333335
    }
  }
}