# YouTube Title & Thumbnail Change Tracker - Metadata Edit Feed (`mibedk/youtube-metadata-change-tracker`) Actor

Watches YouTube videos, channels or playlists and emits only what changed since the last run: title or description edited, thumbnail replaced, tags or category changed, or the video gone private, unlisted or deleted, with before/after values and view count at the edit.

- **URL**: https://apify.com/mibedk/youtube-metadata-change-tracker.md
- **Developed by:** [Mikkel Bech-Hansen](https://apify.com/mibedk) (community)
- **Categories:** Videos, Social media
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 metadata change detecteds

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 Title & Thumbnail Change Tracker

Every other YouTube actor tells you what a video looks like **now**. This one tells you what **changed**. Give it a list of channels, playlists or individual videos, run it on a schedule, and each run returns only the metadata edits it has seen since the previous run: a title rewritten, a description edited, a thumbnail swapped, tags added or removed, the category changed, or the video gone private, unlisted or deleted. Every event carries the before value, the after value, and the view/like/comment counts at the moment the edit was spotted — so when a competitor re-titles a video on day three and it suddenly takes off, you have the old title, the new title, and the view count it was sitting at when they made the call.

It uses the official YouTube Data API v3 with your own free API key. Checking 500 videos costs about 12 units of the free 10,000-unit daily quota, so quota is not a practical limit.

### What a run looks like

- **First run on a video** — nothing to compare against, so the actor records the current metadata and returns one `baseline` record per video, showing what it will be watching from now on. No change events, and none charged.
- **Every run after that** — only the edits. A watchlist where nothing was touched returns an **empty dataset**, and that is the correct answer, not a failure.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `apiKey` | string (secret) | — | **Required.** Your own YouTube Data API v3 key. See below. |
| `channelUrls` | array of strings | `["https://www.youtube.com/@MrBeast"]` | Channel URLs, `@handles`, or raw `UC...` IDs. Tracks each channel's most recent uploads. |
| `videoUrls` | array of strings | `[]` | Specific videos: watch/shorts/embed/`youtu.be` URLs, or bare 11-character IDs. |
| `playlistUrls` | array of strings | `[]` | Playlist URLs or raw `PL...` IDs. |
| `maxVideosPerSource` | integer | `10` | Videos taken from each channel or playlist, newest first. Max 500. |
| `maxVideosPerRun` | integer | `50` | Hard cap on videos checked per run, across all sources. Max 2000. |
| `trackedFields` | array | all six | Any of `title`, `description`, `thumbnail`, `tags`, `category`, `status`. |
| `thumbnailCheckMode` | `headers` | `bytes` | `headers` | See "How thumbnail detection works". |
| `maxChangeEvents` | integer | `500` | Cap on change events returned and charged per run. |
| `markGoneAfterMisses` | integer | `2` | Consecutive runs a video must be missing before a `gone` event fires. 1–10. |
| `includeBaselineRecords` | boolean | `true` | Emit a `baseline` record the first time each video is seen. |
| `forceBaseline` | boolean | `false` | Discard remembered state and re-baseline everything. Leave off for scheduled runs. |
| `stateKeyValueStoreName` | string | `youtube-metadata-tracker-state` | Named key-value store holding last-seen metadata. Change it to run two independent watchlists. |
| `requestTimeoutSecs` | integer | `30` | 5–120. |
| `maxRequestRetries` | integer | `3` | 0–10. |
| `proxyConfiguration` | object | no proxy | Rarely needed — Google's API and image CDN do not IP-block. |

At least one of `channelUrls`, `videoUrls` or `playlistUrls` must be non-empty.

#### Getting an API key (about two minutes, free)

1. Open [console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials).
2. **Create Credentials → API key**, and copy it.
3. In **API Library**, find **YouTube Data API v3** and click **Enable** for the same project.
4. Paste the key into `apiKey`.

No OAuth, no login, no billing account.

### Output

One record per change event. `changeType` is one of `baseline`, `title`, `description`, `thumbnail`, `tags`, `category`, `status` or `gone`.

| Field | Type | Notes |
|---|---|---|
| `changeType` | string | Which field changed. |
| `videoId`, `videoUrl`, `videoTitle` | string | The video. `videoTitle` is the current title (last known, for `gone`). |
| `channelId`, `channelTitle`, `channelUrl` | string | The channel. |
| `publishedAt` | string | ISO 8601 original publish time. |
| `previousValue`, `newValue` | string | null | Before and after. For `tags`, comma-joined. For `category`, the readable name. For `thumbnail`, the image fingerprint. |
| `addedWords`, `removedWords` | string\[] | Word-level title diff. Empty for other change types. |
| `titleDiff` | string | null | Inline title diff, e.g. `(Official [-Music-] Video) {+(4K Remaster)+}`. |
| `addedTags`, `removedTags` | string\[] | Tags gained and lost. |
| `categoryName` | string | null | Current category, readable. |
| `privacyStatus` | string | null | `public`, `unlisted`, or `gone`. |
| `thumbnailUrl` | string | Canonical `i.ytimg.com` thumbnail path. |
| `viewCount`, `likeCount`, `commentCount` | number | null | Counts **at the moment the edit was observed**. `null` when the creator hides them. |
| `previousObservedAt`, `observedAt` | string | ISO 8601. The edit happened somewhere between these two timestamps. |

A real `title` record, from the test run in `TEST-REPORT.md`:

```json
{
  "changeType": "title",
  "videoId": "dQw4w9WgXcQ",
  "videoUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "videoTitle": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "channelId": "UCuAXFkgsw1L7xaCfnd5JJOw",
  "channelTitle": "Rick Astley",
  "channelUrl": "https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
  "publishedAt": "2009-10-25T06:57:33Z",
  "previousValue": "Rick Astley - Never Gonna Give You Up (Official Music Video)",
  "newValue": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
  "addedWords": ["(4K", "Remaster)"],
  "removedWords": ["Music"],
  "titleDiff": "Rick Astley - Never Gonna Give You Up (Official [-Music-] Video) {+(4K Remaster)+}",
  "addedTags": [],
  "removedTags": [],
  "categoryName": "Music",
  "privacyStatus": "public",
  "thumbnailUrl": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
  "viewCount": 1808453650,
  "likeCount": 19353634,
  "commentCount": 2455561,
  "previousObservedAt": "2026-08-26T23:27:03.367Z",
  "observedAt": "2026-08-26T23:27:19.656Z"
}
```

### How thumbnail detection works

When a creator replaces a thumbnail, **the URL does not change** — `i.ytimg.com/vi/{videoId}/hqdefault.jpg` is the same path before and after. Comparing thumbnail URLs, which is what a snapshot scraper gives you, will never detect a swap. This actor compares the image itself:

- **`headers` (default)** — a `HEAD` request to the CDN, comparing the `ETag` and byte length. The ETag is derived from the image version, so it changes when the picture does. Near-zero bandwidth, one cheap request per video.
- **`bytes`** — downloads the JPEG and compares a SHA-256 of the actual bytes. Immune to any CDN quirk, but roughly 20 KB per video per run.

Switching modes re-baselines the thumbnail rather than reporting a false change.

### Cost

Two events: `video-checked` ($0.0005) for each video polled and compared, and `change-detected` ($0.012) for each metadata edit reported. Full reasoning in `MONETIZATION.md`.

| Scenario | Cost |
|---|---|
| Default run: 1 channel, 10 videos, first (baseline) run | **$0.005** |
| Same watchlist, a normal day with no edits | **$0.005** |
| Same watchlist, a day with 2 edits | **$0.029** |
| 100-video watchlist, daily, ~3 edits/day | **$0.086/day**, about **$2.58/month** |
| 500-video watchlist, daily, ~10 edits/day | **$0.37/day**, about **$11.10/month** |

Because most videos are never edited, the recurring cost is dominated by `video-checked`, which is the cheap event by design. Set `maxVideosPerRun` and `maxChangeEvents` to put a hard ceiling on any single run, and set a total budget with the Apify **Max total charge** setting — the actor reads it, stops cleanly when it is reached, and delivers everything it has already charged you for.

### Known limitations

Read these before you run it on something that matters.

- **You need your own YouTube Data API v3 key.** Free, no billing account, about two minutes. But it is a real setup step and the actor cannot run without it.
- **Nothing is detected on the first run.** There is no history to compare against, so run one gets you `baseline` records only. Real change detection starts on run two.
- **Edits are dated to a window, not an instant.** The actor knows an edit happened between `previousObservedAt` and `observedAt`. Run hourly and that window is an hour; run weekly and it is a week. It cannot recover edits made before you started watching — there is no historical backfill, from this actor or from the YouTube API.
- **Private and deleted are indistinguishable.** The API silently omits both, identically. You get `changeType: "gone"` and no way to tell which happened. Unlisted videos are usually still returned by ID, so those normally surface as a `status` change rather than `gone`.
- **`gone` is debounced by design.** With the default `markGoneAfterMisses: 2`, a video must be missing on two consecutive runs before it is reported, so a `gone` event lags real deletion by one run. Set it to `1` for immediate reporting and accept occasional false alarms from API hiccups.
- **Thumbnail changes can be reported late.** `i.ytimg.com` is CDN-cached, so a replacement can take minutes to hours to propagate to the edge node the actor hits. Treat `observedAt` on a thumbnail event as "when we saw it", not "when they changed it".
- **`tags` are only visible on some videos.** `snippet.tags` is returned for many but not all videos, and Google has restricted tag visibility before. Where tags are absent the actor sees an empty list and simply never reports tag changes for that video.
- **Deleting the state store resets everything.** The watchlist's memory lives in the named key-value store. Delete it, rename it, or set `forceBaseline: true`, and the next run re-baselines and reports nothing.
- **`maxVideosPerSource` takes the newest uploads, not the most-watched.** If a channel posts daily, a value of 10 means you are watching a rolling ten-day window and older videos silently fall out of the watchlist. Raise it, or pin the videos you care about in `videoUrls`, which always take priority.
- **Statistics can be `null`.** Creators can hide like counts and disable comments. The actor reports `null` rather than pretending the number is zero.
- **No transcripts, comments, or revenue data.** This actor tracks metadata fields only.

### Legal

Not affiliated with, endorsed by, or sponsored by YouTube or Google LLC. YouTube is a trademark of Google LLC.

**Metadata comes from the official YouTube Data API v3, using your own API key.** That makes you the API client in Google's eyes: the key is registered to your Google Cloud project, the quota is yours, and your use is governed by your own agreement with Google. You are responsible for complying with the [YouTube API Services Terms of Service](https://developers.google.com/youtube/terms/api-services-terms-of-service) and the [YouTube API Services Developer Policies](https://developers.google.com/youtube/terms/developer-policies). Points that bear directly on how this actor works:

- **Stored API data must be refreshed or deleted.** The Developer Policies restrict how long API data (other than resource IDs) may be retained without refreshing — currently 30 days. This actor keeps last-seen titles, descriptions and tags in a key-value store in order to diff them, and a scheduled run refreshes that state every time it runs. **If you stop running the actor, delete its state key-value store**, and apply the same rule to the change events you export downstream.
- **Attribution and branding requirements apply** if you display YouTube data in a product or interface.
- **Do not use the data to build a service that replicates core YouTube features**, and do not use it for the purposes the policies prohibit.

**Thumbnail images are fetched directly from YouTube's public image CDN (`i.ytimg.com`), not through the Data API.** The Data API does not expose an image fingerprint, so there is no API-only way to detect a thumbnail swap. This is a plain request for a publicly served image, but it is outside the documented API surface, and YouTube's Terms of Service likely restrict automated access to its properties regardless of whether the content is publicly reachable. Using this actor may therefore not comply with those terms. You are responsible for your own compliance and for how you use the data it returns. If you would rather not make those requests at all, remove `thumbnail` from `trackedFields` — every other field is served entirely by the official API.

This actor collects public video and channel metadata only. It does not access private or login-walled content, and it collects no personal data beyond the public channel identifiers and channel titles attached to the videos you choose to watch.

# Actor input Schema

## `apiKey` (type: `string`):

A free Google Cloud API key with the 'YouTube Data API v3' enabled. No OAuth or login needed. Get one in ~2 minutes: console.cloud.google.com/apis/credentials -> Create Credentials -> API key, then enable 'YouTube Data API v3' for that project in the API Library. Checking 500 videos costs about 12 units of the free 10,000-unit daily quota, so quota is not a practical limit here.

## `channelUrls` (type: `array`):

Channel URLs, @handles, or raw UC... channel IDs. The actor tracks each channel's most recent uploads (see 'Max videos per source'). Example: https://www.youtube.com/@MrBeast

## `videoUrls` (type: `array`):

Specific YouTube videos to watch, as full URLs (watch, shorts, embed or youtu.be) or bare 11-character video IDs. Use this when you care about a handful of videos rather than a whole channel.

## `playlistUrls` (type: `array`):

Playlist URLs or raw PL.../UU... playlist IDs. The actor tracks the first N videos in each playlist (see 'Max videos per source').

## `maxVideosPerSource` (type: `integer`):

How many videos to take from each channel or playlist, newest first. Individual videos listed above are always included and do not count against this.

## `maxVideosPerRun` (type: `integer`):

Hard cap on how many videos are checked (and charged as 'video-checked') in a single run, across all sources. Protects you from an accidental 5,000-video watchlist.

## `trackedFields` (type: `array`):

Which metadata fields produce a change event. Unticking 'thumbnail' makes runs noticeably faster and cheaper, because it is the only field that needs a request to the image CDN rather than the API.

## `thumbnailCheckMode` (type: `string`):

'headers' sends a HEAD request to i.ytimg.com and compares the ETag and byte length - fast, near-zero bandwidth, and accurate in practice. 'bytes' downloads the JPEG and compares a SHA-256 of the actual pixels - slower and ~20 KB per video, but immune to a CDN that changes an ETag without changing the image. Ignored if 'thumbnail' is not in the tracked fields.

## `maxChangeEvents` (type: `integer`):

Hard cap on the number of change events returned and charged for in one run. A normal run on a stable watchlist returns single digits; this only matters on the run after you widen the watchlist.

## `markGoneAfterMisses` (type: `integer`):

The YouTube API silently omits videos that are deleted or made private, and it also omits them during transient hiccups. The actor waits for this many consecutive runs with the video missing before emitting a 'gone' event, so a blip does not create a false alarm. Set to 1 to report immediately.

## `includeBaselineRecords` (type: `boolean`):

The first run has nothing to compare against, so it cannot report changes. With this on, that run returns one 'baseline' record per video showing the metadata as it stands now, so you can see the watchlist was picked up correctly. Baseline records are not charged as change events. Turn it off if you only ever want edits.

## `forceBaseline` (type: `boolean`):

Discards the remembered state for every video in this run and treats them all as seen for the first time. Use after changing 'Thumbnail check mode', or to clear out state you no longer trust. Leave off for normal scheduled runs, otherwise the actor can never detect a change.

## `stateKeyValueStoreName` (type: `string`):

Name of the named key-value store holding the last-seen metadata for each video. Runs sharing this name share a memory of the watchlist. Change it to run two independent watchlists side by side under one account.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout for YouTube API and thumbnail CDN calls.

## `maxRequestRetries` (type: `integer`):

How many times to retry a failing YouTube API or thumbnail request before giving up on it.

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

Optional. The actor talks to Google's official API and image CDN, which do not IP-block, so the default of no proxy is both cheaper and faster. Only turn this on if your own network is rate-limiting you.

## Actor input object example

```json
{
  "channelUrls": [
    "https://www.youtube.com/@MrBeast"
  ],
  "videoUrls": [],
  "playlistUrls": [],
  "maxVideosPerSource": 10,
  "maxVideosPerRun": 50,
  "trackedFields": [
    "title",
    "description",
    "thumbnail",
    "tags",
    "category",
    "status"
  ],
  "thumbnailCheckMode": "headers",
  "maxChangeEvents": 500,
  "markGoneAfterMisses": 2,
  "includeBaselineRecords": true,
  "forceBaseline": false,
  "stateKeyValueStoreName": "youtube-metadata-tracker-state",
  "requestTimeoutSecs": 30,
  "maxRequestRetries": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `changeEvents` (type: `string`):

No description

# 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 = {
    "channelUrls": [
        "https://www.youtube.com/@MrBeast"
    ],
    "videoUrls": [],
    "playlistUrls": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("mibedk/youtube-metadata-change-tracker").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 = {
    "channelUrls": ["https://www.youtube.com/@MrBeast"],
    "videoUrls": [],
    "playlistUrls": [],
}

# Run the Actor and wait for it to finish
run = client.actor("mibedk/youtube-metadata-change-tracker").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 '{
  "channelUrls": [
    "https://www.youtube.com/@MrBeast"
  ],
  "videoUrls": [],
  "playlistUrls": []
}' |
apify call mibedk/youtube-metadata-change-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mibedk/youtube-metadata-change-tracker"
        }
    }
}
```

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/mifw8ufKmjWEkZgEs/builds/Ef8qFeSCXYnZEsFQ7/openapi.json
