YouTube Data API skill for Codex
Channel automation that fits in 10,000 units. Codex lists a channel's videos through its uploads playlist and batches statistics 50 IDs per call, so a full export costs dozens of quota units instead of thousands spent on search. Uploads use resumable OAuth with private-by-default status, metadata edits fetch and resend whole snippets after you confirm the diff, and errors such as quotaExceeded are handled with checkpoints.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- youtube-data-api Installs to ~/.agents/skills/youtube-data-api/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install youtube-data-api The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project youtube-data-api Installs to .agents/skills/youtube-data-api/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/youtube-data-api.zipunzip -o youtube-data-api.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) youtube-data-api PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $youtube-data-api, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Automates YouTube with the YouTube Data API v3 in Python - list a channel's uploads via the uploads playlist instead of search, batch video statistics 50 IDs at a time, read comments, resumable uploads with OAuth, playlists and metadata updates - while budgeting the 10,000-unit daily quota and following API policy. Use when the user wants to export channel or video stats, bulk-update titles or descriptions, upload or schedule videos from a script, pull comments, or fix quotaExceeded and 403 errors.
Use it when
- You need channel or video data exported or refreshed on a schedule
- Videos should be uploaded, scheduled or updated in bulk from a script
- Comments or playlists need to be read or managed programmatically
- A YouTube script fails with quotaExceeded or 403 errors
Use something else when
- You need to process the video files themselves: use ffmpeg-video
- You need generic Google OAuth setup across Workspace APIs: use google-workspace-apis
How to check the result
Count API calls times unit cost and compare with the Cloud console quota graph, confirm exported IDs match the channel's video count, and re-fetch edited videos to diff against intended values.
The mistake it prevents
Using search.list to enumerate a channel's videos, which costs 100 units per page and still misses videos.
Done well means: the job fits comfortably inside the daily quota, uses API keys for public reads and OAuth only where a channel owner must consent, every write (upload, metadata change, comment) was confirmed by the user, and stored data follows YouTube’s developer policies.
Quota first
Default projects get 10,000 units per day, reset at midnight Pacific time. Cost is per request, not per item, and failed requests still cost at least 1 unit.
| Call | Approx. units | Use it for |
|---|---|---|
channels.list, videos.list, playlistItems.list, playlists.list, commentThreads.list | 1 | Almost everything read-side |
search.list | 100 | Only real keyword search; never to list a channel’s videos |
videos.update, playlists.insert, playlistItems.insert, comments.insert | 50 | Writes |
thumbnails.set | 50 | Custom thumbnails (channel must be verified) |
captions.list / captions.download | 50 / 200 | Owner-only caption access |
videos.insert (upload) | High, see the quota calculator | Uploads; budget a handful per day by default |
Check Google’s “Quota Calculator” page for current values before planning a large job; costs have changed over time. Request a quota increase through the YouTube API audit form, which requires compliance with the developer policies.
Quick estimate: exporting stats for a 2,000-video channel costs about 40 playlist pages
plus 40 videos.list batches = ~80 units. The same job via search.list would cost
4,000+ units and still miss videos (search results are capped and not exhaustive).
Credentials
| Need | Auth |
|---|---|
| Public data (any channel’s videos, stats, public comments) | API key, restricted to the YouTube Data API |
| The user’s own channel: uploads, private videos, edits, captions | OAuth 2.0 client (Desktop app type) with the narrowest scope |
Scopes: youtube.readonly (read), youtube.upload (upload only), youtube (manage),
youtube.force-ssl (needed for comments and captions writes). Store the refresh token
outside the repo; never print tokens. Creating the Cloud project, enabling the API and
the OAuth consent screen happen in the Google Cloud console and need the user.
pip install google-api-python-client google-auth-oauthlib # needs network
Read: every upload of a channel with stats
import os
from googleapiclient.discovery import build
yt = build("youtube", "v3", developerKey=os.environ["YT_API_KEY"])
ch = yt.channels().list(part="contentDetails,statistics", forHandle="@SomeChannel").execute()
if not ch.get("items"):
raise SystemExit("channel not found")
uploads = ch["items"][0]["contentDetails"]["relatedPlaylists"]["uploads"]
ids: list[str] = []
req = yt.playlistItems().list(part="contentDetails", playlistId=uploads, maxResults=50)
while req is not None:
resp = req.execute()
ids += [it["contentDetails"]["videoId"] for it in resp["items"]]
req = yt.playlistItems().list_next(req, resp)
rows = []
for i in range(0, len(ids), 50): # videos.list takes up to 50 IDs
resp = yt.videos().list(part="snippet,statistics,contentDetails",
id=",".join(ids[i:i + 50]),
fields="items(id,snippet(title,publishedAt),"
"statistics(viewCount,likeCount,commentCount),"
"contentDetails(duration))").execute()
for v in resp["items"]:
s = v.get("statistics", {})
rows.append({
"id": v["id"], "title": v["snippet"]["title"],
"published": v["snippet"]["publishedAt"],
"duration": v["contentDetails"]["duration"], # ISO 8601, e.g. PT4M13S
"views": int(s.get("viewCount", 0)),
"likes": int(s["likeCount"]) if "likeCount" in s else None, # hidden likes
"comments": int(s["commentCount"]) if "commentCount" in s else None,
})
fieldstrims the response (less bandwidth; same quota cost).- Statistics are strings; convert. Missing keys mean hidden or disabled, not zero.
- Deleted or private videos stay in the uploads playlist for the owner but return nothing
from
videos.listfor others; compare counts and report the gap. - Watch time, audience retention and revenue are not in the Data API. Use the YouTube
Analytics API (
youtubeAnalyticsv2,reports.query) with the owner’s OAuth consent.
Write: upload and metadata (OAuth)
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.http import MediaFileUpload
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
creds = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES).run_local_server(port=0)
yt = build("youtube", "v3", credentials=creds)
body = {
"snippet": {"title": title, "description": desc, "tags": tags, "categoryId": "28"},
"status": {"privacyStatus": "private", "publishAt": "2026-11-03T15:00:00Z",
"selfDeclaredMadeForKids": False},
}
media = MediaFileUpload("render.mp4", mimetype="video/mp4", chunksize=-1, resumable=True)
req = yt.videos().insert(part="snippet,status", body=body, media_body=media)
response = None
while response is None:
status, response = req.next_chunk() # retry 5xx here with backoff
if status:
print(f"{int(status.progress() * 100)}%")
print("uploaded", response["id"])
- Uploading, scheduling, changing titles or posting comments is publishing on the user’s
channel: show the exact metadata and get explicit confirmation before each run, and
default new uploads to
private. publishAtonly works withprivacyStatus: private; YouTube flips it public at that time.- Videos uploaded through an unverified API project are locked to private. The project must pass Google’s audit before public uploads work.
videos.updatereplaces every field in the parts you send: fetch the currentsnippet, modify it, and send it back whole, or you wipe tags and description.categoryIdis required insnippetupdates.- Bulk metadata edits: build a CSV of old and new values, show the diff, confirm, then apply with a log of video IDs so the change can be reverted.
Errors
| Error | Meaning | Action |
|---|---|---|
403 quotaExceeded | Daily quota spent | Stop; resume after the Pacific midnight reset; checkpoint progress |
403 forbidden / insufficientPermissions | Wrong scope or not the owner | Re-consent with the right scope |
403 commentsDisabled, videoNotFound (404) | Per-item condition | Skip the item, log it, continue |
400 invalidCategoryId, invalidTitle | Bad metadata (titles max 100 chars, no < or >) | Fix input |
| 401 | Expired or revoked token | Refresh; if revoked, the user must authorise again |
5xx, backendError | Transient | Exponential backoff, then retry |
Policy points that change designs
- The YouTube API Services Terms and Developer Policies limit how long API data may be stored without refreshing and require deleting data when users revoke access. Re-fetch stored statistics on a schedule rather than archiving them forever, and read the current policy before building a long-term dataset.
- Do not scrape YouTube pages to get around quota; it violates the terms.
- Show YouTube attribution and link to the video when displaying API data publicly.
Gotchas
- Handles (
@name) resolve withforHandle; legacy usernames withforUsername; channel IDs start withUC. The uploads playlist ID is usually the channel ID withUUprefix, but read it fromcontentDetailsrather than deriving it. search.listresults are approximate and capped at roughly 500 per query; it is not a listing API.commentThreads.listreturns top-level comments with up to 5 replies; fetch the rest withcomments.list(parentId=...).- Durations are ISO 8601 (
PT1H2M3S); Shorts are not flagged explicitly in the API. - Quota is per Cloud project, shared by every script using that key.
Verification
- Log units spent per run (count calls x cost) and compare with the Cloud console quota graph after the first run.
- For exports, the number of IDs equals the channel’s video count shown in YouTube Studio (minus private videos when using an API key).
- For writes, re-fetch the updated videos and diff against the intended values; uploads start private, so check processing status in Studio before announcing anything.
Run report
**Job:** export uploads of @SomeChannel with stats -> data/videos-2026-10-28.csv
**Calls:** 41 playlistItems.list + 41 videos.list = 82 units (of 10,000)
**Result:** 2,031 videos; 4 with hidden like counts; 0 errors
**Writes:** none What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "YouTube Data API"
short_description: "Channel automation that fits in 10,000 units"
default_prompt: "$youtube-data-api Export every video on our channel with views, likes and duration to a CSV" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.