oytc is a read-only CLI for public YouTube Data API v3 resources and the authorized
user's YouTube Analytics. Public-data commands use an API key; analytics commands require
OAuth. (The default OAuth grant covers only analytics — it is used for public reads only
when it also includes the YouTube Data read-only scope.) This page documents every
command, shared flags, output formats, pagination, configuration, and error behavior.
Help output is always authoritative: run oytc --help, oytc <command> --help, or
oytc <command> <subcommand> --help for the flags supported by your installed version.
- Command tree
- Authentication commands
- Analytics
- Search
- Channels
- Videos
- Playlists
- Comments
- Subscriptions
- Metadata lists
- Agent skill installation
- Version and self-update
- Output
- Pagination and quota
- Configuration and precedence
- Errors and exit codes
- Read-only limitations
oytc login [--oauth]
oytc status [--check]
oytc logout
oytc analytics report --metrics <CSV>
oytc analytics overview [--by day|month]
oytc analytics video <VIDEO_ID>
oytc analytics traffic-sources
oytc analytics demographics
oytc search [QUERY]
oytc channel get <REFERENCE>...
oytc channel activities <CHANNEL>
oytc channel sections <CHANNEL>
oytc channel sections --id <SECTION_IDS>
oytc channel uploads <CHANNEL>
oytc video get <VIDEO_ID>...
oytc video stats <VIDEO_ID>...
oytc video popular
oytc video trainability <VIDEO_ID>
oytc playlist get <PLAYLIST_ID>...
oytc playlist list --channel <CHANNEL_ID>
oytc playlist items <PLAYLIST_ID>
oytc comment get <COMMENT_ID>...
oytc comment replies <PARENT_COMMENT_ID>
oytc comment threads --video <VIDEO_ID>
oytc comment threads --channel <CHANNEL_ID>
oytc comment threads --id <THREAD_IDS>
oytc subscription list --channel <CHANNEL_ID>
oytc subscription list --id <SUBSCRIPTION_IDS>
oytc category list (--region <REGION> | --id <CATEGORY_IDS>)
oytc language list
oytc region list
oytc skills install # `oytc skill install` also works
oytc version
oytc update [--check] [--version vX.Y.Z] # `oytc upgrade` is an alias
Without flags, prompts for a YouTube Data API key without terminal echo, validates it with
a cheap i18nLanguages.list call, and atomically writes it as JSON to the config path. A
key can also be piped on standard input for non-interactive secret injection; there is
deliberately no --api-key flag, so the key never appears in shell history or the process
table. The login prompt and login --help link to the API key setup guide.
oytc login # interactive
some-secret-manager get yt | oytc login # pipedIf OYTC_API_KEY is set, login still saves the file but notes that the environment
variable remains the active, higher-precedence credential.
oytc login --oauth runs a PKCE-protected loopback-browser flow and requests the
read-only yt-analytics.readonly scope. Without an API key saved in auth.json, it also
requests youtube.readonly for Data API reads (that scope is sensitive; see
OAuth setup
for the unverified-app caveat). It prints the authorization URL as a
headless fallback, stores access/refresh tokens in the same protected file, and preserves
an existing API key. Client credentials come from OYTC_OAUTH_CLIENT_ID and
OYTC_OAUTH_CLIENT_SECRET when set; missing values are prompted (the secret without echo).
The prompt and login --help link to OAuth setup.
Local-only by default. It reports the API key's source and SHA-256 fingerprint and the
OAuth client ID, granted scopes, and token expiry — never the key, tokens, or client secret.
--check validates each configured credential and may refresh/persist an expiring OAuth
token. Even when no credential is configured, status --check --format json prints the
status object before exiting 3, so scripts can inspect what is missing.
Attempts to revoke the stored OAuth refresh token, continues if revocation fails, then
idempotently removes the complete credential file. It warns when OYTC_API_KEY remains set.
All analytics commands query ids=channel==MINE and require oytc login --oauth. Dates
are inclusive YYYY-MM-DD; both default to the last 28 complete UTC days (ending
yesterday). Every command accepts:
| Flag | Meaning |
|---|---|
--start YYYY-MM-DD / --end YYYY-MM-DD |
inclusive report range |
--filters EXPR |
YouTube Analytics filter expression |
--sort CSV |
sort fields, prefix descending fields with - |
--page-size N |
rows per Analytics request, 1–200 (default 200) |
--all |
follow Analytics pages until the report is complete |
--limit N |
cap total rows (0 = no cap; default 0) |
--start-index N |
resume at a 1-based row index (default 1) |
Without --all, Analytics fetches one page. A full time-grouped page may omit
rows, so it sets completionUncertain: true and advises rerunning with --all
instead of offering an unsafe index. Other full pages may include nextStartIndex
for API-index resumption. Table/TSV/JSONL output prints safe continuation hints
on stderr unless --quiet is set. --page-size is capped at 200 by the API.
With --all, reports grouped by day or month use smaller date windows because
the API can return an empty offset page while omitting rows from a long report.
These results are ordered by date (descending with --sort=-day or --sort=-month),
and --start-index refers to that order. Other sorts cannot be preserved across
date windows and are rejected before a request. Reports without a time dimension
use the API's row index. If the API ends that traversal after a full page and
completeness cannot be verified, JSON includes completionUncertain: true and the
CLI prints a warning on stderr. Narrow the date range or filters to verify coverage.
For a month dimension, --start and --end select whole calendar months:
January 20 through March 22 requests January through March. The CLI sends the
first day of each month because the Analytics API requires those bounds.
Google's metric/dimension compatibility rules are authoritative. Incompatible combinations are returned verbatim as API errors.
Raw report command. --metrics CSV is required; --dimensions CSV is optional.
oytc analytics report --metrics views,estimatedMinutesWatched \
--dimensions day --start 2026-01-01 --end 2026-01-31 --sort day --format json
# Fetch 200 rows, then resume with the returned nextStartIndex and identical flags.
oytc analytics overview --by day --start 2026-01-01 --all --limit 200 --format json
oytc analytics overview --by day --start 2026-01-01 --all --limit 200 --start-index 201 --format jsonoytc analytics overview --by day --format table
oytc analytics video VIDEO_ID --start 2026-01-01 --end 2026-01-31
oytc analytics traffic-sources --sort=-views --format tsv
oytc analytics demographics --format jsonloverviewrequests views, engaged views, estimated watch minutes, average view duration, average view percentage, and subscribers gained.--by day|monthis optional. (Thumbnail impressions and impression click-through rate are only available in YouTube Studio; the Analytics API exposes no such metrics.)video <VIDEO_ID>appliesvideo==VIDEO_IDand requests core engagement/watch metrics, including engaged views.traffic-sourcesgroups views and estimated watch minutes byinsightTrafficSourceType.demographicsgroupsviewerPercentagebyageGroup,gender.
Analytics' column-oriented response is normalized into ordinary objects, so all shared
formats and --columns work unchanged. Example JSON envelope:
{
"items": [{"day": "2026-01-01", "views": 42}],
"nextStartIndex": 2,
"requests": 1
}nextStartIndex appears only when another page may exist. For time reports fetched
with --all, it is an exact logical resume position when --limit stops early
(this example uses --all --limit 1).
oytc search 'Go conference' --type video --region US --page-size 10
oytc search 'live coding' --type video --event-type live --format jsonlsearch costs one call from the Data API's dedicated search bucket (100 search.list
calls per day by default), so it never follows extra pages unless --all is explicit.
Filters (see oytc search --help for the complete list):
| Flag | Values / meaning |
|---|---|
--type |
comma-separated video, channel, playlist (default: all three) |
--channel |
only resources created by this channel ID |
--order |
date, rating, relevance (default), title, videoCount, viewCount |
--published-after / --published-before |
RFC 3339 bounds |
--region / --language |
region code / relevance language |
--safe-search |
moderate (default), none, strict |
--event-type |
completed, live, upcoming (requires --type video) |
--location + --location-radius |
geographic video search (both required together) |
--topic |
Freebase topic ID |
--video-caption, --video-category, --video-duration, --video-embeddable, --video-license, --video-paid-product-placement, --video-syndicated |
video-only filters (require --type video) |
--channel-type |
any, show (requires --type channel) |
Incompatible combinations are rejected locally before any request is made.
Channel references accept canonical UC… IDs, @handles, and common
youtube.com/channel/…, youtube.com/@…, youtube.com/user/…, and youtube.com/c/…
URL forms. Legacy custom /c/ names cannot be resolved exactly through the API, so they
use a best-match channel search (may resolve to the wrong channel for ambiguous names).
oytc channel get @GoogleDevelopers --parts snippet,statistics
oytc channel activities UC_x5XG1OV2P6uZZ5FSM9Ttw --published-after 2026-01-01T00:00:00Z
oytc channel sections @GoogleDevelopers
oytc channel uploads https://youtube.com/@GoogleDevelopers --all --limit 100getaccepts multiple references and batches IDs (50 per request).activitieslists public channel activity with publication bounds.sectionslists a channel's sections, or fetches specific section IDs with--id.uploadsresolves the channel'scontentDetails.relatedPlaylists.uploadsplaylist and then lists its items — the reliable way to enumerate all public uploads.- Owner-only parts (
auditDetails,contentOwnerDetails) are rejected locally.
oytc video get dQw4w9WgXcQ --parts snippet,contentDetails,statistics
oytc video stats dQw4w9WgXcQ jNQXAC9IVRw --format tsv --columns id,statistics.viewCount
oytc video popular --region CA --category 28
oytc video trainability dQw4w9WgXcQ --format jsonget/statsbatch up to 50 IDs per request.statsdefaults to thestatisticspart.popularlists themostPopularchart for a region (defaultUS), optionally by category.trainabilitycalls the publicvideoTrainability.getendpoint without any key and consumes no Data API quota.- Owner-only parts (
fileDetails,processingDetails,suggestions) are rejected locally.
oytc playlist get PLxxxx
oytc playlist list --channel UC_x5XG1OV2P6uZZ5FSM9Ttw
oytc playlist items PLxxxx --all --limit 250getbatches playlist IDs (50 per request).listlists a channel's public playlists (--channelis required).itemslists a playlist's items;--videofilters to entries for one video ID.
oytc comment threads --video dQw4w9WgXcQ --order relevance --search music
oytc comment threads --channel UC_x5XG1OV2P6uZZ5FSM9Ttw
oytc comment replies Ugz...parent-id
oytc comment get Ugz...comment-idthreadsrequires exactly one of--video,--channel(all threads related to the channel), or--id.--order time|relevanceand--searchare incompatible with--id.replieslists replies to one top-level comment.getbatches comment IDs (100 per request).--text-format plainText|htmlcontrols comment text rendering (defaultplainText).
oytc subscription list --channel UC_x5XG1OV2P6uZZ5FSM9Ttw
oytc subscription list --channel UC... --for-channel UCtarget --order alphabeticalLists a channel's public subscriptions (many channels hide these; a hidden list returns
an API error). Requires exactly one of --channel or --id. The owner-only
subscriberSnippet part is rejected locally.
oytc category list --region GB
oytc category list --id 1,10,28
oytc language list
oytc region listoytc skills installskills install (also available as skill install) shows the exact destination and the
write permission it needs, then waits for an explicit y/yes confirmation. On approval,
it atomically installs the skill bundled with that CLI release to
~/.agents/skills/oytc, replacing an older installation. Declining makes no filesystem
changes. The installed bundle contains SKILL.md and its references/ files.
oytc version # version, commit, build date, Go/platform
oytc update # self-update to the latest GitHub release
oytc update --check # report only; change nothing
oytc update --version v0.2.0 # install an exact tag (permits pinning/downgrade)
oytc upgrade # alias of updateoytc_update and oytc_upgrade executables (symlinks created by the installer) run the
same operation via argv[0] dispatch — oytc_update --check is oytc update --check.
The updater downloads the platform archive and checksums.txt from GitHub Releases,
verifies the archive's SHA-256, extracts the binary with path-traversal protection, and
atomically replaces the current executable. Guarantees and edge cases:
- It never reads, needs, or transmits the YouTube API key.
- A checksum mismatch or a missing
checksums.txtaborts before anything is replaced. - Without an explicit
--version, it refuses to downgrade. - Prereleases are never installed implicitly;
releases/latestexcludes them. Pass--versionto install a specific prerelease tag. - Homebrew-managed installs are detected and refused — use
brew upgradeinstead. - If the install directory is not writable it fails with instructions rather than escalating; re-run with appropriate permissions or reinstall via the install script.
- On Windows the running
oytc.exeis renamed tooytc.exe.oldand the new binary takes its place; if that is not possible, it prints manual replacement instructions.
Global flags available on all commands:
| Flag | Meaning |
|---|---|
--format table|json|jsonl|tsv |
output format (default: table on a TTY, json otherwise) |
--columns a.b,c.d |
dotted property paths for table/TSV columns |
--no-header |
omit the table/TSV header row |
--no-color |
accepted for scripting (current output emits no color) |
--quiet, -q |
suppress human request summaries on stderr |
--timeout |
per-request timeout (default 20s) |
Resource commands also accept --parts (API resource parts) and --fields (Google's
partial-response selector); localized endpoints add --hl.
Finite JSON output uses a stable envelope:
{
"items": [],
"nextPageToken": "…",
"requests": 1
}- JSONL emits exactly one resource per line (no envelope). JSONL and TSV print a safe
continuation token on stderr when one is available, unless
--quietis set. - TSV/table cells replace embedded tabs and newlines so records stay one line.
- YouTube counters (view counts etc.) are JSON strings in the API and are preserved as strings, avoiding integer precision loss.
- Human table output prints an item/request summary on stderr unless
--quietis used. - The CLI retains the API metadata it needs for pagination even when
--fieldsselects only item fields. This lets--allcontinue without requiring callers to list cursor fields themselves.
List commands support:
--page-size N API results per request
--page-token TOKEN start/resume at a token
--all follow nextPageToken until exhausted
--limit N cap emitted resources (0 = no cap)
The default is one page. --limit can stop an --all traversal early; the final page can
leave a valid nextPageToken in the JSON envelope for resumption. When --limit discards
items from the middle of a fetched page, no nextPageToken is reported — the server's
token would resume past the discarded items and silently skip them. A limit that lands
exactly on a page boundary keeps the token. Batch-get commands split IDs according to
endpoint limits (50 for channels/videos/playlists, 100 for comments).
Quota: most list requests cost 1 unit against the default 10,000-unit daily quota, while
search.list draws from its own default bucket of 100 calls/day. See
quota costs.
| Platform | Default credential file |
|---|---|
| macOS | ~/Library/Application Support/oytc/auth.json |
| Linux/Unix | ${XDG_CONFIG_HOME:-~/.config}/oytc/auth.json |
| Windows | %APPDATA%\oytc\auth.json |
API-key precedence (highest first):
OYTC_API_KEYenvironment variableauth.jsoninOYTC_CONFIG_DIR(if set)auth.jsonin the platform default directory
OYTC_OAUTH_CLIENT_ID and OYTC_OAUTH_CLIENT_SECRET independently override stored client
credentials only when bootstrapping login --oauth; authorized tokens are always read from
auth.json. API-key and OAuth sections coexist and updates to one preserve the other.
On POSIX systems the config directory is created 0700 and the file 0600 where the
filesystem permits. Keys use X-Goog-Api-Key; OAuth uses Authorization: Bearer. Neither
secret is placed in request URLs.
Exit statuses are grouped for automation:
| Code | Meaning |
|---|---|
| 0 | success |
| 2 | usage/validation error |
| 3 | missing/invalid API key or OAuth authorization; re-run the indicated login |
| 4 | resource unavailable / not found / forbidden |
| 5 | quota or rate limit |
| 6 | network, temporary upstream, config, or other operational failure |
| 130 | interrupted before a command could shut down cleanly |
Transient transport errors and HTTP 429/5xx responses are retried with bounded exponential
backoff and Retry-After support. Google JSON errors are parsed into status, message, and
reason without exposing the key.
- OAuth is limited to read-only YouTube and Analytics scopes for the authorized channel. Revenue/content-owner reports, uploads, moderation, and all mutations remain absent.
/c/custom channel URLs require a search and can resolve to the API's best match.--fieldsselects resource fields; the CLI also requests the metadata needed to paginate safely.- Table defaults are intentionally compact; use JSON or custom
--columnsfor full resources.