# YouTube Comments Scraper with Incremental Sync (`danthedataman/youtube-channel-comment-ledger`) Actor

Export public YouTube comments and replies with author details, likes and reply counts. Use incremental scans to collect new comments only across repeated runs.

- **URL**: https://apify.com/danthedataman/youtube-channel-comment-ledger.md
- **Developed by:** [Eli J](https://apify.com/danthedataman) (community)
- **Categories:** Videos, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.11 / 1,000 comments

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### Export YouTube comments and replies

This YouTube comments scraper takes public video URLs or IDs and returns comment text, author details, likes and reply counts. Use the results for discussion analysis or comment monitoring. Incremental sync (the `incremental` setting) collects previously unseen comments, including replies added under old threads, without emitting remembered IDs again. No YouTube login or API key is required.

### Try the prefilled input

The prefilled form contains three public videos, Top comments and replies, with a limit of 40 new rows per video and 120 for the run. Run it to inspect the dataset, then replace the sample videos with your own. The limits are ceilings, not guaranteed result counts.

```json
{
  "videoUrls": [
    "9hetShMMp2s",
    "B-ul3b0lz-o",
    "D8PikZ1KhUo"
  ],
  "maxComments": 40,
  "maxItems": 120,
  "includeReplies": true,
  "sortCommentsBy": "TOP_COMMENTS"
}
```

Use watch, youtu.be, Shorts, embed or live video URLs, or bare video IDs. This collects video discussions; live chat and channel-video discovery are outside this product. Pass each URL or ID as a string. An API call with `{}` succeeds with guidance and collects nothing: the prefilled form does not become an omitted-input default.

### Output and exports

Open Results and the Comments and replies dataset view to inspect rows or export them as JSON or CSV. The following excerpt contains literal fields from a retained 2026-10-10 run of the first sample video, `9hetShMMp2s`. It omits other row fields for readability; comment content and counters can change between runs.

```json
{
  "recordType": "comment",
  "id": "Ugzl-ky9OdrQg-UCbHp4AaABAg",
  "videoId": "9hetShMMp2s",
  "channelId": "UC2ojq-nuP8ceeHqiroeKhBA",
  "title": "Claude Code Mods Are Game Changers. Set Up These 5 NOW.",
  "text": "Thanks Nate - The handoff is probably the most useful one to prevent context rot",
  "authorText": "@GuybrushWins",
  "likeCount": 7,
  "replyCount": 3,
  "publishedTimeText": "7 days ago",
  "publishedAtApprox": "2026-10-03T21:33:31.820663+00:00",
  "publishedAtPrecision": "day",
  "inputSource": "9hetShMMp2s",
  "billingEvent": "comment-row"
}
```

Every dataset row has `recordType` (comment or reply), stable `id`, `inputSource`, `scrapedAt` and `billingEvent`. Each row also includes:

- Text: `text`, `comment` and `textDisplay`.
- Comment identity: `cid`, `commentId` and `replyToCid`.
- Relative-time labels: `publishedTime`, `publishedTimeText` and `commentPublishText`.
- Counts: `likeCount`, `likesCount`, `voteCount`, `replyCount` and `commentsCount`.
- Author details: the `author` object, `authorText`, `authorChannelId`, `authorAvatar` and `authorUrl`.
- Flags: verified, artist, creator, pinned, edited and creator-heart status.
- Video details: `videoId`, `channelId`, `pageUrl` and `title`.

Like and reply counters are integers, with absent/null/blank counts represented as 0. Nonblank unparseable counts are null with their literal text preserved in `likeCountText`/`replyCountText`. Rounded source counts such as 1.2K project to 1200 and keep their displayed text. `commentsCount` is the available video discussion count, or null when unknown. `authorText` is the scalar author alias.

publishedAtApprox is derived at capture time from YouTube's English relative label; publishedAtPrecision is minute/hour/day/week/month/year. Months mean 30 days and years mean 365 days. It is an estimate, not an exact creation or edit timestamp. Unknown ages keep null estimates. When an `oldestCommentDate` filter requires an unreadable or missing age, that is a `DATE_UNAVAILABLE` error.

publishedAtDayApprox is the same estimate truncated to its UTC day and rendered as YYYY-MM-DDT00:00:00Z. Its precision is still publishedAtPrecision: a week/month/year label remains coarser than a day, even though the rendering contains a day. It is an approximate day from YouTube's relative text, not a creation or published date. Unknown ages have null estimates. publishedTime and publishedTimeText both preserve the literal relative label, including an edited suffix.

The following table maps exported fields to flat-author and nested-author formats while preserving measured values and JSON types. Date estimates use the same capture-time anchor; values observed at different capture times are not guaranteed equal.

| Own value (JSON type) | Flat-author format | Nested-author format |
| --- | --- | --- |
| authorText (string) | author | author.name |
| authorChannelId (string), authorAvatar (string or null) | Not mapped | author.id, author.thumbnails; compose its author object from these and authorText |
| publishedTimeText (string or null) | publishedTimeText | Not mapped |
| publishedAtDayApprox (string or null; approximate) | Not mapped | publishedTime, its day rendering |
| replyCount, voteCount (integer; unknown null) | replyCount, voteCount | Not mapped |
| likeCountText, replyCountText (string) | Not mapped | likeCount, replyCount |
| authorIsChannelOwner, hasCreatorHeart (boolean) | same names | Not mapped |
| cid, comment, pageUrl, title, videoId, type (string); commentsCount (integer or null); replyToCid (string or null) | same names | type |
| id, inputSource, text (string) | Not mapped | same names |

### Pricing and billing units

Check the Actor's pricing panel for installed event prices. Event prices cover the Actor's platform usage, so computation, storage and proxy traffic are not billed separately.

For each requested video in each run, the first 20 new comments or replies use `comment-intro`; later rows use `comment-row`. Each emitted row has one billing event. Apify's standard Actor start event is charged once per run at up to 1 GB of memory. There is no query, reply-thread or cursor event. Remembered IDs produce no row event. Polls with no new rows are still charged the start event, so choose a useful polling interval and scan window.

### Input settings

| Field | Type | API default | Meaning |
| --- | --- | --- | --- |
| `videoUrls` | array of strings | empty | Up to 500 video URLs or IDs. |
| `maxComments` | integer | 20 | New comments plus replies per video; 0 performs no collection. |
| `maxItems` | integer | 1000 | New output rows for the whole run; 0 performs no collection and returns guidance. At a positive cap, remaining inputs are not visited by the output loop; SUMMARY.stopReason is "maxItems" and coverage lists visited sources. Prefetched bootstrap requests may already have occurred. |
| `includeReplies` | boolean | true | Follow reply continuations; replies count toward maxComments. |
| `sortCommentsBy` | string | TOP_COMMENTS | TOP_COMMENTS or NEWEST_FIRST, selected from delivered controls. |
| `oldestCommentDate` | string | none | Inclusive approximate age boundary, using YouTube's relative label. An unreadable age is an error. |
| `incremental` | boolean | false | Persist seen identities and emit only unseen rows. |
| `stateStoreName` | string | generated | Named Apify key-value store for incremental STATE: 1–63 ASCII letters, digits or hyphens, starting with a letter or digit. |
| `resetState` | boolean | false | After recovery, clear remembered IDs and replay old rows. |
| `maxPagesPerSource` | integer | 100 | Page budget per discussion including reply pages; 0 performs no collection; maximum 10000. |
| `maxRetriesPerRequest` | integer | 2 | Fresh HTTP session retries after the first attempt; maximum 8. Missing-data retries stop after two total attempts. |
| `maxRunSeconds` | integer | 240 | Collection time budget; maximum 3600. |
| `proxyConfiguration` | object | datacenter | Apify proxy configuration; defaults to Apify datacenter proxies. A blocked egress can require a different proxy route. |

Omitted sources never trigger collection. Superseded channel/video-listing/discovery controls are rejected with INVALID_INPUT. Metadata and relative-age parsing use English/US public responses.

### Collect new comments only

1. Set `incremental` to `true`.
2. Keep the same source set, or supply a `stateStoreName` such as `my-video-comment-sync` and reuse that name.
3. Run polls serially. Each poll scans the requested window and emits IDs that its store has not remembered.

"New" means previously unseen by this store. A later poll can find older comments beyond the first poll's cap. Caps count newly emitted rows; page and time budgets bound rescanning. Increase the scan window when you need to reach older threads or their new replies. Set `resetState` to `true` to make remembered rows eligible again.

#### State and recovery

Generated names hash sorted unique canonical submitted video IDs plus the comment/reply record families. Equivalent video URL/ID spellings share a store. Caps, filters, sorting, reply inclusion, defaults and transport budgets do not change the key; changing the source set does. An explicit name shares history across different source sets or scan windows.

STATE version 2 snapshots remembered comment/reply IDs and output/charge receipts grouped by owner channel, with per-video exhausted-scan markers and an applied journal sequence.

Up to 40 new rows are buffered; a JOURNAL reservation precedes each batch, including earlier committed receipts since the last source snapshot. Rows are pushed in homogeneous billing-event groups. STATE checkpoints follow every 400 committed receipts and each source with changes; the applied sequence makes an old reservation harmless. Version 1 single-row journals are reconciled and migrated before collection.

Recovery reads the pending batch from its prior run dataset tail, checks its receipt and remembers exactly the written prefix. For priced events, charged-event totals must also match. Unpriced receipts are valid when no Actor event price is installed. A foreign writer must be terminal; the current run ID can reconcile during its own restart.

A restarted run also reads its existing default dataset once, in bounded pages, to rebuild total and per-video row counts so caps and the first-20 intro tariff remain per run. For a non-incremental restart, those same pages also restore the current run's IDs in memory so already delivered comments are not duplicated. Fresh runs have empty default datasets and skip this read.

Normal emission performs no per-row key-value write or dataset read. Incremental persistence uses up to two writes per batch: one JOURNAL reservation and a STATE snapshot at source/end. A video that yields one 40-row batch uses two writes. In the retained 1,000-row single-video test, batches shared snapshots until the 400-receipt checkpoint and used 28 writes. Legacy migration can add one journal-retirement write. Small positive polls have higher fixed storage overhead.

SUMMARY.coverage marks exhausted videos complete; capped and failed videos are incomplete, and failures are listed in ERRORS. Partial-scan IDs remain remembered, and lastSuccess advances only on exhaustion.

Before interrupted output is remembered, the dataset prefix and its receipt must agree. A foreign writer must be terminal; a matching current run ID can reconcile its own interrupted output. An ambiguous receipt stops with STATE_UNCERTAIN and retains the journal for review; it is never replayed blindly. resetState clears history only after recovery on a fresh run and makes old rows billable again. A resumed run with existing output retains the reset it already applied and never clears those delivered IDs again. Deleting the named store also removes history.

### Coverage, errors and limits

Only logged-out public discussions are supported. Errors and unresolvable inputs go to ERRORS, never the dataset. A run succeeds when at least one row was delivered and no charge receipt is uncertain, even if a later source or page fails. See ERRORS and SUMMARY.coverage for the incomplete work. It fails when input is invalid, when incremental state cannot be recovered, when a charge receipt is uncertain, or when no row was delivered and every attempted source failed without collection stopping at a cap. Valid empty or filtered videos can succeed with zero rows. SUMMARY records source outcomes, caps, charge-limit stop and transport metrics. No exhaustive-history or exact-time claim is made outside the requested windows.

#### Collection and fallback

Collection uses cookie-free HTTP/JSON, with isolated Chromium fallback after retryable failures. Video identity, title, owner and the discussion token come from one InnerTube next request by video ID; collection fetches neither player nor the watch page. Unavailable videos and comments-off responses are source errors and are not retried. Malformed discussion data is checked inside the retry/fallback boundary. A failed fallback is reported as an error, not an empty video.

Each HTTP attempt has a total wall deadline of 25 seconds or the remaining run budget, whichever is shorter. This includes up to two seconds reserved for client close. Attempt timeouts use the `NETWORK` retry and fallback path. Malformed data or rejected request context gets at most two HTTP attempts, with 0.1-second retry spacing. Other retryable failures use 0.4 seconds. Successful first attempts have no fixed wait. Up to four independent reads overlap; row, billing and journal commits stay serial. While the next comment page for a video is requested, one received page can wait in a queue, so a run can request comment pages it does not use. One measured run of three videos at the default 20 comments per video made 8 requests instead of 6 and received about 13 KB (5%) more response bytes.

Browser requests share a 45-second budget for queueing, work and cleanup, with up to five seconds reserved for cleanup. Shorter remaining run budgets limit both work and cleanup. Browser work runs one request at a time. Contexts are fresh and nonpersistent, clear cookies before routed requests and block service workers. The Actor never logs in.

Use at least 512 MB memory. `SUMMARY.transport` reports encoded bytes of completed HTTP responses, browser fallback counts and browser body bytes. Attempts ended by the attempt deadline are not byte-counted. These byte counts are not a complete proxy invoice.

#### Cold starts

Cold image pulls can dominate launch time. In five scored ten-comment runs on 2026-10-10, two pulls took 14–18 seconds. This infrastructure tail is separate from request work. Requests can also be slow: a later five-run ten-comment cohort had a 19.673-second run containing an 11.552-second continuation request. In that run the request stayed inside the 25-second attempt limit; its image pull took 0.002 seconds. The request interval includes proxy and network time; its cause was not isolated.

#### Reading failures

The Request errors output opens the ERRORS record when present. A clean first attempt can have no ERRORS record. A same-run restart can retain an earlier ERRORS record while SUMMARY describes the current attempt.

### Common questions

#### Why did a successful run return no rows?

Empty input and zero row or page caps return no rows successfully. On these no-work paths, `SUMMARY` contains `pushed`, `allFailed` and `guidance`; it has no stop reason or coverage. For a collected scan, including an empty video, filtered window or incremental window with no unseen IDs, inspect `SUMMARY.stopReason` and `SUMMARY.coverage`.

#### Why are some videos missing from coverage?

Once a positive `maxItems` limit is reached, remaining inputs are not visited by the output loop. Prefetched bootstrap requests may already have occurred. `SUMMARY.stopReason` is `maxItems`, and coverage lists only visited sources. A source error can also leave a visited video incomplete; inspect `ERRORS` and its coverage entry.

#### Are the published dates exact?

YouTube supplies relative labels such as “7 days ago.” The Actor preserves that text and labels its inferred timestamps approximate. Coarse labels stay coarse even when a day-shaped alias is rendered.

#### Can two runs share the same incremental store?

Use serial runs. Concurrent writers to the same store are unsupported. Interrupted output is reconciled before collection or reset; an uncertain receipt stops for review instead of replaying blindly.

### Help

When reporting a problem, include the run ID, your input, the relevant `ERRORS` code and the incomplete coverage entry. Remove proxy credentials and other secrets from anything you share. Keep the named incremental store until an interrupted run's receipts have been reviewed.

# Changelog

This Actor's version history is a separate document: https://apify.com/danthedataman/youtube-channel-comment-ledger/changelog.md

# Actor input Schema

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

Up to 500 video URLs or IDs.

## `oldestCommentDate` (type: `string`):

Inclusive approximate age boundary, using YouTube's relative label. An unreadable age is an error.

## `stateStoreName` (type: `string`):

Named Apify key-value store for incremental STATE: 1–63 ASCII letters, digits or hyphens, starting with a letter or digit.

## `maxComments` (type: `integer`):

New comments plus replies per video; 0 performs no collection.

## `maxItems` (type: `integer`):

New output rows for the whole run; 0 performs no collection and returns guidance. At a positive cap, remaining inputs are not visited by the output loop; SUMMARY.stopReason is maxItems and coverage lists visited sources. Prefetched bootstrap requests may already have occurred.

## `maxPagesPerSource` (type: `integer`):

Page budget per discussion including reply pages; 0 performs no collection; maximum 10000.

## `maxRetriesPerRequest` (type: `integer`):

Fresh HTTP session retries after the first attempt; maximum 8. Missing-data retries stop after two total attempts.

## `maxRunSeconds` (type: `integer`):

Collection time budget; maximum 3600.

## `incremental` (type: `boolean`):

Persist seen identities and emit only unseen rows.

## `resetState` (type: `boolean`):

After recovery, intentionally clear seen IDs and replay old rows.

## `includeReplies` (type: `boolean`):

Follow reply continuations; replies count toward maxComments.

## `sortCommentsBy` (type: `string`):

TOP_COMMENTS or NEWEST_FIRST, selected from delivered controls.

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

Apify proxy configuration; defaults to Apify datacenter proxies. A blocked egress can require a different proxy route.

## Actor input object example

```json
{
  "videoUrls": [
    "9hetShMMp2s",
    "B-ul3b0lz-o",
    "D8PikZ1KhUo"
  ],
  "maxComments": 40,
  "maxItems": 120,
  "includeReplies": true,
  "sortCommentsBy": "TOP_COMMENTS"
}
```

# Actor output Schema

## `results` (type: `string`):

Comment/reply rows. Streamers author maps to authorText, publishedTimeText to the literal alias, and replyCount/voteCount to integer counters. Apidojo likeCount/replyCount map to likeCountText/replyCountText strings; author.id/name/thumbnails map to authorChannelId/authorText/authorAvatar. Its publishedTime day rendering maps to publishedAtDayApprox, the same publishedAtApprox inference truncated to UTC day, never an exact creation or published date; publishedAtPrecision remains the source unit, including coarser week/month/year. Same-named remaining fields retain their types. Absent counters are zero; nonblank unparseable counters are null with literal text preserved.

## `errors` (type: `string`):

Handled request failures, when present; never result rows.

# 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 = {
    "videoUrls": [
        "9hetShMMp2s",
        "B-ul3b0lz-o",
        "D8PikZ1KhUo"
    ],
    "maxComments": 40,
    "maxItems": 120,
    "includeReplies": true,
    "sortCommentsBy": "TOP_COMMENTS"
};

// Run the Actor and wait for it to finish
const run = await client.actor("danthedataman/youtube-channel-comment-ledger").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 = {
    "videoUrls": [
        "9hetShMMp2s",
        "B-ul3b0lz-o",
        "D8PikZ1KhUo",
    ],
    "maxComments": 40,
    "maxItems": 120,
    "includeReplies": True,
    "sortCommentsBy": "TOP_COMMENTS",
}

# Run the Actor and wait for it to finish
run = client.actor("danthedataman/youtube-channel-comment-ledger").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 '{
  "videoUrls": [
    "9hetShMMp2s",
    "B-ul3b0lz-o",
    "D8PikZ1KhUo"
  ],
  "maxComments": 40,
  "maxItems": 120,
  "includeReplies": true,
  "sortCommentsBy": "TOP_COMMENTS"
}' |
apify call danthedataman/youtube-channel-comment-ledger --silent --output-dataset

```

## MCP server setup

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

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/zyRFGcv3RLsK3Z94f/builds/BQLhaYbgz326px6ha/openapi.json
