{
  "data": {
    "id": "dYQGCXbTDOGK2new4",
    "actId": "Tv5YxlFwQjqWpwlLR",
    "userId": "9Um4NEFR7z42j35AD",
    "startedAt": "2026-08-16T17:54:37.454Z",
    "finishedAt": "2026-08-16T17:55:35.512Z",
    "status": "SUCCEEDED",
    "meta": {
      "origin": "CLI"
    },
    "stats": {
      "durationMillis": 57957,
      "runTimeSecs": 57.957,
      "computeUnits": 0.06439666666666667,
      "imageSizeBytes": 522583040
    },
    "options": {
      "useCache": true,
      "betaPackages": false,
      "memoryMbytes": 4096,
      "diskMbytes": 8192
    },
    "exitCode": 0,
    "inputSchema": "{\n  \"title\": \"YouTube Search Scraper input\",\n  \"type\": \"object\",\n  \"schemaVersion\": 1,\n  \"properties\": {\n    \"queries\": {\n      \"title\": \"Search queries\",\n      \"type\": \"array\",\n      \"description\": \"YouTube keyword queries to search.\",\n      \"editor\": \"stringList\",\n      \"default\": [\n        \"ai tools for small business\"\n      ],\n      \"prefill\": [\n        \"ai tools for small business\"\n      ],\n      \"minItems\": 1,\n      \"items\": {\n        \"type\": \"string\",\n        \"minLength\": 1\n      }\n    },\n    \"maxResults\": {\n      \"title\": \"Max unique videos\",\n      \"type\": \"integer\",\n      \"description\": \"Maximum unique YouTube video/Short candidates to save across all queries.\",\n      \"editor\": \"number\",\n      \"default\": 25,\n      \"minimum\": 1,\n      \"maximum\": 1000\n    },\n    \"resultsPerQuery\": {\n      \"title\": \"Results per query\",\n      \"type\": \"integer\",\n      \"description\": \"Maximum unique video/Short candidates to save per query in sequential mode. Balanced mode rotates across queries until the global cap or source/page limits are reached.\",\n      \"editor\": \"number\",\n      \"default\": 25,\n      \"minimum\": 1,\n      \"maximum\": 250\n    },\n    \"maxPagesPerQuery\": {\n      \"title\": \"Max pages per query\",\n      \"type\": \"integer\",\n      \"description\": \"Maximum YouTube search continuation pages to attempt for each query.\",\n      \"editor\": \"number\",\n      \"default\": 1,\n      \"minimum\": 1,\n      \"maximum\": 20\n    },\n    \"searchDistribution\": {\n      \"title\": \"Search distribution\",\n      \"type\": \"string\",\n      \"description\": \"Balanced rotates fairly across queries until maxResults or source/page limits are reached. Sequential fills from earlier queries first.\",\n      \"editor\": \"select\",\n      \"default\": \"balanced\",\n      \"enum\": [\n        \"balanced\",\n        \"sequential\"\n      ],\n      \"enumTitles\": [\n        \"Balanced\",\n        \"Sequential\"\n      ]\n    },\n    \"includeShorts\": {\n      \"title\": \"Include Shorts\",\n      \"type\": \"boolean\",\n      \"description\": \"Include Shorts-style search cards when YouTube exposes them in public search payloads.\",\n      \"editor\": \"checkbox\",\n      \"default\": true\n    },\n    \"enableQueryExpansion\": {\n      \"title\": \"Use YouTube suggestions\",\n      \"type\": \"boolean\",\n      \"description\": \"Expand each input query with native YouTube autocomplete suggestions. Existing input queries always run first.\",\n      \"editor\": \"checkbox\",\n      \"default\": false\n    },\n    \"enrichWithVideoMetrics\": {\n      \"title\": \"Enrich with video metrics\",\n      \"type\": \"boolean\",\n      \"description\": \"Fetch each saved video/Short as a second stage to fill richer public metrics such as likes, comments, and channel subscriber status. Disable this for low-cost URL discovery before transcript replay.\",\n      \"editor\": \"checkbox\",\n      \"default\": true\n    },\n    \"enrichMissingChannelFields\": {\n      \"title\": \"Enrich missing channel fields\",\n      \"type\": \"boolean\",\n      \"description\": \"When video metrics enrichment is disabled, optionally fetch lightweight public video details for rows missing channel, duration, or publish-date fields.\",\n      \"editor\": \"checkbox\",\n      \"default\": true\n    },\n    \"maxChannelEnrichmentRequests\": {\n      \"title\": \"Max channel enrichment requests\",\n      \"type\": \"integer\",\n      \"description\": \"Maximum lightweight public video-detail enrichment requests when video metrics enrichment is disabled.\",\n      \"editor\": \"number\",\n      \"default\": 100,\n      \"minimum\": 0,\n      \"maximum\": 1000\n    }\n  },\n  \"required\": [\n    \"queries\"\n  ]\n}",
    "readme": "# YouTube Search Scraper\n\n## 🔎 What does YouTube Search Scraper do?\n\nYouTube Search Scraper finds public YouTube videos and Shorts from keyword\nsearches, then exports clean discovery rows with video URLs, YouTube IDs,\ntitles, descriptions, publish dates, thumbnails, channel identity, views,\nlikes, comments, subscriber context, durations, and query provenance.\n\nUse it for content research, concept discovery, creator discovery, competitor\nresearch, SEO research, social listening, trend analysis, and lead generation\nworkflows where you need a list of relevant YouTube videos or Shorts before\ndoing deeper analysis.\n\nWith YouTube Search Scraper, you can:\n\n- 🔍 Search YouTube by one keyword or many keywords.\n- 🎬 Discover both regular YouTube videos and Shorts.\n- 📎 Export canonical YouTube URLs and stable `postId` values.\n- 📊 Collect public views, likes, comments, durations, and metric status when\n  YouTube exposes them.\n- 👤 Get channel IDs, handles, names, URLs, verification, and subscriber counts\n  when available publicly.\n- 🧭 Preserve query context with `rootQuery`, `query`, `querySource`,\n  `pageIndex`, and `pageRank`.\n- 🧪 Run broad concept panels with balanced distribution across query families.\n- 📦 Download results as JSON, JSONL, CSV, Excel, XML, RSS, or HTML.\n\nThis Actor uses logged-out public YouTube surfaces. It does not use search\nengines, YouTube API keys, user cookies, logged-in accounts, private analytics,\nYouTube Studio, transcript scraping, comment text scraping, or video downloads.\n\n## ✨ What You Get\n\nEach dataset row represents one discovered public YouTube video or Short.\n\n| Data group         | Example fields                                                                  |\n| ------------------ | ------------------------------------------------------------------------------- |\n| 🔍 Query context   | `rootQuery`, `query`, `querySource`, `pageIndex`, `pageRank`                    |\n| 🔗 Video identity  | `postUrl`, `postId`, `postType`, `isShort`                                      |\n| 🧾 Video metadata  | `title`, `description`, `publishedAt`, `thumbnailUrl`, `videoDurationSeconds`   |\n| 👤 Channel details | `channelId`, `channelHandle`, `channelName`, `channelUrl`, `channelVerified`    |\n| 📊 Post metrics    | `views`, `likes`, `comments`, `postMetricStatus`                                |\n| 👥 Channel metrics | `channelSubscribersCount`, `channelMetricStatus`                                |\n| 🚦 Row status      | `status`, `statusReason`, `observedAt`                                          |\n\nMetric availability depends on what YouTube exposes publicly for each result at\nrun time. Missing counts stay missing; they are not guessed and they are not\nreplaced with fake zeroes.\n\nRows are streamed to the dataset as soon as they are terminal: search discovery,\ndedupe, metadata fill, and public metric enrichment have finished for that row.\n\n## ⚙️ Can I use this Actor through an API?\n\nYes. You can run YouTube Search Scraper manually in Apify Console or use it\nthrough:\n\n- Apify API\n- Python SDK\n- Node.js SDK\n- Webhooks\n- Scheduled runs\n- Apify integrations\n\nThis makes it useful for research pipelines, creator databases, trend monitors,\ncontent intelligence dashboards, enrichment queues, and automated market\nresearch workflows.\n\n## 🎯 Why scrape YouTube search results?\n\nYouTube search results show what videos and Shorts YouTube currently surfaces\nfor a topic. This is useful when you need to understand which concepts,\nformats, creators, keywords, and content angles are visible in a market.\n\n| Use case                    | How YouTube search data helps                                                     |\n| --------------------------- | --------------------------------------------------------------------------------- |\n| 🔥 Concept research         | Find videos and Shorts ranking for a topic before deciding what to analyze next.  |\n| 📈 Trend discovery          | Compare topics by visible videos, views, comments, and channel context.           |\n| 🧑‍🎨 Creator discovery      | Build lists of creators publishing around a niche or keyword family.              |\n| 🕵️ Competitor monitoring    | See what content appears for competitor, category, or product keywords.           |\n| 🧱 Data pipeline seeding    | Feed discovered URLs into downstream metrics, databases, or review workflows.     |\n| 🧭 SEO and content planning | Study titles, descriptions, ranks, Shorts mix, and topic language.                |\n\n## 💵 Pricing Event\n\nYouTube Search Scraper uses pay-per-result pricing. One result means one unique\nYouTube video or Short row written to the default dataset.\n\nUse `maxResults` to control result volume before a run. Start with 10 to 25\nresults when testing a keyword, then increase the cap for broader concept\nresearch.\n\n## 🚀 How do I use YouTube Search Scraper?\n\n1. Create or log in to your Apify account.\n2. Open **YouTube Search Scraper**.\n3. Enter one or more YouTube search queries.\n4. Set the maximum number of unique videos to collect.\n5. Choose whether to include Shorts.\n6. Optionally enable YouTube suggestion expansion for broader topic discovery.\n7. Click **Start**.\n8. Open the **Output** tab to inspect the dataset and run summary.\n9. Download your data in JSON, JSONL, CSV, Excel, XML, RSS, or HTML.\n\n## ⬇️ Input\n\nThe main input is `queries`. Enter one or more YouTube keyword searches.\n\n```json\n{\n    \"queries\": [\"ai tools for small business\"],\n    \"maxResults\": 25,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": false\n}\n```\n\nFor broader concept discovery, enter multiple related queries:\n\n```json\n{\n    \"queries\": [\n        \"ai tools for small business\",\n        \"small business marketing ideas\",\n        \"ugc ad examples\",\n        \"productivity tools for entrepreneurs\"\n    ],\n    \"maxResults\": 100,\n    \"maxPagesPerQuery\": 3,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": true\n}\n```\n\nFor low-cost URL discovery before passing results to a transcript actor, disable\nboth enrichment stages:\n\n```json\n{\n    \"queries\": [\"ai news\", \"machine learning tutorial\", \"creator economy\"],\n    \"maxResults\": 120,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": false,\n    \"enrichWithVideoMetrics\": false,\n    \"enrichMissingChannelFields\": false\n}\n```\n\n### 🎛️ Settings\n\n| Option                 | What it does                                                                                   |\n| ---------------------- | ---------------------------------------------------------------------------------------------- |\n| `queries`              | YouTube keyword queries to search.                                                             |\n| `maxResults`           | Maximum unique videos or Shorts to save across the whole run.                                  |\n| `resultsPerQuery`      | Per-query saved-row cap for sequential mode.                                                    |\n| `maxPagesPerQuery`     | Maximum YouTube search continuation pages to attempt for each query.                           |\n| `searchDistribution`   | `balanced` rotates across queries; `sequential` fills from earlier queries first.              |\n| `includeShorts`        | Include Shorts-style search results when YouTube exposes them.                                 |\n| `enableQueryExpansion` | Add native YouTube autocomplete suggestions as additional searches.                            |\n| `enrichWithVideoMetrics` | Fetch each saved video/Short as a second stage to fill richer public metrics.                |\n| `enrichMissingChannelFields` | When video metrics are off, optionally fetch lightweight details for missing channel/duration fields. |\n| `maxChannelEnrichmentRequests` | Cap lightweight detail requests when video metrics are off.                            |\n\nBalanced mode is best for broad concept panels because it gives each query a\nturn before going deeper. Sequential mode is best when earlier queries should\ntake priority.\n\n## ⬆️ Output sample\n\nResults are stored in the default dataset. Each result is one discovered\nYouTube video or Short.\n\n```json\n{\n    \"platform\": \"youtube\",\n    \"rootQuery\": \"ai tools for small business\",\n    \"query\": \"ai tools for small business\",\n    \"querySource\": \"input\",\n    \"pageIndex\": 0,\n    \"pageRank\": 1,\n    \"postUrl\": \"https://www.youtube.com/watch?v=-y4swMaeJeI\",\n    \"postId\": \"-y4swMaeJeI\",\n    \"postType\": \"video\",\n    \"isShort\": false,\n    \"title\": \"AI Tools for Small Business - 7 Ways Small Business Can Use AI Today\",\n    \"description\": \"Learn practical ways small businesses can use AI tools today...\",\n    \"publishedAt\": \"2024-03-26T07:00:00-07:00\",\n    \"thumbnailUrl\": \"https://i.ytimg.com/vi/-y4swMaeJeI/hq720.jpg\",\n    \"videoDurationSeconds\": 778,\n    \"channelId\": \"UCd2J-PizcFDxWHBBfRkp38Q\",\n    \"channelHandle\": \"@PhilipVanDusen\",\n    \"channelName\": \"Philip VanDusen\",\n    \"channelUrl\": \"https://www.youtube.com/@PhilipVanDusen\",\n    \"channelVerified\": true,\n    \"views\": 232202,\n    \"likes\": 3800,\n    \"comments\": 179,\n    \"postMetricStatus\": [\n        {\n            \"metric\": \"views\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"duration\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"likes\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"comments\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        }\n    ],\n    \"channelSubscribersCount\": 310000,\n    \"channelMetricStatus\": [\n        {\n            \"metric\": \"channelSubscribersCount\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        }\n    ],\n    \"status\": \"ok\",\n    \"statusReason\": null,\n    \"observedAt\": \"2026-07-01T21:50:45.735Z\"\n}\n```\n\n## 🧭 Query fields explained\n\n`rootQuery` is the original query you entered.\n\n`query` is the exact query searched on YouTube. When query expansion is off,\n`query` usually matches `rootQuery`. When expansion is on, `query` may be a\nYouTube-suggested related search.\n\n`querySource` explains where the searched query came from:\n\n- `input` means the query was entered directly by the user.\n- `youtube_suggest` means the query came from native YouTube autocomplete\n  expansion.\n\n## 🧾 Run Summary\n\nThe `OUTPUT` key-value-store record includes:\n\n- `urls`: discovered YouTube video and Short URLs;\n- candidate, duplicate, page, and stop-reason counters;\n- per-query accounting;\n- query expansion accounting;\n- metric enrichment accounting;\n- field coverage for public metrics and channel context.\n\nIf no videos are found, the Actor does not fail the run. It returns a successful\nrun summary with `status: \"no_results\"` and a `statusReason` explaining the\noutcome.\n\n## 🔒 Source Boundary\n\nYouTube Search Scraper uses logged-out public YouTube web surfaces only.\n\nIt does not use:\n\n- YouTube API keys;\n- user cookies;\n- private sessions;\n- logged-in account automation;\n- search engines;\n- YouTube Studio analytics;\n- challenge solving;\n- video downloads;\n- transcript scraping;\n- comment text scraping.\n\n## ⚠️ Limits and interpretation\n\nYouTube search ranking and result availability are live public YouTube behavior\nand can change between runs.\n\n`maxResults` is a saved-row cap, not a guarantee that YouTube has that many\npublic candidates for every query. Narrow queries may source-exhaust below the\nrequested cap.\n\nQuery expansion can broaden intent. Use `rootQuery`, `query`, and `querySource`\nto decide which rows to keep downstream.\n\nPublic counts can be exact, rounded, hidden, delayed, or unavailable depending\non the video, channel, and public YouTube surface. Check `postMetricStatus` and\n`channelMetricStatus` when metric precision matters.\n\n## ❓ FAQ\n\n### Does this Actor need a YouTube API key?\n\nNo. It uses public YouTube web surfaces and does not require a YouTube API key.\n\n### Does this Actor scrape private YouTube data?\n\nNo. It only returns data available from logged-out public YouTube surfaces.\n\n### Does this Actor download videos or scrape transcripts?\n\nNo. It discovers videos and Shorts and returns public metadata and metrics.\n\n### What happens when no videos are found?\n\nThe run succeeds and the summary reports `status: \"no_results\"` with a\n`statusReason`. Empty search results are treated as a data outcome, not a run\nfailure.\n\n## 🔗 Related social media scrapers\n\nContinue the same workflow on this platform or compare public data across\nplatforms.\n\n### Continue on YouTube\n\n- [YouTube Channel Scraper](https://apify.com/chronometrica/youtube-channel-scraper)\n  — collect public channel details, links, and audience stats.\n- [YouTube Metrics Scraper](https://apify.com/chronometrica/youtube-metrics-scraper)\n  — collect public video, Short, channel, and engagement metrics.\n- [YouTube Comments Scraper](https://apify.com/chronometrica/youtube-comments-scraper)\n  — collect public comments, replies, authors, likes, pins, and creator\n  hearts.\n- [YouTube Transcript Scraper](https://apify.com/chronometrica/youtube-transcript-scraper)\n  — extract public captions as text, timestamps, SRT, VTT, or JSON.\n\n### Compare search across sources\n\n- [Google Search Scraper](https://apify.com/chronometrica/google-search-scraper)\n  — collect public web results from Google, Bing, Brave, and DuckDuckGo.\n- [Instagram Search Scraper](https://apify.com/chronometrica/instagram-search-scraper)\n  — find public Reels and metrics by keyword.\n- [TikTok Search Scraper](https://apify.com/chronometrica/tiktok-search-scraper)\n  — find public videos, creators, and metrics by keyword.\n- [Reddit Search Scraper](https://apify.com/chronometrica/reddit-search-scraper)\n  — find public Reddit posts, communities, authors, scores, and dates.\n- [X Search & Discovery Scraper](https://apify.com/chronometrica/x-search-scraper)\n  — find public posts and profiles by keyword.\n",
    "changelog": "# Changelog\n\n- Public release: YouTube Search Scraper for keyword-based discovery of videos and Shorts with URLs, IDs, metadata, and public metrics.\n- Fixed balanced search distribution so one productive page cannot consume the\n  shared global cap before later query slots get a turn. Balanced mode now\n  fair-shares rows across active queries and buffers unoffered rows for later\n  rounds.\n- Validated build 1.0.30 on the 10-query Chronometrica transcript-discovery\n  panel: 120 rows, 10/10 queries represented, 12 rows per root query, zero\n  `not_started` searches.\n- Added low-cost transcript-discovery controls: callers can now disable terminal\n  video metrics and lightweight channel/detail enrichment when they only need\n  discovered YouTube URLs or video IDs.\n",
    "actorDefinition": {
      "$schema": "https://apify.com/schemas/v1/actor.ide.json",
      "actorSpecification": 1,
      "name": "youtube-search-scraper",
      "title": "YouTube Search Scraper - Videos & Shorts",
      "description": "Scrape YouTube search results by keyword. Discover videos and Shorts, then export URLs, video IDs, titles, channels, thumbnails, parsed views, durations, and metric status. No YouTube API key required.",
      "seoTitle": "YouTube Search Scraper for Videos and Shorts",
      "seoDescription": "Scrape YouTube search results by keyword. Find videos and Shorts, export URLs, video IDs, titles, channel metadata, thumbnails, parsed views, durations, and metric status. No YouTube API key required.",
      "categories": [
        "SOCIAL_MEDIA",
        "VIDEOS"
      ],
      "version": "1.0",
      "buildTag": "latest",
      "defaultRunOptions": {
        "build": "latest",
        "timeoutSecs": 3600,
        "memoryMbytes": 4096
      },
      "exampleRunInput": {
        "contentType": "application/json; charset=utf-8",
        "body": "{\n    \"queries\": [\n        \"ai tools for small business\"\n    ],\n    \"maxResults\": 25,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": false\n}"
      },
      "input": {
        "title": "YouTube Search Scraper input",
        "type": "object",
        "schemaVersion": 1,
        "properties": {
          "queries": {
            "title": "Search queries",
            "type": "array",
            "description": "YouTube keyword queries to search.",
            "editor": "stringList",
            "default": [
              "ai tools for small business"
            ],
            "prefill": [
              "ai tools for small business"
            ],
            "minItems": 1,
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "maxResults": {
            "title": "Max unique videos",
            "type": "integer",
            "description": "Maximum unique YouTube video/Short candidates to save across all queries.",
            "editor": "number",
            "default": 25,
            "minimum": 1,
            "maximum": 1000
          },
          "resultsPerQuery": {
            "title": "Results per query",
            "type": "integer",
            "description": "Maximum unique video/Short candidates to save per query in sequential mode. Balanced mode rotates across queries until the global cap or source/page limits are reached.",
            "editor": "number",
            "default": 25,
            "minimum": 1,
            "maximum": 250
          },
          "maxPagesPerQuery": {
            "title": "Max pages per query",
            "type": "integer",
            "description": "Maximum YouTube search continuation pages to attempt for each query.",
            "editor": "number",
            "default": 1,
            "minimum": 1,
            "maximum": 20
          },
          "searchDistribution": {
            "title": "Search distribution",
            "type": "string",
            "description": "Balanced rotates fairly across queries until maxResults or source/page limits are reached. Sequential fills from earlier queries first.",
            "editor": "select",
            "default": "balanced",
            "enum": [
              "balanced",
              "sequential"
            ],
            "enumTitles": [
              "Balanced",
              "Sequential"
            ]
          },
          "includeShorts": {
            "title": "Include Shorts",
            "type": "boolean",
            "description": "Include Shorts-style search cards when YouTube exposes them in public search payloads.",
            "editor": "checkbox",
            "default": true
          },
          "enableQueryExpansion": {
            "title": "Use YouTube suggestions",
            "type": "boolean",
            "description": "Expand each input query with native YouTube autocomplete suggestions. Existing input queries always run first.",
            "editor": "checkbox",
            "default": false
          },
          "enrichWithVideoMetrics": {
            "title": "Enrich with video metrics",
            "type": "boolean",
            "description": "Fetch each saved video/Short as a second stage to fill richer public metrics such as likes, comments, and channel subscriber status. Disable this for low-cost URL discovery before transcript replay.",
            "editor": "checkbox",
            "default": true
          },
          "enrichMissingChannelFields": {
            "title": "Enrich missing channel fields",
            "type": "boolean",
            "description": "When video metrics enrichment is disabled, optionally fetch lightweight public video details for rows missing channel, duration, or publish-date fields.",
            "editor": "checkbox",
            "default": true
          },
          "maxChannelEnrichmentRequests": {
            "title": "Max channel enrichment requests",
            "type": "integer",
            "description": "Maximum lightweight public video-detail enrichment requests when video metrics enrichment is disabled.",
            "editor": "number",
            "default": 100,
            "minimum": 0,
            "maximum": 1000
          }
        },
        "required": [
          "queries"
        ]
      },
      "output": {
        "actorOutputSchemaVersion": 1,
        "title": "YouTube Search Scraper output",
        "description": "Discovered YouTube search rows are stored in the default dataset. OUTPUT contains coverage counts, stop reasons, and search accounting.",
        "properties": {
          "results": {
            "type": "string",
            "title": "Search results",
            "template": "{{links.apiDefaultDatasetUrl}}/items"
          },
          "output": {
            "type": "string",
            "title": "Run summary",
            "template": "{{links.apiDefaultKeyValueStoreUrl}}/records/OUTPUT"
          }
        }
      },
      "storages": {
        "dataset": {
          "actorSpecification": 1,
          "fields": {
            "$schema": "http://json-schema.org/draft-07/schema#",
            "type": "object",
            "properties": {
              "platform": {
                "type": "string",
                "title": "Platform"
              },
              "rootQuery": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Root query"
              },
              "query": {
                "type": "string",
                "title": "Query"
              },
              "querySource": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Query source"
              },
              "pageIndex": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Page index"
              },
              "pageRank": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Page rank"
              },
              "postUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Post URL"
              },
              "postId": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Post ID"
              },
              "postType": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Post type"
              },
              "isShort": {
                "type": [
                  "boolean",
                  "null"
                ],
                "title": "Is Short"
              },
              "title": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Title"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Description"
              },
              "publishedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "title": "Published At"
              },
              "thumbnailUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Thumbnail"
              },
              "videoDurationSeconds": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Video Duration (Seconds)"
              },
              "channelId": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Channel ID"
              },
              "channelHandle": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Channel handle"
              },
              "channelName": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Channel name"
              },
              "channelUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Channel URL"
              },
              "channelVerified": {
                "type": [
                  "boolean",
                  "null"
                ],
                "title": "Channel verified"
              },
              "views": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Views"
              },
              "likes": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Likes"
              },
              "comments": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Comments"
              },
              "postMetricStatus": {
                "type": "array",
                "title": "Post Metric Status",
                "items": {
                  "type": "object",
                  "properties": {
                    "metric": {
                      "type": "string",
                      "enum": [
                        "views",
                        "duration",
                        "likes",
                        "comments"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "present",
                        "unavailable",
                        "not_attempted"
                      ]
                    },
                    "precision": {
                      "type": "string",
                      "enum": [
                        "exact",
                        "display_rounded",
                        "likely_rounded",
                        "unavailable",
                        "unknown"
                      ]
                    }
                  },
                  "required": [
                    "metric",
                    "status",
                    "precision"
                  ]
                }
              },
              "channelSubscribersCount": {
                "type": [
                  "integer",
                  "null"
                ],
                "title": "Subscribers"
              },
              "channelMetricStatus": {
                "type": [
                  "array",
                  "null"
                ],
                "title": "Channel Metric Status",
                "items": {
                  "type": "object",
                  "properties": {
                    "metric": {
                      "type": "string",
                      "enum": [
                        "channelSubscribersCount"
                      ]
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "present",
                        "unavailable",
                        "not_attempted"
                      ]
                    },
                    "precision": {
                      "type": "string",
                      "enum": [
                        "exact",
                        "display_rounded",
                        "likely_rounded",
                        "unavailable",
                        "unknown"
                      ]
                    }
                  },
                  "required": [
                    "metric",
                    "status",
                    "precision"
                  ]
                }
              },
              "status": {
                "type": "string",
                "title": "Status"
              },
              "statusReason": {
                "type": [
                  "string",
                  "null"
                ],
                "title": "Status detail"
              },
              "observedAt": {
                "type": "string",
                "format": "date-time",
                "title": "Observed at"
              }
            },
            "required": [
              "platform",
              "query",
              "status",
              "observedAt"
            ]
          },
          "views": {
            "overview": {
              "title": "YouTube search results",
              "transformation": {
                "fields": [
                  "platform",
                  "rootQuery",
                  "query",
                  "querySource",
                  "pageIndex",
                  "pageRank",
                  "postUrl",
                  "postId",
                  "postType",
                  "isShort",
                  "title",
                  "description",
                  "publishedAt",
                  "thumbnailUrl",
                  "videoDurationSeconds",
                  "channelId",
                  "channelHandle",
                  "channelName",
                  "channelUrl",
                  "channelVerified",
                  "views",
                  "likes",
                  "comments",
                  "postMetricStatus",
                  "channelSubscribersCount",
                  "channelMetricStatus",
                  "status",
                  "statusReason",
                  "observedAt"
                ]
              },
              "display": {
                "component": "table",
                "properties": {
                  "platform": {
                    "label": "Platform",
                    "format": "text"
                  },
                  "rootQuery": {
                    "label": "Root query",
                    "format": "text"
                  },
                  "query": {
                    "label": "Query",
                    "format": "text"
                  },
                  "querySource": {
                    "label": "Query source",
                    "format": "text"
                  },
                  "pageIndex": {
                    "label": "Page",
                    "format": "number"
                  },
                  "pageRank": {
                    "label": "Page rank",
                    "format": "number"
                  },
                  "postUrl": {
                    "label": "Post",
                    "format": "link"
                  },
                  "postId": {
                    "label": "Post ID",
                    "format": "text"
                  },
                  "postType": {
                    "label": "Type",
                    "format": "text"
                  },
                  "isShort": {
                    "label": "Short",
                    "format": "boolean"
                  },
                  "title": {
                    "label": "Title",
                    "format": "text"
                  },
                  "description": {
                    "label": "Description",
                    "format": "text"
                  },
                  "publishedAt": {
                    "label": "Published",
                    "format": "date"
                  },
                  "thumbnailUrl": {
                    "label": "Thumbnail",
                    "format": "link"
                  },
                  "videoDurationSeconds": {
                    "label": "Duration",
                    "format": "number"
                  },
                  "channelId": {
                    "label": "Channel ID",
                    "format": "text"
                  },
                  "channelHandle": {
                    "label": "Handle",
                    "format": "text"
                  },
                  "channelName": {
                    "label": "Channel",
                    "format": "text"
                  },
                  "channelUrl": {
                    "label": "Channel URL",
                    "format": "link"
                  },
                  "channelVerified": {
                    "label": "Verified",
                    "format": "boolean"
                  },
                  "views": {
                    "label": "Views",
                    "format": "number"
                  },
                  "likes": {
                    "label": "Likes",
                    "format": "number"
                  },
                  "comments": {
                    "label": "Comments",
                    "format": "number"
                  },
                  "postMetricStatus": {
                    "label": "Post Metric Status",
                    "format": "object"
                  },
                  "channelSubscribersCount": {
                    "label": "Subscribers",
                    "format": "number"
                  },
                  "channelMetricStatus": {
                    "label": "Channel Metric Status",
                    "format": "object"
                  },
                  "status": {
                    "label": "Status",
                    "format": "text"
                  },
                  "statusReason": {
                    "label": "Status detail",
                    "format": "text"
                  },
                  "observedAt": {
                    "label": "Observed",
                    "format": "date"
                  }
                }
              }
            }
          }
        }
      },
      "dockerfile": "../Dockerfile",
      "changelog": "# Changelog\n\n- Public release: YouTube Search Scraper for keyword-based discovery of videos and Shorts with URLs, IDs, metadata, and public metrics.\n- Fixed balanced search distribution so one productive page cannot consume the\n  shared global cap before later query slots get a turn. Balanced mode now\n  fair-shares rows across active queries and buffers unoffered rows for later\n  rounds.\n- Validated build 1.0.30 on the 10-query Chronometrica transcript-discovery\n  panel: 120 rows, 10/10 queries represented, 12 rows per root query, zero\n  `not_started` searches.\n- Added low-cost transcript-discovery controls: callers can now disable terminal\n  video metrics and lightweight channel/detail enrichment when they only need\n  discovered YouTube URLs or video IDs.\n",
      "readme": "# YouTube Search Scraper\n\n## 🔎 What does YouTube Search Scraper do?\n\nYouTube Search Scraper finds public YouTube videos and Shorts from keyword\nsearches, then exports clean discovery rows with video URLs, YouTube IDs,\ntitles, descriptions, publish dates, thumbnails, channel identity, views,\nlikes, comments, subscriber context, durations, and query provenance.\n\nUse it for content research, concept discovery, creator discovery, competitor\nresearch, SEO research, social listening, trend analysis, and lead generation\nworkflows where you need a list of relevant YouTube videos or Shorts before\ndoing deeper analysis.\n\nWith YouTube Search Scraper, you can:\n\n- 🔍 Search YouTube by one keyword or many keywords.\n- 🎬 Discover both regular YouTube videos and Shorts.\n- 📎 Export canonical YouTube URLs and stable `postId` values.\n- 📊 Collect public views, likes, comments, durations, and metric status when\n  YouTube exposes them.\n- 👤 Get channel IDs, handles, names, URLs, verification, and subscriber counts\n  when available publicly.\n- 🧭 Preserve query context with `rootQuery`, `query`, `querySource`,\n  `pageIndex`, and `pageRank`.\n- 🧪 Run broad concept panels with balanced distribution across query families.\n- 📦 Download results as JSON, JSONL, CSV, Excel, XML, RSS, or HTML.\n\nThis Actor uses logged-out public YouTube surfaces. It does not use search\nengines, YouTube API keys, user cookies, logged-in accounts, private analytics,\nYouTube Studio, transcript scraping, comment text scraping, or video downloads.\n\n## ✨ What You Get\n\nEach dataset row represents one discovered public YouTube video or Short.\n\n| Data group         | Example fields                                                                  |\n| ------------------ | ------------------------------------------------------------------------------- |\n| 🔍 Query context   | `rootQuery`, `query`, `querySource`, `pageIndex`, `pageRank`                    |\n| 🔗 Video identity  | `postUrl`, `postId`, `postType`, `isShort`                                      |\n| 🧾 Video metadata  | `title`, `description`, `publishedAt`, `thumbnailUrl`, `videoDurationSeconds`   |\n| 👤 Channel details | `channelId`, `channelHandle`, `channelName`, `channelUrl`, `channelVerified`    |\n| 📊 Post metrics    | `views`, `likes`, `comments`, `postMetricStatus`                                |\n| 👥 Channel metrics | `channelSubscribersCount`, `channelMetricStatus`                                |\n| 🚦 Row status      | `status`, `statusReason`, `observedAt`                                          |\n\nMetric availability depends on what YouTube exposes publicly for each result at\nrun time. Missing counts stay missing; they are not guessed and they are not\nreplaced with fake zeroes.\n\nRows are streamed to the dataset as soon as they are terminal: search discovery,\ndedupe, metadata fill, and public metric enrichment have finished for that row.\n\n## ⚙️ Can I use this Actor through an API?\n\nYes. You can run YouTube Search Scraper manually in Apify Console or use it\nthrough:\n\n- Apify API\n- Python SDK\n- Node.js SDK\n- Webhooks\n- Scheduled runs\n- Apify integrations\n\nThis makes it useful for research pipelines, creator databases, trend monitors,\ncontent intelligence dashboards, enrichment queues, and automated market\nresearch workflows.\n\n## 🎯 Why scrape YouTube search results?\n\nYouTube search results show what videos and Shorts YouTube currently surfaces\nfor a topic. This is useful when you need to understand which concepts,\nformats, creators, keywords, and content angles are visible in a market.\n\n| Use case                    | How YouTube search data helps                                                     |\n| --------------------------- | --------------------------------------------------------------------------------- |\n| 🔥 Concept research         | Find videos and Shorts ranking for a topic before deciding what to analyze next.  |\n| 📈 Trend discovery          | Compare topics by visible videos, views, comments, and channel context.           |\n| 🧑‍🎨 Creator discovery      | Build lists of creators publishing around a niche or keyword family.              |\n| 🕵️ Competitor monitoring    | See what content appears for competitor, category, or product keywords.           |\n| 🧱 Data pipeline seeding    | Feed discovered URLs into downstream metrics, databases, or review workflows.     |\n| 🧭 SEO and content planning | Study titles, descriptions, ranks, Shorts mix, and topic language.                |\n\n## 💵 Pricing Event\n\nYouTube Search Scraper uses pay-per-result pricing. One result means one unique\nYouTube video or Short row written to the default dataset.\n\nUse `maxResults` to control result volume before a run. Start with 10 to 25\nresults when testing a keyword, then increase the cap for broader concept\nresearch.\n\n## 🚀 How do I use YouTube Search Scraper?\n\n1. Create or log in to your Apify account.\n2. Open **YouTube Search Scraper**.\n3. Enter one or more YouTube search queries.\n4. Set the maximum number of unique videos to collect.\n5. Choose whether to include Shorts.\n6. Optionally enable YouTube suggestion expansion for broader topic discovery.\n7. Click **Start**.\n8. Open the **Output** tab to inspect the dataset and run summary.\n9. Download your data in JSON, JSONL, CSV, Excel, XML, RSS, or HTML.\n\n## ⬇️ Input\n\nThe main input is `queries`. Enter one or more YouTube keyword searches.\n\n```json\n{\n    \"queries\": [\"ai tools for small business\"],\n    \"maxResults\": 25,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": false\n}\n```\n\nFor broader concept discovery, enter multiple related queries:\n\n```json\n{\n    \"queries\": [\n        \"ai tools for small business\",\n        \"small business marketing ideas\",\n        \"ugc ad examples\",\n        \"productivity tools for entrepreneurs\"\n    ],\n    \"maxResults\": 100,\n    \"maxPagesPerQuery\": 3,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": true\n}\n```\n\nFor low-cost URL discovery before passing results to a transcript actor, disable\nboth enrichment stages:\n\n```json\n{\n    \"queries\": [\"ai news\", \"machine learning tutorial\", \"creator economy\"],\n    \"maxResults\": 120,\n    \"searchDistribution\": \"balanced\",\n    \"includeShorts\": true,\n    \"enableQueryExpansion\": false,\n    \"enrichWithVideoMetrics\": false,\n    \"enrichMissingChannelFields\": false\n}\n```\n\n### 🎛️ Settings\n\n| Option                 | What it does                                                                                   |\n| ---------------------- | ---------------------------------------------------------------------------------------------- |\n| `queries`              | YouTube keyword queries to search.                                                             |\n| `maxResults`           | Maximum unique videos or Shorts to save across the whole run.                                  |\n| `resultsPerQuery`      | Per-query saved-row cap for sequential mode.                                                    |\n| `maxPagesPerQuery`     | Maximum YouTube search continuation pages to attempt for each query.                           |\n| `searchDistribution`   | `balanced` rotates across queries; `sequential` fills from earlier queries first.              |\n| `includeShorts`        | Include Shorts-style search results when YouTube exposes them.                                 |\n| `enableQueryExpansion` | Add native YouTube autocomplete suggestions as additional searches.                            |\n| `enrichWithVideoMetrics` | Fetch each saved video/Short as a second stage to fill richer public metrics.                |\n| `enrichMissingChannelFields` | When video metrics are off, optionally fetch lightweight details for missing channel/duration fields. |\n| `maxChannelEnrichmentRequests` | Cap lightweight detail requests when video metrics are off.                            |\n\nBalanced mode is best for broad concept panels because it gives each query a\nturn before going deeper. Sequential mode is best when earlier queries should\ntake priority.\n\n## ⬆️ Output sample\n\nResults are stored in the default dataset. Each result is one discovered\nYouTube video or Short.\n\n```json\n{\n    \"platform\": \"youtube\",\n    \"rootQuery\": \"ai tools for small business\",\n    \"query\": \"ai tools for small business\",\n    \"querySource\": \"input\",\n    \"pageIndex\": 0,\n    \"pageRank\": 1,\n    \"postUrl\": \"https://www.youtube.com/watch?v=-y4swMaeJeI\",\n    \"postId\": \"-y4swMaeJeI\",\n    \"postType\": \"video\",\n    \"isShort\": false,\n    \"title\": \"AI Tools for Small Business - 7 Ways Small Business Can Use AI Today\",\n    \"description\": \"Learn practical ways small businesses can use AI tools today...\",\n    \"publishedAt\": \"2024-03-26T07:00:00-07:00\",\n    \"thumbnailUrl\": \"https://i.ytimg.com/vi/-y4swMaeJeI/hq720.jpg\",\n    \"videoDurationSeconds\": 778,\n    \"channelId\": \"UCd2J-PizcFDxWHBBfRkp38Q\",\n    \"channelHandle\": \"@PhilipVanDusen\",\n    \"channelName\": \"Philip VanDusen\",\n    \"channelUrl\": \"https://www.youtube.com/@PhilipVanDusen\",\n    \"channelVerified\": true,\n    \"views\": 232202,\n    \"likes\": 3800,\n    \"comments\": 179,\n    \"postMetricStatus\": [\n        {\n            \"metric\": \"views\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"duration\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"likes\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        },\n        {\n            \"metric\": \"comments\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        }\n    ],\n    \"channelSubscribersCount\": 310000,\n    \"channelMetricStatus\": [\n        {\n            \"metric\": \"channelSubscribersCount\",\n            \"status\": \"present\",\n            \"precision\": \"exact\"\n        }\n    ],\n    \"status\": \"ok\",\n    \"statusReason\": null,\n    \"observedAt\": \"2026-07-01T21:50:45.735Z\"\n}\n```\n\n## 🧭 Query fields explained\n\n`rootQuery` is the original query you entered.\n\n`query` is the exact query searched on YouTube. When query expansion is off,\n`query` usually matches `rootQuery`. When expansion is on, `query` may be a\nYouTube-suggested related search.\n\n`querySource` explains where the searched query came from:\n\n- `input` means the query was entered directly by the user.\n- `youtube_suggest` means the query came from native YouTube autocomplete\n  expansion.\n\n## 🧾 Run Summary\n\nThe `OUTPUT` key-value-store record includes:\n\n- `urls`: discovered YouTube video and Short URLs;\n- candidate, duplicate, page, and stop-reason counters;\n- per-query accounting;\n- query expansion accounting;\n- metric enrichment accounting;\n- field coverage for public metrics and channel context.\n\nIf no videos are found, the Actor does not fail the run. It returns a successful\nrun summary with `status: \"no_results\"` and a `statusReason` explaining the\noutcome.\n\n## 🔒 Source Boundary\n\nYouTube Search Scraper uses logged-out public YouTube web surfaces only.\n\nIt does not use:\n\n- YouTube API keys;\n- user cookies;\n- private sessions;\n- logged-in account automation;\n- search engines;\n- YouTube Studio analytics;\n- challenge solving;\n- video downloads;\n- transcript scraping;\n- comment text scraping.\n\n## ⚠️ Limits and interpretation\n\nYouTube search ranking and result availability are live public YouTube behavior\nand can change between runs.\n\n`maxResults` is a saved-row cap, not a guarantee that YouTube has that many\npublic candidates for every query. Narrow queries may source-exhaust below the\nrequested cap.\n\nQuery expansion can broaden intent. Use `rootQuery`, `query`, and `querySource`\nto decide which rows to keep downstream.\n\nPublic counts can be exact, rounded, hidden, delayed, or unavailable depending\non the video, channel, and public YouTube surface. Check `postMetricStatus` and\n`channelMetricStatus` when metric precision matters.\n\n## ❓ FAQ\n\n### Does this Actor need a YouTube API key?\n\nNo. It uses public YouTube web surfaces and does not require a YouTube API key.\n\n### Does this Actor scrape private YouTube data?\n\nNo. It only returns data available from logged-out public YouTube surfaces.\n\n### Does this Actor download videos or scrape transcripts?\n\nNo. It discovers videos and Shorts and returns public metadata and metrics.\n\n### What happens when no videos are found?\n\nThe run succeeds and the summary reports `status: \"no_results\"` with a\n`statusReason`. Empty search results are treated as a data outcome, not a run\nfailure.\n\n## 🔗 Related social media scrapers\n\nContinue the same workflow on this platform or compare public data across\nplatforms.\n\n### Continue on YouTube\n\n- [YouTube Channel Scraper](https://apify.com/chronometrica/youtube-channel-scraper)\n  — collect public channel details, links, and audience stats.\n- [YouTube Metrics Scraper](https://apify.com/chronometrica/youtube-metrics-scraper)\n  — collect public video, Short, channel, and engagement metrics.\n- [YouTube Comments Scraper](https://apify.com/chronometrica/youtube-comments-scraper)\n  — collect public comments, replies, authors, likes, pins, and creator\n  hearts.\n- [YouTube Transcript Scraper](https://apify.com/chronometrica/youtube-transcript-scraper)\n  — extract public captions as text, timestamps, SRT, VTT, or JSON.\n\n### Compare search across sources\n\n- [Google Search Scraper](https://apify.com/chronometrica/google-search-scraper)\n  — collect public web results from Google, Bing, Brave, and DuckDuckGo.\n- [Instagram Search Scraper](https://apify.com/chronometrica/instagram-search-scraper)\n  — find public Reels and metrics by keyword.\n- [TikTok Search Scraper](https://apify.com/chronometrica/tiktok-search-scraper)\n  — find public videos, creators, and metrics by keyword.\n- [Reddit Search Scraper](https://apify.com/chronometrica/reddit-search-scraper)\n  — find public Reddit posts, communities, authors, scores, and dates.\n- [X Search & Discovery Scraper](https://apify.com/chronometrica/x-search-scraper)\n  — find public posts and profiles by keyword.\n"
    },
    "buildNumber": "1.0.34",
    "actVersion": {
      "sourceType": "SOURCE_FILES",
      "buildTag": "latest",
      "versionNumber": "1.0"
    },
    "usage": {
      "ACTOR_COMPUTE_UNITS": 0.06439666666666667
    }
  }
}