YouTube Email Scraper for extracting a channel's public contact record: business email, website, social profiles and About links from YouTube.com channels. This repo has a free YouTube contact web scraping script you can run right now, and a YouTube channel contact API that returns the public contact block for one channel.
This is the channel-contact endpoint on its own. The YouTube Scraper repo covers the rest of the surface: video, transcript, search, playlists, comments and Shorts.
This repo reads public brand, media and institutional channels only. Every capture and screenshot on this page targets an organisation channel (@NASA, @TED, @Microsoft). It is not tooling for profiling a private individual.
Last updated: 2026-07-20. Working against YouTube.com as of July 2026, and re-verified whenever YouTube changes their markup.
Every JSON block on this page was captured from the live API on 2026-07-20. Long arrays are trimmed and each block says exactly what was cut; the fields shown are verbatim. Full uncut samples are committed in youtube_email_scraper_api_data/. Every code example calls the actual API and is runnable from youtube_email_scraper_api_codes/.
pip install requests
export CHOCODATA_API_KEY="your_key" # free: 1,000 requests, one-time, no card
python youtube_email_scraper_api_codes/contact.py @NASAThose lines return this, live from YouTube.com:
{
"channel_name": "NASA",
"channel_id": "UCLA_DiR1FfKNvjuUpBHmylQ",
"email_count": 0,
"links_count": 6,
"links": [{ "title": "nasa.gov", "url": "https://www.nasa.gov/", "from": "links_section" }],
"reveal_available": false
}Point it at a list of handles and you get one contact row each:
What this endpoint gives you, and what populates. It reliably returns the channel's public contact block: the channel id and name, and every link in the About section (the website, the social profiles, and any booking or media-request page). It also reads a business email when the creator typed one in plain text in the description or a link row. Across 43 public brand, media and creator channels sampled on 2026-07-20, 4 returned a public email and 39 did not, so this page leads with the links, which populate on almost every channel.
- Free YouTube Contact Scraper
- Avoid getting blocked when scraping YouTube channels
- YouTube Email Scraper API reference
- Qualify a list of YouTube channels by contact route
- Measured latency
- License
YouTube server-renders a channel's About page into a JavaScript object called ytInitialData, so you can read the public contact surface without a headless browser or a login. No key, no cost:
python free_scraper/youtube_email_free_scraper.py @NASA
python free_scraper/youtube_email_free_scraper.py https://www.youtube.com/@TEDSource: free_scraper/youtube_email_free_scraper.py. It brace-counts ytInitialData out of the HTML, reads channelMetadataRenderer for the id, name and description, walks every channelExternalLinkViewModel for the About links, and runs the same plain-text email parse the API uses.
After running the command, your terminal should look something like this:
It works. @NASA returns the channel id, name and 6 About links from a plain requests.get, no login. What it does not get is the business email, and that is not a bug in the script, it is where YouTube put the data. The next section is about that.
The About page loads fine for a cold client. The hard parts are where the data lives and how the links are stored, and all of them are measured against YouTube on 2026-07-20.
First, the "business email" is not in the page. YouTube hides a channel's business email behind a "View email address" button that triggers a reCAPTCHA and requires a logged-in session, so the address is never in the About page HTML. No HTML scraper, free or paid, gets it. The reveal_available: false field says so explicitly. What this endpoint returns in the emails array is only what a creator additionally typed in plain text in the description or a link row, which is why it populates on a minority of channels.
Second, the About "Links" URLs are redirect-wrapped. Each link row stores its target as a YouTube /redirect?...&q=<url-encoded destination> wrapper, not the real URL. A scraper that reads the visible display text gets a bare domain with no scheme (nasa.gov), and one that reads the raw href gets YouTube's redirect URL. The real destination has to be pulled out of the q= parameter and URL-decoded, which is what the url field already contains.
Third, the handle has to resolve. A channel is addressable by @handle, channel id (UC...), legacy /c/ or /user/ path, or a full URL, and the API accepts all of them. But a wrong or renamed handle returns a 404, not an empty result: in the sample above, two well-known brands answered 404 on the obvious @handle because their real handle differs. Confirm the handle before a bulk run.
Here is the full picture, and what each item costs you:
| What bites you | Why | What it costs you |
|---|---|---|
| The business email is reCAPTCHA-gated | It sits behind a "View email address" button and a login, never in the HTML. | No HTML scraper reaches it; emails only carries plain-text addresses a creator published. |
| Most brand channels publish no plain-text email | 39 of 43 sampled returned an empty emails array. |
Email coverage is a minority; the About links are the reliable contact route. |
Link URLs are /redirect wrappers |
The real target is url-encoded in a q= param. |
Read the display text and you get a scheme-less bare domain; the url field is already resolved. |
A wrong handle returns 404 |
Handles get renamed and are case- and form-sensitive. | A bulk run silently drops channels unless you handle the 404 and confirm handles. |
| The About page is per-IP rate limited | Fetching many channels from one address gets throttled. | Bulk contact work needs IP rotation you would otherwise build and maintain. |
So the two paths, side by side, same channels, same day:
Both read the same public About page and neither reaches the gated business email. The difference is that the API resolves the redirect-wrapped link URLs, adds the links a creator drops into the description text (on @TED, one extra), and handles the per-IP throttle for a bulk run.
The managed option, and the one this repo is built around: the Chocodata YouTube Email Scraper API. One GET request per channel returns the public contact record: id, name, the resolved About links tagged by where they came from, and any plain-text email, with a ~99% success rate and no proxy management. Free for the first 1,000 requests.
Below is the YouTube Email Scraper API reference to get you started: authentication, the error bodies, the rate limits, and the endpoint itself.
curl "https://api.chocodata.com/api/v1/youtube/email?api_key=YOUR_KEY&channel=@NASA"import requests
r = requests.get(
"https://api.chocodata.com/api/v1/youtube/email",
params={"api_key": "YOUR_KEY", "channel": "@NASA"},
timeout=90,
)
c = r.json()
print(c["channel_name"], c["links_count"], c["email_count"])
# NASA 6 0After running the command, your terminal should look something like this:
Pass your key as the api_key query parameter. There is no header form and no OAuth step.
https://api.chocodata.com/api/v1/youtube/email?api_key=YOUR_KEY&channel=@NASA
A free key is 1,000 requests, one time, with no card. Get one at chocodata.com.
Nothing below is billed: you are only charged on a 2xx.
| Status | error code |
Meaning | Billed | What to do |
|---|---|---|---|---|
400 |
invalid_params |
channel was not supplied. The body names the missing param and its path. |
no | Add channel. |
401 |
INVALID_API_KEY |
Key missing, unrecognised, or revoked. | no | Check api_key. Get one at chocodata.com. |
402 |
INSUFFICIENT_CREDITS |
Balance exhausted. | no | Top up or upgrade. |
404 |
item_not_found |
No channel exists for that handle or id. | no | Check the @handle or channel id. |
429 |
RATE_LIMITED |
Over 120 requests/60s, or over your plan's concurrency. | no | Back off and retry. |
A bad key, verbatim:
curl "https://api.chocodata.com/api/v1/youtube/email?api_key=totally_invalid_key&channel=@NASA"{"error": {"code": "INVALID_API_KEY", "message": "Api key not recognised."}}A missing required param, verbatim. Note that it names the real parameter, channel:
{"error": "invalid_params", "issues": [{"code": "invalid_type", "expected": "string", "received": "undefined", "path": ["channel"], "message": "Required"}]}Two response shapes exist: auth and billing errors nest under error.code (uppercase), while parameter and scrape-layer errors are flat with a lowercase error string.
The scripts in this repo turn each of these errors into an actionable message, so a typo'd key does not hand you a stack trace:
Two separate limits apply, and they are enforced independently.
| Limit | Value |
|---|---|
| Requests per key | 120 per 60 seconds (sliding window) |
| Concurrent requests, Free | 10 |
| Concurrent requests, Vibe | 30 |
| Concurrent requests, Pro | 50 |
| Concurrent requests, Custom | 100 |
Exceed either and you get 429, not a queue. Every call is a synchronous GET: there is no webhook, callback, or async job to poll. The examples use timeout=90 for headroom.
Fan out with a thread pool, sized to stay inside both limits at once:
from concurrent.futures import ThreadPoolExecutor
import requests
def one(channel):
r = requests.get("https://api.chocodata.com/api/v1/youtube/email",
params={"api_key": KEY, "channel": channel}, timeout=90)
return r.json() if r.status_code == 200 else None
with ThreadPoolExecutor(max_workers=4) as pool:
contacts = [c for c in pool.map(one, channels) if c]The public contact record for one channel: id, name, the About links tagged by source, any plain-text email, and the reveal_available flag.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
channel |
string | yes | - | @handle, channel id (UC...), /c/ or /user/ path, or a full channel URL. A wrong or renamed handle returns 404. |
country |
string | no | US |
ISO-3166 alpha-2 egress country hint. |
api_key |
string | yes | - | Your key. Query parameter, not a header. |
curl "https://api.chocodata.com/api/v1/youtube/email?api_key=YOUR_KEY&channel=@NASA"Real response. links cut to 2 of 6; links_count is the real total, and every other field is verbatim (full sample):
{
"channel": "NASA",
"channel_id": "UCLA_DiR1FfKNvjuUpBHmylQ",
"channel_name": "NASA",
"emails": [],
"email_count": 0,
"links": [
{ "title": "nasa.gov", "display": "nasa.gov", "url": "https://www.nasa.gov/", "from": "links_section" },
{ "title": "X", "display": "x.com/nasa", "url": "https://x.com/nasa", "from": "links_section" }
],
"links_count": 6,
"source": "public_description",
"reveal_available": false
}links is the field most people actually use, because it is the contact route on a brand channel: the website, the social profiles, and any booking or media page. A few things worth knowing, all measured on the channels sampled on 2026-07-20:
emailsis populated only when a creator publishes an address in plain text. Across 43 public brand, media and creator channels sampled, 4 returned a public business email in this array and 39 returned[]; the reCAPTCHA-gated "View email address" address is never included, whichreveal_available: falsesignals.urlis the resolved destination, not YouTube's redirect wrapper. The API pulls the real target out of the/redirect?...&q=parameter and URL-decodes it, sourlis ready to use whiledisplaykeeps the scheme-less text YouTube shows.fromtags where each link came from:links_sectionis the structured About "Links" widget;descriptionis a URL or social handle parsed out of the description blurb. The second sample shows both.channel_idis the stable identifier. Handles get renamed; theUC...id does not, so store that if you are tracking a channel over time.
A second committed sample, email_ted.json, is @TED. It shows a Media Requests link (the real contact route for a media organisation) and a from: "description" link parsed out of the description on top of the structured widget:
{
"channel": "TED",
"channel_name": "TED",
"emails": [],
"email_count": 0,
"links": [
{ "title": "Media Requests", "display": "media-requests.ted.com", "url": "https://media-requests.ted.com/", "from": "links_section" },
{ "title": null, "display": "www.ted.com/about/our-organization/our-policies-terms/ted-talks-usage-policy", "url": "https://www.ted.com/about/our-organization/our-policies-terms/ted-talks-usage-policy", "from": "description" }
],
"links_count": 4,
"source": "public_description",
"reveal_available": false
}Runnable: youtube_email_scraper_api_codes/contact.py
The job most people arrive with: a shortlist of channel handles (a sponsorship pipeline, a partner list, a competitor set) and a need for one row each with the contact route filled in: website, socials, and a public email where the creator published one.
export CHOCODATA_API_KEY="your_key"
python youtube_email_scraper_api_codes/qualify_channels.pyIt reads channels.txt if present, one @handle or channel URL per line, otherwise runs the sample list. Four workers keep it inside both the concurrency cap and the 120 requests/60s limit, and it writes channels_contacts.csv with the website and link count per channel:
Source: youtube_email_scraper_api_codes/qualify_channels.py
Ten consecutive calls to /youtube/email for the same channel, 2 seconds apart, on 2026-07-20. Nine returned 200 and are timed below; the sixth returned a retryable 502:
| Metric | Value |
|---|---|
| calls attempted | 10 |
200 responses |
9 |
| min | 1,319 ms |
| median | 2,866 ms |
| max | 3,980 ms |
Ten calls is a small sample. One returned a retryable 502, which is why a bulk run should retry once before giving up on a channel.
MIT. See LICENSE.







