# YouTube Channel Finder: Score, Filter, Monitor (`solalab_digital/youtube-channel-search`) Actor

Search YouTube by keyword or results URL and save one row per public channel: profile, metrics, links, description e-mails, posting activity and a 0-100 opportunity score. Filter by subscribers, country, views and recency. Includes an HTML report and a compare-with-previous-run monitor mode.

- **URL**: https://apify.com/solalab\_digital/youtube-channel-search.md
- **Developed by:** [Sankov Vadim](https://apify.com/solalab_digital) (community)
- **Categories:** Social media, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 results

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

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### 🔎 YouTube channels for any keyword, scored 0-100 and ready to shortlist

**0-100 opportunity score** · **HTML dashboard on every run** · **run-to-run monitoring** · **public contacts** · **checkpointed and retried**

Type a topic, get back real channels: who they are, how big, where they are based, whether they still post, and how to reach them. The Actor searches YouTube for your keywords (or for search-result URLs you already have), opens each channel's public About page, and saves one clean row per channel. It needs no YouTube login, cookies, API key or browser. It reads public pages only.

#### 🚀 Quick start in 3 steps

1. Press **Start**. The input is pre-filled with one keyword and a cap of 10 channels, so the first run is a cheap test.
2. Open the **Output** tab: the dataset holds the channels and the **Report** link opens the HTML dashboard.
3. Replace the keyword, raise the limits, add filters, and turn on activity, contacts or monitoring when you need them.

#### ✨ Why this Actor

| You get | Instead of |
| --- | --- |
| A 0-100 opportunity score per channel (recent activity, audience size, profile completeness, verification) | A flat list you have to rank by hand |
| A standalone HTML report with filters, histogram, top-10 cards and CSV export | Only raw rows |
| Monitor mode: new channels, subscriber changes and missing channels since the last run | One-off snapshots |
| Links and plain-text e-mails from the description, with hidden e-mails left alone | Guessed or invented contacts |
| Filters for subscribers, videos, views, country, verified badge and posting recency, applied before charging | Paying for rows you then delete |
| Checkpoints, retries and a spending-limit stop that keeps rows already saved | Lost work on a migration or early stop |

#### 🎯 Use cases

- Build a niche **creator list** from topics, products or audience interests.
- Shortlist channels for **influencer outreach**, ranked by score instead of gut feeling.
- Run **competitor and market research** on a content category.
- Find **small, active channels** with the subscriber range, country and posting-recency filters.
- Pull **public contact details** (links and any e-mail written in the description) for outreach research.
- **Monitor a niche** on a schedule: see which channels are new and how subscriber counts moved since the last run.

#### 📦 What you get

Each dataset row is one unique public channel:

- `channel`: ID, handle, title, URL, description, verified flag, family-safe / unlisted / noindex signals
- `metrics`: subscribers, videos, lifetime views, plus the exact text YouTube showed (`"2.56M subscribers"`)
- `profile`: country, join date text, keywords, tags, RSS feed URL, tabs, external links, country availability
- `images`: thumbnail, avatar, banner, avatar sizes
- `discoverySource`: which keyword or URL found it, on which surface, at what rank
- `sourceVideo`: the video that surfaced the channel (when it came from video results)
- `score`: the 0-100 opportunity score, explained below
- `activity`: last upload, posting frequency, average views of recent uploads (optional)
- `contacts`: external links and e-mails found in the description (optional)
- `monitor`: new or existing, and the subscriber change since the last run (optional)
- `scrapedAt`: timestamp

Anything YouTube does not publish comes back as `null` or an empty list. The row is still saved. Export as JSON, CSV, Excel, XML or HTML, or read it through the Apify API.

#### 📊 The HTML report

Open the **Report** link on the run's Output tab. It is one self-contained page with no external requests:

- a sortable table with text, subscriber, verified and country filters
- a subscriber histogram and a top-10 country breakdown
- cards for the ten highest-scoring channels
- an **Export CSV** button that exports whatever your filters currently show
- a monitor section (new, changed, missing channels) when comparison is on
- a short notes box listing anything that limited the run, such as a spending cap or channels without a readable About page

It follows your browser's light or dark theme. The report lives in the run's key-value store under the key `REPORT`.

#### ▶️ How to run it

1. Keep the sample keyword or replace it with your own. Add as many as 500, or upload up to 20 TXT/CSV files (one term per line; for CSV, only the first column is read).
2. Optionally paste public `youtube.com/results` URLs to repeat searches you already use.
3. Pick a discovery mode: video results, channel-only search, or both.
4. Set limits and filters.
5. Turn on **activity**, **contacts** or **monitoring** if you want them.
6. Run it, then open the dataset or the report.

**Discovery modes.** *Channel-only search* uses YouTube's channel filter and returns channels directly. *Video results* reads the normal search page and takes the channel behind each video, which tends to surface smaller creators the channel filter buries. *Both* runs the two passes. If you paste a search URL, the mode is read from the URL's own filter when it is recognised; otherwise it is treated as video results.

**One row per channel.** The first match that passes your filters is saved. Later matches for the same channel ID, from another keyword or surface, are ignored, and the saved row keeps its first discovery evidence.

#### ⚙️ Input

| Field | Type | What it does |
| --- | --- | --- |
| `discoveryMode` | enum | `both` (default), `videoResults` or `channelFilter`. Required. |
| `searchTerms` | string list | Topics, products or creator types. Up to 500 including file terms. |
| `searchTermsFiles` | file list | Up to 20 TXT or CSV files. Terms are de-duplicated. |
| `searchUrls` | object list | Public `youtube.com/results` URLs, each as `{ "url": "..." }`. |
| `maxChannelsPerSearchTerm` | integer | Stop a source after this many saved channels. Empty means read until YouTube runs out. |
| `maxTotalResults` | integer | Stop the run after this many saved channels. |
| `minSubscribers` / `maxSubscribers` | integer | Public subscriber range. |
| `keepHiddenSubscribers` | boolean | Keep channels that hide their count even when a subscriber filter is set. Their size score is 0. |
| `minVideos` | integer | Minimum public video count. |
| `minTotalViews` | integer | Minimum lifetime views (from the About panel). |
| `channelCountry` | string | Two-letter code, for example `GB`. Matches the country shown on the About panel. |
| `activeWithinDays` | integer | Keep channels whose newest video is at most this old. Turns activity on automatically. |
| `verifiedOnly` | boolean | Only channels with YouTube's verification badge. |
| `excludedChannels` | object list | `@handle` or `/channel/UC...` URLs to skip. Matched on ID and handle. |
| `sortBy` | enum | `relevance` (default), `subscribers`, `score`, `videos`, `lastVideo` or `views`. |
| `includeActivity` | boolean | Read each channel's Videos tab for posting recency, frequency and average views. One extra request per channel. |
| `recentVideosCount` | integer | 3 to 30 newest videos to average. Default 10. |
| `includeContacts` | boolean | Add e-mails found in the description to the `contacts` block. |
| `compareWithPrevious` | boolean | Monitor mode. Compares with the last complete run. |
| `monitorKey` | string | Names the history. Empty means derived from your sources and filters. |
| `languageHint` / `countryHint` | string | Default `en` / `US`. See the notes below. |
| `maxConcurrency` | integer | 1 to 10 sources in parallel. Default 5. |
| `proxyConfiguration` | proxy | Apify Proxy datacenter by default. Switch to residential if YouTube starts blocking. |

Two things worth knowing about filters. Subscriber, video, view and country filters that need the About panel drop a channel when that figure is not published. And `sortBy` other than `relevance` holds all rows until the run ends, so the dataset fills at the end instead of as it goes.

**Example input**

```json
{
  "discoveryMode": "both",
  "searchTerms": ["coffee roasting"],
  "maxChannelsPerSearchTerm": 5,
  "maxTotalResults": 10,
  "includeActivity": true,
  "includeContacts": true,
  "recentVideosCount": 5,
  "languageHint": "en",
  "countryHint": "US"
}
```

#### 🧾 Output

**Channel fields**

| Field | Type | Notes |
| --- | --- | --- |
| `channel.id` | string | Stable channel ID. |
| `channel.handle`, `title`, `url`, `description` | string or null | Public identity. |
| `channel.isVerified` | boolean | Verification badge in search results. |
| `channel.isFamilySafe`, `isUnlisted`, `isNoindex` | boolean or null | Taken from the channel page when present. |
| `metrics.subscribers` | integer or null | Parsed from the displayed text. |
| `metrics.subscriberText` | string or null | What YouTube showed, for example `150K subscribers`. |
| `metrics.subscriberCountIsExact` | boolean or null | `false` when YouTube shortened the number. |
| `metrics.videos`, `videoText` | integer / string | Public video count. |
| `metrics.totalViews`, `totalViewText` | integer / string | Lifetime views. |
| `profile.country`, `joinedText`, `keywords` | string or null | About panel. Often empty. |
| `profile.tags`, `tabs`, `externalLinks`, `availableCountryCodes`, `ownerUrls`, `alternateUrls` | arrays | Empty when not shown. |
| `profile.rssUrl` | string | Public video feed URL. |
| `images.thumbnailUrl`, `avatarUrl`, `bannerUrl`, `avatarImages` | string / array | `bannerUrl` is null for channels without one. |
| `discoverySource.sourceKind` | string | `keyword` or `search_url`. |
| `discoverySource.value`, `discoverySurface`, `rank`, `sourceUrl` | mixed | `video_results` or `channel_filter`; rank starts at 1. |
| `sourceVideo` | object or null | `videoId`, `title`, `url`, `publishedText`, `viewText`. Null for channel-only matches. |
| `score` | integer or null | 0-100. Null when the About page could not be read. |
| `activity` | object or null | Present only with `includeActivity`. |
| `contacts` | object or null | Present only with `includeContacts`. |
| `monitor` | object or null | Present only with `compareWithPrevious`. |
| `scrapedAt` | string | ISO timestamp. |

**The `activity` block**

| Field | Meaning |
| --- | --- |
| `lastVideoAt` | ISO date of the newest video. Approximate, see the limits section. |
| `lastVideoText` | The age text YouTube showed, such as `3w ago`. |
| `videosPerMonth` | Upload rate estimated from the sampled videos. |
| `avgViewsRecent` | Average views across the sampled videos. |
| `recentSampled` | How many videos were actually read. |

**The `contacts` block** holds `externalLinks` (title and URL for each link on the About panel) and `emails`.

**The `monitor` block** holds `status` (`new` or `existing`), `subscribersPrev` and `subscribersDelta`.

**Example row** (shortened; from a local test run of the input above, with big arrays replaced by `"..."`)

```json
{
  "channel": {
    "id": "UC_t7lJPh4kpIxgdRmTFi1Qg",
    "handle": "@FlairEspresso",
    "title": "Flair Espresso",
    "url": "https://www.youtube.com/@FlairEspresso",
    "description": "...",
    "isVerified": false,
    "isFamilySafe": true,
    "isUnlisted": false,
    "isNoindex": false
  },
  "metrics": {
    "subscribers": 150000,
    "subscriberText": "150K subscribers",
    "subscriberCountIsExact": false,
    "videos": 355,
    "videoText": "355 videos",
    "totalViews": 86775028,
    "totalViewText": "86,775,028 views"
  },
  "profile": {
    "country": "United States",
    "joinedText": "Joined Oct 19, 2016",
    "keywords": "...",
    "tags": "...",
    "rssUrl": "https://www.youtube.com/feeds/videos.xml?channel_id=UC_t7lJPh4kpIxgdRmTFi1Qg",
    "tabs": "...",
    "externalLinks": ["http://www.flairespresso.com", "http://www.instagram.com/flairespressomaker"],
    "availableCountryCodes": "...",
    "ownerUrls": "...",
    "alternateUrls": "..."
  },
  "discoverySource": {
    "sourceKind": "keyword",
    "value": "coffee roasting",
    "discoverySurface": "video_results",
    "rank": 4,
    "sourceUrl": "https://www.youtube.com/results?search_query=coffee+roasting&hl=en&gl=US"
  },
  "sourceVideo": {
    "videoId": "q7aZpkQgbOc",
    "title": "A Beginner's Guide To Coffee Roasting At Home",
    "url": "https://www.youtube.com/watch?v=q7aZpkQgbOc",
    "publishedText": "4y ago",
    "viewText": "290,287 views"
  },
  "score": 77,
  "activity": {
    "lastVideoAt": "2026-09-25T14:29:25+00:00",
    "lastVideoText": "1d ago",
    "videosPerMonth": 2.51,
    "avgViewsRecent": 3040,
    "recentSampled": 5
  },
  "contacts": {
    "externalLinks": [
      { "title": "flairespresso.com", "url": "http://www.flairespresso.com" },
      { "title": "instagram", "url": "http://www.instagram.com/flairespressomaker" }
    ],
    "emails": []
  },
  "monitor": null,
  "scrapedAt": "2026-09-26T14:29:25+00:00"
}
```

#### 🎯 How the score works

The score is a quick way to sort a long list. It is not a prediction of anything. Four parts, 100 points in total:

| Part | Points | How it is earned |
| --- | --- | --- |
| Recent activity | 35 | Newest video within 7 days: 35, within 30: 25, within 90: 15, within 365: 5, older: 0 |
| Audience size | 30 | Grows with the log of subscribers, capped at 30. Roughly 13 at 1K, 21 at 100K, 30 at 10M. Hidden count: 0 |
| Profile completeness | 20 | Description 5, country 3, join date 3, handle 2, banner 3, at least one external link 4 |
| Verification | 15 | YouTube's verification badge |

With `includeActivity` off, the activity part cannot be measured, so the remaining 65 points are rescaled to 100 rather than counted as zero. Scores from a run with activity and a run without are therefore not directly comparable. If a channel's About page could not be read, its score is `null`.

#### 🔁 Monitor mode

Switch on `compareWithPrevious` and schedule the run. Each run is compared with the last complete snapshot, kept in a named store called `yt-channel-search-monitor-<monitorKey>`.

- Rows are tagged `new` or `existing`, with the subscriber difference.
- A `MONITOR_DIFF` JSON record lists **new**, **changed** (subscriber delta) and **missing** channels, and the report shows the same.
- **Missing** means "not in this run's results". It can just as well be a result limit or a filter as a real change, so treat it as a hint. Missing channels are never written to the dataset or charged.
- The snapshot is only overwritten after a run that read every source to the end. A run that stopped early leaves the old snapshot alone, so the next comparison is not distorted.

Leave `monitorKey` empty and identical searches share one history automatically. Set it yourself if you want two setups to share a history or keep apart.

#### 🛡️ Reliability

- Rows are saved as they are found (unless you choose a sort order, in which case they are flushed sorted when the run ends, including when it is stopped early).
- Failed requests are retried up to four times with growing pauses, and the proxy session is swapped on blocks, rate limits, server errors and cookie-consent pages.
- One broken source does not sink the run. It is noted in the report and the others carry on.
- A checkpoint is written every 20 channels, so a restart or server migration does not charge for the same channel twice.
- The run reserves 30 seconds before its timeout to write the dataset tail, the report and the monitor diff.
- The Actor honours your maximum spend and stops at it.

#### 💳 Pricing

Pay per event: **$0.0005 per result row saved to the dataset**, which is **$0.50 per 1,000 results**. Every channel written to the dataset counts as one result. Channels removed by your filters and repeat matches inside a run never reach the dataset, so they are not charged. Activity, contacts, score, report and monitoring add no extra per-result charge.

Each run also has a small one-time start fee of **$0.0005** (one per GB of memory the run uses, at least one).

Platform usage (compute, proxy) is billed separately according to your Apify plan.

#### 🚧 Limits

Read these before you rely on a field.

- **E-mail is often missing.** The Actor reads only e-mail addresses written in plain text in the channel description. The one YouTube hides behind "View email address" needs a signed-in human and is not reachable here. Expect many channels to return an empty `emails` list.
- **`country`, `bannerUrl` and `keywords` are frequently null.** Channels simply do not fill them in.
- **Subscriber counts are rounded** by YouTube above 1,000. Check `subscriberCountIsExact`.
- **Last-upload dates are approximate.** YouTube shows "3 weeks ago", not a date, so `lastVideoAt` is a rounded estimate.
- **Channels with hidden subscriber counts** have `subscribers: null`. A subscriber filter drops them unless `keepHiddenSubscribers` is on.
- **This is not a video scraper.** It does not return video lists, comments or transcripts. You only get the one source video that surfaced a channel.
- **No analytics or revenue.** Only what any visitor can see.
- **`languageHint` should stay `en`.** Counts and relative dates are parsed in English. Other languages make those fields unreadable.
- **Avoid EU country hints.** They can trigger YouTube's cookie-consent page. The Actor sends consent cookies and rotates sessions, but a non-EU hint like `US` is safer.
- **YouTube changes its pages.** If the search page turns into something this Actor cannot read, it degrades and reports it rather than pretending. Fewer rows than requested is normal: YouTube caps and localises results, and your filters cut more.

#### ❓ FAQ

##### Do I need a YouTube API key?

No. It reads public search and channel pages. No login, cookies or key.

##### Can I paste search URLs instead of keywords?

Yes. Add `youtube.com/results` URLs on their own or together with keywords and uploaded lists.

##### Can the same channel show up twice?

No. Each channel ID is saved once per run, with the evidence from its first match.

##### How do I find small but active channels in one country?

Set `maxSubscribers`, `channelCountry` and `activeWithinDays`, use `both` discovery mode, and sort by `score`. Keep in mind that channels with no published country are dropped when `channelCountry` is set.

##### Why is my score different with activity on and off?

Without activity the score is rescaled from 65 to 100 points. Compare scores only within runs that use the same setting.

##### Will it find every email?

No. See the limits above. It finds what is written in the description, nothing else.

##### Why did my run return fewer channels than the limit?

Filters, the spending cap, and YouTube's own search caps all reduce it. The report's notes box says which one applied when it can tell.

##### Does it return videos?

No. Only the source video that first surfaced a channel, if any.

##### How do I stop paying for a run that grows?

Set a maximum spend on the run, or use `maxTotalResults`. The Actor stops at either.

#### 📝 Changelog

**v0.1**

- Keyword and search-URL discovery through video results, the channel filter, or both.
- Public profile, metrics and first-match evidence per channel.
- Filters: subscribers, videos, lifetime views, country, verified, posting recency, exclusions, hidden-count handling.
- 0-100 opportunity score, activity metrics and contacts.
- Standalone HTML report and run-to-run monitoring.
- Checkpoints, retries with session rotation, and saved-row protection on early stops.

**v0.2**

- Input form reorganised into Search, Filters, Enrichment, Monitoring and Performance sections, with clearer titles and descriptions.
- Pre-filled test run: one keyword and a cap of 10 channels, so a first run costs a few cents.
- Billing fix: rows are now charged through the standard per-result event ($0.0005 per result, plus the $0.0005 start fee). Prices did not change. The spending-limit stop uses the same event.

#### 🆘 Support

Something off, or a field you need? Open an issue from the Actor's **Issues** tab with a run link and a short description.

#### 🔗 Related

The publisher's Store profile lists other YouTube and lead-generation Actors, useful when you already have a channel list and want more than a search can give.

# Actor input Schema

## `searchTerms` (type: `array`):

Topics, products or kinds of creators to look for, one per line. Terms typed here and terms from uploaded files can total up to 500.

## `searchTermsFiles` (type: `array`):

Up to 20 TXT or CSV files with one search term per line. For CSV only the first column is read. Terms are de-duplicated against the list above, and the combined total is capped at 500.

## `searchUrls` (type: `array`):

Public youtube.com/results URLs for searches you want to repeat. The surface is read from the URL's own sp filter when it is one this Actor recognises, otherwise the URL is treated as video results.

## `discoveryMode` (type: `string`):

Which YouTube search surface to read. 'Channel-only search' uses YouTube's channel filter and returns channels directly. 'Video results' reads the normal search page and takes the channel that owns each video, which surfaces smaller creators the channel filter hides. 'Both' runs the two passes over every source.

## `maxChannelsPerSearchTerm` (type: `integer`):

Stop a source after this many channels have been saved from it. Channels dropped by filters or already seen do not count. Leave empty to read each source until YouTube runs out of results.

## `maxTotalResults` (type: `integer`):

Stop the whole run after this many channels have been saved. Channels dropped by filters do not count. Leave empty to read every source to the end.

## `minSubscribers` (type: `integer`):

Keep only channels at or above this public subscriber count. Channels that hide their count are dropped unless 'Keep channels with a hidden subscriber count' is on.

## `maxSubscribers` (type: `integer`):

Keep only channels at or below this public subscriber count. Works together with the minimum.

## `minVideos` (type: `integer`):

Keep only channels with at least this many public videos. The count comes from the channel's About panel, so a channel whose count YouTube does not publish is dropped.

## `verifiedOnly` (type: `boolean`):

Keep only channels YouTube marks with the verification badge in search results.

## `excludedChannels` (type: `array`):

Channel URLs to leave out, as https://www.youtube.com/@Handle or https://www.youtube.com/channel/UC.... Matching is on the channel ID and the handle, not on the URL text.

## `channelCountry` (type: `string`):

Two-letter country code, such as GB. Kept only when the channel publishes that country on its About panel; channels with no published country are dropped.

## `minTotalViews` (type: `integer`):

Keep only channels with at least this many lifetime views. The figure comes from the About panel, so channels that hide it are dropped.

## `activeWithinDays` (type: `integer`):

Keep only channels whose newest video is no older than this. YouTube publishes ages as text ('3 weeks ago'), so the date is approximate. Setting this turns on 'Include posting activity' automatically.

## `keepHiddenSubscribers` (type: `boolean`):

By default a subscriber filter drops channels whose count YouTube does not publish. Turn this on to keep them; their size score is then 0.

## `sortBy` (type: `string`):

Anything other than 'Discovery order' holds the rows until the run ends so they can be sorted, so results appear in the dataset only at the end.

## `includeActivity` (type: `boolean`):

Read the channel's Videos tab to work out how recently and how often it posts and the average views of its newest uploads. Costs one extra request per channel and is required for the activity part of the score.

## `recentVideosCount` (type: `integer`):

How many of the newest videos to average when 'Include posting activity' is on.

## `includeContacts` (type: `boolean`):

Collect e-mail addresses written in plain text in the channel description. Addresses YouTube hides behind 'Sign in to see email address' are not reachable and are never guessed. External links are always returned, with or without this option.

## `compareWithPrevious` (type: `boolean`):

Turn this run into a monitor: each channel is marked new or existing against the last snapshot, subscriber changes are reported, and channels missing from this run are listed in the report.

## `monitorKey` (type: `string`):

Names the snapshot store, as yt-channel-search-monitor-<key>. Leave empty to derive it from the sources and filters, which keeps identical searches on the same history.

## `languageHint` (type: `string`):

Two-letter YouTube language hint. Leave it at en: relative dates like '3 weeks ago' and counts like '2.56M' are parsed in English, and another language makes those fields unreadable.

## `countryHint` (type: `string`):

Two-letter YouTube country hint, such as US. A country inside the EU can make YouTube answer with a cookie-consent page instead of results, so a non-EU hint is safer.

## `maxConcurrency` (type: `integer`):

How many sources to read at the same time. Pages inside one source are always read in order.

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

Datacenter proxies are enough for most runs. If YouTube answers with consent pages or 429 errors, switch to residential.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee roasting"
  ],
  "discoveryMode": "both",
  "maxChannelsPerSearchTerm": 10,
  "maxTotalResults": 10,
  "verifiedOnly": false,
  "keepHiddenSubscribers": false,
  "sortBy": "relevance",
  "includeActivity": false,
  "recentVideosCount": 10,
  "includeContacts": false,
  "compareWithPrevious": false,
  "languageHint": "en",
  "countryHint": "US",
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}
```

# Actor output Schema

## `channels` (type: `string`):

Every saved channel with identity, public metrics, profile facts, images, discovery evidence and the 0-100 score. Download as JSON, CSV or XLSX, or read through the API.

## `report` (type: `string`):

Standalone report for the run: sortable channel table with filters, subscriber histogram, country breakdown, top channels by score, CSV export and the monitor diff when there is one. Open it in a browser.

## `monitorDiff` (type: `string`):

New, changed and missing channels against the previous snapshot. Written only when 'Compare with the previous run' is on.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "coffee roasting"
    ],
    "discoveryMode": "both",
    "maxChannelsPerSearchTerm": 10,
    "maxTotalResults": 10,
    "includeActivity": false,
    "recentVideosCount": 10,
    "includeContacts": false,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": []
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("solalab_digital/youtube-channel-search").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "searchTerms": ["coffee roasting"],
    "discoveryMode": "both",
    "maxChannelsPerSearchTerm": 10,
    "maxTotalResults": 10,
    "includeActivity": False,
    "recentVideosCount": 10,
    "includeContacts": False,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": [],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("solalab_digital/youtube-channel-search").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "coffee roasting"
  ],
  "discoveryMode": "both",
  "maxChannelsPerSearchTerm": 10,
  "maxTotalResults": 10,
  "includeActivity": false,
  "recentVideosCount": 10,
  "includeContacts": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}' |
apify call solalab_digital/youtube-channel-search --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/v6Ts7ovuMDJ8B6g6D/builds/ZJob5lvuzozH8q79i/openapi.json
