{
  "openapi": "3.0.1",
  "info": {
    "title": "YouTube Search & Video Metrics [Only $0.9💰] Scraper",
    "description": "Scrape YouTube search results without login or API key. Metrics mode returns flat analytics rows — parsed views, duration, channel, publish age — per video, with free title/age filters (filtered videos never charged). Schedule it for keyword monitoring. JSON or CSV.",
    "version": "0.0",
    "x-build-id": "KsomhDFNeSRhbMnwq"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/memo23~youtube-search-results-scraper/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-memo23-youtube-search-results-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/memo23~youtube-search-results-scraper/runs": {
      "post": {
        "operationId": "runs-sync-memo23-youtube-search-results-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/memo23~youtube-search-results-scraper/run-sync": {
      "post": {
        "operationId": "run-sync-memo23-youtube-search-results-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "searchQueries": {
            "title": "Search queries",
            "type": "array",
            "description": "Plain YouTube search terms, one per line — e.g. <code>lofi hip hop</code>. Each term runs as its own search and gets its own <code>maxItems</code> budget.",
            "default": [
              "lofi hip hop"
            ],
            "items": {
              "type": "string"
            }
          },
          "startUrls": {
            "title": "Direct YouTube search URLs",
            "type": "array",
            "description": "YouTube URLs of any kind this actor accepts: search (<code>/results?search_query=</code>), a single video / Short / youtu.be link, a playlist, a channel (<code>/channel/UC…</code> or <code>@handle</code>), or a hashtag. Search URLs honour their <code>sp=</code> token unless the filter inputs below override it. Plain keywords still work here too.",
            "default": [],
            "items": {
              "type": "string"
            }
          },
          "maxItems": {
            "title": "Max Items",
            "minimum": 0,
            "type": "integer",
            "description": "Maximum number of items to scrape <b>per search query</b>. Default 100. <code>0</code> = no limit (the run stops when YouTube runs out of results).",
            "default": 100
          },
          "videoType": {
            "title": "Result type",
            "enum": [
              "any",
              "video",
              "channel",
              "playlist",
              "movie"
            ],
            "type": "string",
            "description": "Restrict results to one kind. <code>channel</code>, <code>playlist</code> and <code>movie</code> return channel/playlist/movie rows instead of videos. In <b>metrics mode</b> every row carries a <code>resultType</code> field so a mixed dataset stays sortable; the default raw output keeps YouTube's own wrapper (<code>videoRenderer</code>, <code>channelRenderer</code>, <code>lockupViewModel</code>, <code>movieRenderer</code>) as the discriminator. Whatever this is set to, the other kinds are treated as junk: skipped, never pushed, never charged.",
            "default": "any"
          },
          "lengthFilter": {
            "title": "Video length",
            "enum": [
              "any",
              "under4",
              "between420",
              "plus20"
            ],
            "type": "string",
            "description": "Duration bucket, matching YouTube's own three options.",
            "default": "any"
          },
          "dateFilter": {
            "title": "Upload date",
            "enum": [
              "any",
              "hour",
              "today",
              "week",
              "month",
              "year"
            ],
            "type": "string",
            "description": "How recently the video was uploaded. For an exact cutoff use <b>Oldest post date</b> below — the two combine, and the tighter one wins.",
            "default": "any"
          },
          "oldestPostDate": {
            "title": "Oldest post date",
            "type": "string",
            "description": "Keep only results published after this point. Accepts an ISO date (<code>2026-01-15</code>) or a relative phrase (<code>3 days</code>, <code>2 weeks</code>). YouTube has no exact-date filter, so this narrows the search to the tightest matching upload-date bucket and then applies the exact cutoff per row — free, before billing. <b>Precision caveat:</b> YouTube only reports relative ages, so \"1 month ago\" covers 30-59 days and rows near the boundary can be off by weeks. Results with no age at all (live streams) are kept.",
            "default": ""
          },
          "publishedBefore": {
            "title": "Newest post date (published before)",
            "type": "string",
            "description": "Keep only results published BEFORE this point — the opposite bound of <b>Oldest post date</b>. Accepts an ISO date (<code>2026-01-15</code>) or a relative phrase (<code>3 days</code>). Free, before billing. Combine both for a date window. Rows with no age (live streams) are kept.",
            "default": ""
          },
          "minDurationSeconds": {
            "title": "Min duration (seconds)",
            "minimum": 0,
            "type": "integer",
            "description": "Drop videos shorter than this. <code>0</code> = off. Live rows with no duration are kept. Free.",
            "default": 0
          },
          "maxDurationSeconds": {
            "title": "Max duration (seconds)",
            "minimum": 0,
            "type": "integer",
            "description": "Drop videos longer than this. <code>0</code> = off. Free.",
            "default": 0
          },
          "language": {
            "title": "Interface language (hl)",
            "type": "string",
            "description": "YouTube interface / relevance language as a BCP-47 code — <code>en</code>, <code>de</code>, <code>ja</code>, <code>pt-BR</code>. Sent as InnerTube <code>hl</code>. Empty = <code>en</code>.",
            "default": ""
          },
          "country": {
            "title": "Country (gl)",
            "type": "string",
            "description": "Localise search results to this ISO 3166-1 alpha-2 country — <code>US</code>, <code>GB</code>, <code>IN</code>, <code>DE</code>. Sent as InnerTube <code>gl</code>. On paid runs the proxy country is ignored for the exit IP, but THIS field still localises the search. Empty falls back to the proxy country, then <code>US</code>.",
            "default": ""
          },
          "videoDepthDetails": {
            "title": "Watch-page depth",
            "enum": [
              "none",
              "standard",
              "detailed"
            ],
            "type": "string",
            "description": "<code>none</code> is the cheap search-renderer row. <code>standard</code> adds one ANDROID player request per video for likes, full description and the exact publish date (charged as <code>watch-depth</code>). <code>detailed</code> also copies caption-track metadata off that payload.",
            "default": "none"
          },
          "aiVideoSummary": {
            "title": "Add a short video summary",
            "type": "boolean",
            "description": "Write a short extractive <code>summary</code> from the description and transcript (when we have them). Not an LLM call — charged as <code>ai-summary</code> only when a summary is actually produced. Pair with <b>Watch-page depth</b> so there is a description to summarise.",
            "default": false
          },
          "sortVideosBy": {
            "title": "Sort results by",
            "enum": [
              "relevance",
              "date",
              "views",
              "rating",
              "NEWEST",
              "POPULAR",
              "OLDEST"
            ],
            "type": "string",
            "description": "YouTube's own sort order. <code>NEWEST</code>, <code>POPULAR</code> and <code>OLDEST</code> are accepted as aliases for date / views / date-ascending, so input written for another YouTube scraper can be pasted in unchanged.",
            "default": "relevance"
          },
          "sortingOrder": {
            "title": "Sorting order",
            "enum": [
              "descending",
              "ascending"
            ],
            "type": "string",
            "description": "YouTube's search has no ascending option — every server-side sort is newest/most-viewed first. <code>Ascending</code> therefore reorders the results <b>this run actually collected</b>, not all of YouTube: with a limit of 100 you get the newest 100 shown oldest-first, not the 100 oldest videos on the platform. With <b>Sort results by</b> set to upload date or view count the rows are ordered on the parsed number; on relevance/rating there is no number to order on, so the collected order is simply reversed. Rows are held in memory until the crawl ends (capped at 5,000 rows or ~32 MB, past which the run streams normally), so an aborted or migrated run loses the buffer.",
            "default": "descending"
          },
          "isHD": {
            "title": "HD only",
            "type": "boolean",
            "description": "Only results available in HD.",
            "default": false
          },
          "is4K": {
            "title": "4K only",
            "type": "boolean",
            "description": "Only results available in 4K.",
            "default": false
          },
          "isHDR": {
            "title": "HDR only",
            "type": "boolean",
            "description": "Only results available in HDR.",
            "default": false
          },
          "is360": {
            "title": "360-degree only",
            "type": "boolean",
            "description": "Only 360-degree results. A niche format — expect small result sets.",
            "default": false
          },
          "is3D": {
            "title": "3D only",
            "type": "boolean",
            "description": "Only 3D results. 3D is a near-dead format on YouTube, so an empty or tiny result set here is the platform, not a bug.",
            "default": false
          },
          "isVR180": {
            "title": "VR180 only",
            "type": "boolean",
            "description": "Only VR180 results. Same caveat as 3D — very few videos qualify.",
            "default": false
          },
          "isLive": {
            "title": "Live only",
            "type": "boolean",
            "description": "Only streams that are live right now. Live rows carry no duration and no publish date, so <code>durationSeconds</code> and <code>publishedAgeDays</code> come back null and the view count reads as \"watching\". Because every result is then already a live stream, <b>Max live streams</b> has nothing extra to add and is ignored — <code>maxItems</code> is the budget for a live-only run.",
            "default": false
          },
          "hasSubtitles": {
            "title": "Has subtitles/CC",
            "type": "boolean",
            "description": "Only results with subtitles or closed captions. This is YouTube's <i>Subtitles/CC</i> filter — the cheap, reliable way to restrict a run to caption-bearing videos. Not to be confused with <b>Creative Commons</b> below.",
            "default": false
          },
          "hasCC": {
            "title": "Creative Commons licence",
            "type": "boolean",
            "description": "Only results published under a Creative Commons licence. This is YouTube's <i>Creative Commons</i> filter, NOT the subtitles one above — the two are the most commonly mixed-up pair in this list.",
            "default": false
          },
          "hasLocation": {
            "title": "Has a location",
            "type": "boolean",
            "description": "Only results tagged with a location.",
            "default": false
          },
          "isBought": {
            "title": "Purchased only",
            "type": "boolean",
            "description": "YouTube's <i>Purchased</i> filter. <b>Read before enabling:</b> \"purchased\" means titles bought by the SIGNED-IN account, and this scraper sends no cookies or session, so a guest run returns an empty or effectively unfiltered set. Shipped for parity with YouTube's filter list; expect nothing useful back.",
            "default": false
          },
          "maxResultsShorts": {
            "title": "Max Shorts",
            "minimum": 0,
            "type": "integer",
            "description": "Collect up to this many Shorts <b>per query</b>, budgeted separately from <code>maxItems</code>. Shorts arrive in shelves of ~5 per search page and are flattened into one row each (tagged <code>resultType: \"short\"</code> in metrics mode; the raw output pushes the shelf entry itself). <b>Throughput caveat:</b> a search page carries far fewer Shorts than a channel's Shorts tab, so a large target needs many pages. <b>Measured limitation:</b> YouTube stops sending Shorts shelves altogether once <b>Result type</b> or <b>Sort results by</b> is set, so those combinations collect zero Shorts — the run gives up on the Shorts budget after 5 empty pages instead of paginating to the end of the results. 0 = off.",
            "default": 0
          },
          "maxResultStreams": {
            "title": "Max live streams",
            "minimum": 0,
            "type": "integer",
            "description": "Collect up to this many <b>additional</b> live streams per query, budgeted separately from <code>maxItems</code> (tagged <code>resultType: \"stream\"</code> in metrics mode). Because a normal search feed contains barely any live rows, enabling this queues a second, live-filtered search per query, reusing all your other filters. Live streams that turn up in the normal feed anyway keep counting toward <code>maxItems</code>, exactly as they do today — so a query returns at most <code>maxItems + maxResultStreams</code> rows. <b>Ignored</b> when <b>Live only</b> is on (every result is already a live stream) or when <b>Result type</b> is channel/playlist/movie (that search returns no videos at all). 0 = off.",
            "default": 0
          },
          "transcriptionAndSubtitle": {
            "title": "Subtitle mode",
            "enum": [
              "NONE",
              "SUBTITLE_METADATA",
              "ALWAYS_SUBTITLES"
            ],
            "type": "string",
            "description": "<code>NONE</code> costs nothing extra. The other two add ONE watch-page request per result row, which lengthens the run and raises the block rate on the search itself — leave off unless you need it.",
            "default": "NONE"
          },
          "subtitlesLanguage": {
            "title": "Subtitle language",
            "type": "string",
            "description": "Which caption track to select, as a BCP-47 code — <code>en</code>, <code>de</code>, <code>es</code>, <code>fr</code>, <code>it</code>, <code>ja</code>, <code>ko</code>, <code>nl</code>, <code>pl</code>, <code>pt</code>, <code>ru</code>, … any code YouTube serves works. <code>any</code> takes whatever the video has. A bare language code also matches its regional variants (<code>en</code> matches <code>en-GB</code>).",
            "default": "any"
          },
          "preferAutoGeneratedSubtitles": {
            "title": "Prefer auto-generated subtitles",
            "type": "boolean",
            "description": "When a video has both an authored and an auto-generated (ASR) track in the chosen language, pick the auto-generated one. Off = prefer the human-authored track.",
            "default": false
          },
          "subtitlesFormat": {
            "title": "Subtitle format",
            "enum": [
              "srt",
              "vtt",
              "xml",
              "plaintext",
              "json"
            ],
            "type": "string",
            "description": "Output format for subtitle text, when it can be retrieved. <code>plaintext</code> de-duplicates the overlapping cues that auto-generated tracks repeat. <b>No effect today:</b> the caption text sits behind YouTube's proof-of-origin gate (see the section note above), so rows come back as <code>unavailable_requires_pot</code> before a format is ever applied. Wired and tested, waiting on that gate.",
            "default": "srt"
          },
          "saveSubsToKVS": {
            "title": "Save subtitles to the key-value store",
            "type": "boolean",
            "description": "Write each subtitle file to the run's key-value store and put its public URL on the row as <code>subtitlesUrl</code>, instead of inlining a large string into the dataset item. <b>No effect today</b>, for the same reason as the format above: there is no caption text to write while the proof-of-origin gate holds.",
            "default": false
          },
          "metricsMode": {
            "title": "Metrics mode (flat analytics rows)",
            "type": "boolean",
            "description": "Output ONE flat, analytics-ready row per video — <code>title</code>, <code>channelName</code>, <code>views</code> (parsed number), <code>durationSeconds</code>, <code>publishedAgeDays</code>, <code>isLive</code>, <code>resultType</code>, <code>query</code>, <code>checkedAt</code> — instead of YouTube's raw renderer object. Junk (shelves, ads, and the channel cards and playlist lockups YouTube mixes into a keyword search) is skipped whatever this is set to, so you only pay for the kind of result you asked for in <b>Result type</b>. Ideal for dashboards, spreadsheets, and scheduled keyword monitoring.",
            "default": false
          },
          "titleMustInclude": {
            "title": "Title must include",
            "type": "array",
            "description": "Keep only videos whose title contains at least ONE of these terms (case-insensitive). Filtered videos are never charged.",
            "default": [],
            "items": {
              "type": "string"
            }
          },
          "titleExclude": {
            "title": "Skip titles containing",
            "type": "array",
            "description": "Drop videos whose title contains ANY of these terms (case-insensitive). Applied after 'Title must include'. Free.",
            "default": [],
            "items": {
              "type": "string"
            }
          },
          "maxAgeDays": {
            "title": "Max video age (days)",
            "minimum": 0,
            "type": "integer",
            "description": "Drop videos older than this many days (based on YouTube's relative publish time). <code>0</code> = no age limit. Combine with a scheduled run for fresh-content monitoring. Free — dropped videos are never charged.",
            "default": 0
          },
          "maxConcurrency": {
            "title": "Max Concurrency",
            "minimum": 1,
            "type": "integer",
            "description": "Maximum number of pages that can be processed at the same time. Default 10.",
            "default": 10
          },
          "minConcurrency": {
            "title": "Min Concurrency",
            "minimum": 1,
            "type": "integer",
            "description": "Minimum number of pages that will be processed at the same time. Default 1.",
            "default": 1
          },
          "maxRequestRetries": {
            "title": "Max Request Retries",
            "minimum": 0,
            "type": "integer",
            "description": "Number of times the crawler will retry a failed request before giving up. Default 10.",
            "default": 10
          },
          "proxy": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Runs go out through the actor's own residential pools, so Apify proxy groups and countries chosen here are not used (the run logs a line saying so). Only your own proxy URLs, with 'Use Apify Proxy' switched off, are honoured.",
            "default": {
              "useApifyProxy": true
            }
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}