The YouTube Comments API enables developers to build powerful engagement tools that fetch, post, and moderate comments programmatically. Whether you're building a social media dashboard, a community management tool, or an automated response system, understanding the YouTube Data API's comment resources is essential for creating effective integrations.
This guide covers everything you need to implement robust YouTube comment management in your applications, from authentication setup to quota optimization strategies that keep your integration running smoothly.
Introduction to YouTube Data API v3
The YouTube Data API v3 provides programmatic access to YouTube's core features, including video uploads, playlist management, and comment operations. For comment management specifically, the API exposes two primary resources:
- CommentThreads: Top-level comments on videos, including metadata about reply counts
- Comments: Individual comments, including replies to top-level comments
The API follows RESTful conventions and returns JSON responses. All comment operations require OAuth 2.0 authentication since they involve user-specific data and actions.
// Base configuration for YouTube API requests
const YOUTUBE_API_BASE = 'https://www.googleapis.com/youtube/v3';
interface YouTubeApiConfig {
accessToken: string;
baseUrl: string;
}
const config: YouTubeApiConfig = {
accessToken: process.env.YOUTUBE_ACCESS_TOKEN || '',
baseUrl: YOUTUBE_API_BASE,
};
API Key vs OAuth 2.0 Authentication
YouTube's comment operations require different authentication levels depending on the action:
| Operation | API Key | OAuth 2.0 |
|---|---|---|
| Read public comments | ✅ | ✅ |
| Read comments on own videos | ❌ | ✅ |
| Post comments | ❌ | ✅ |
| Reply to comments | ❌ | ✅ |
| Delete comments | ❌ | ✅ |
| Moderate comments | ❌ | ✅ |
For any write operation or access to private data, you must use OAuth 2.0. Here's how to set up the OAuth flow:
interface OAuthConfig {
clientId: string;
clientSecret: string;
redirectUri: string;
scopes: string[];
}
const oauthConfig: OAuthConfig = {
clientId: process.env.YOUTUBE_CLIENT_ID || '',
clientSecret: process.env.YOUTUBE_CLIENT_SECRET || '',
redirectUri: 'https://yourapp.com/auth/youtube/callback',
scopes: [
'https://www.googleapis.com/auth/youtube.force-ssl',
'https://www.googleapis.com/auth/youtube',
],
};
function getAuthUrl(state?: string): string {
const params = new URLSearchParams({
client_id: oauthConfig.clientId,
redirect_uri: oauthConfig.redirectUri,
scope: oauthConfig.scopes.join(' '),
response_type: 'code',
access_type: 'offline',
prompt: 'consent',
});
if (state) {
params.append('state', state);
}
return `https://accounts.google.com/o/oauth2/auth?${params.toString()}`;
}
async function exchangeCodeForToken(code: string): Promise<{
access_token: string;
refresh_token: string;
expires_in: number;
}> {
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: oauthConfig.clientId,
client_secret: oauthConfig.clientSecret,
redirect_uri: oauthConfig.redirectUri,
grant_type: 'authorization_code',
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
}
return response.json();
}
Note: The
youtube.force-sslscope is required for comment operations. Without it, write requests will fail with a 403 error.
Understanding YouTube API Quota (10,000 Units/Day)
The YouTube API quota system is one of the most important concepts to understand before building any integration. Every API request consumes quota units, and you're limited to 10,000 units per day by default.
Here's the quota cost breakdown for comment-related operations:
| Operation | Quota Cost |
|---|---|
| commentThreads.list | 1 unit |
| comments.list | 1 unit |
| commentThreads.insert | 50 units |
| comments.insert (reply) | 50 units |
| comments.delete | 50 units |
| comments.update | 50 units |
| comments.setModerationStatus | 50 units |
With 10,000 units per day, you can perform approximately:
- 10,000 read operations, OR
- 200 write operations, OR
- A combination of both
// Quota tracking utility
interface QuotaTracker {
used: number;
limit: number;
operations: Map<string, number>;
}
const quotaCosts: Record<string, number> = {
'commentThreads.list': 1,
'comments.list': 1,
'commentThreads.insert': 50,
'comments.insert': 50,
'comments.delete': 50,
'comments.update': 50,
'comments.setModerationStatus': 50,
};
function createQuotaTracker(dailyLimit: number = 10000): QuotaTracker {
return {
used: 0,
limit: dailyLimit,
operations: new Map(),
};
}
function trackQuotaUsage(
tracker: QuotaTracker,
operation: string
): { allowed: boolean; remaining: number } {
const cost = quotaCosts[operation] || 1;
if (tracker.used + cost > tracker.limit) {
return { allowed: false, remaining: tracker.limit - tracker.used };
}
tracker.used += cost;
const currentCount = tracker.operations.get(operation) || 0;
tracker.operations.set(operation, currentCount + 1);
return { allowed: true, remaining: tracker.limit - tracker.used };
}
Note: Quota resets at midnight Pacific Time (PT). If you hit quota limits, your requests will fail until the reset. Plan your operations accordingly.
CommentThreads vs Comments Resources
Understanding the difference between these two resources is crucial for efficient YouTube comment management.
CommentThreads represent top-level comments on a video. Each thread contains:
- The original comment (topLevelComment)
- Reply count
- Optionally, a subset of replies
Comments represent individual comments, which can be either:
- Top-level comments (accessed via CommentThreads)
- Replies to top-level comments
Use CommentThreads when you want to:
- Fetch all top-level comments on a video
- Get comment counts and basic reply information
- Post a new top-level comment
Use Comments when you want to:
- Fetch all replies to a specific comment
- Post a reply to an existing comment
- Update or delete a specific comment
// Type definitions for YouTube comment structures
interface YouTubeCommentSnippet {
authorDisplayName: string;
authorProfileImageUrl: string;
authorChannelId: { value: string };
textDisplay: string;
textOriginal: string;
likeCount: number;
publishedAt: string;
updatedAt: string;
}
interface YouTubeComment {
id: string;
snippet: YouTubeCommentSnippet;
}
interface YouTubeCommentThread {
id: string;
snippet: {
videoId: string;
topLevelComment: YouTubeComment;
totalReplyCount: number;
isPublic: boolean;
};
replies?: {
comments: YouTubeComment[];
};
}
Fetching Video Comments with the YouTube Comments API
Retrieving comments is the most common operation. Here's a complete implementation that handles pagination and HTML entity decoding:
// Helper to decode HTML entities from YouTube API responses
function decodeHtmlEntities(text: string): string {
if (!text) return text;
return text
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/'/g, "'")
.replace(///g, '/')
.replace(/ /g, ' ');
}
interface CommentData {
commentId: string;
comment: string;
created: string;
from: {
id: string;
name: string;
picture: string;
};
likeCount: number;
replyCount: number;
platform: 'youtube';
platformPostId: string;
replies: CommentData[];
}
interface GetCommentsOptions {
limit?: number;
pageToken?: string;
}
interface GetCommentsResult {
comments: CommentData[];
pagination: {
hasMore: boolean;
pageToken?: string;
};
}
async function getVideoComments(
accessToken: string,
videoId: string,
options: GetCommentsOptions = {}
): Promise<GetCommentsResult> {
const maxResults = Math.min(options.limit || 25, 100);
const params = new URLSearchParams({
part: 'snippet,replies',
videoId: videoId,
maxResults: maxResults.toString(),
});
if (options.pageToken) {
params.append('pageToken', options.pageToken);
}
const response = await fetch(
`${YOUTUBE_API_BASE}/commentThreads?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to fetch comments: ${response.status} ${errorBody}`);
}
const data = await response.json();
const comments: CommentData[] = (data.items || []).map(
(thread: YouTubeCommentThread) => {
const topComment = thread.snippet.topLevelComment.snippet;
const replies: CommentData[] = (thread.replies?.comments || []).map(
(reply: YouTubeComment) => ({
commentId: reply.id,
comment: decodeHtmlEntities(reply.snippet.textDisplay),
created: reply.snippet.publishedAt,
from: {
id: reply.snippet.authorChannelId?.value || '',
name: reply.snippet.authorDisplayName,
picture: reply.snippet.authorProfileImageUrl,
},
likeCount: reply.snippet.likeCount || 0,
replyCount: 0,
platform: 'youtube' as const,
platformPostId: thread.snippet.videoId,
replies: [],
})
);
return {
commentId: thread.snippet.topLevelComment.id,
comment: decodeHtmlEntities(topComment.textDisplay),
created: topComment.publishedAt,
from: {
id: topComment.authorChannelId?.value || '',
name: topComment.authorDisplayName,
picture: topComment.authorProfileImageUrl,
},
likeCount: topComment.likeCount || 0,
replyCount: thread.snippet.totalReplyCount || 0,
platform: 'youtube' as const,
platformPostId: thread.snippet.videoId,
replies,
};
}
);
return {
comments,
pagination: {
hasMore: !!data.nextPageToken,
pageToken: data.nextPageToken,
},
};
}
Posting Comment Replies
Replying to comments requires the comments.insert endpoint with the parent comment ID:
interface ReplyResult {
replyId: string;
text: string;
publishedAt: string;
}
async function replyToComment(
accessToken: string,
parentCommentId: string,
text: string
): Promise<ReplyResult> {
const response = await fetch(
`${YOUTUBE_API_BASE}/comments?part=snippet`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
snippet: {
parentId: parentCommentId,
textOriginal: text,
},
}),
}
);
if (!response.ok) {
const errorBody = await response.text();
// Handle specific error cases
if (response.status === 403) {
if (errorBody.includes('commentsDisabled')) {
throw new Error('Comments are disabled on this video');
}
if (errorBody.includes('forbidden')) {
throw new Error('You do not have permission to comment on this video');
}
}
if (response.status === 400) {
if (errorBody.includes('processingFailure')) {
throw new Error('Comment processing failed. The comment may contain prohibited content.');
}
}
throw new Error(`Failed to post reply: ${response.status} ${errorBody}`);
}
const data = await response.json();
return {
replyId: data.id,
text: data.snippet.textDisplay,
publishedAt: data.snippet.publishedAt,
};
}
// Post a new top-level comment on a video
async function postComment(
accessToken: string,
videoId: string,
text: string
): Promise<{ commentId: string }> {
const response = await fetch(
`${YOUTUBE_API_BASE}/commentThreads?part=snippet`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
snippet: {
videoId,
topLevelComment: {
snippet: {
textOriginal: text,
},
},
},
}),
}
);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Failed to post comment: ${response.status} ${errorBody}`);
}
const data = await response.json();
return { commentId: data.id };
}
Comment Moderation Operations
YouTube provides moderation capabilities for channel owners to manage comments on their videos:
type ModerationStatus = 'heldForReview' | 'published' | 'rejected';
interface ModerationResult {
success: boolean;
commentId: string;
newStatus: ModerationStatus;
}
async function setCommentModerationStatus(
accessToken: string,
commentId: string,
moderationStatus: ModerationStatus,
banAuthor: boolean = false
): Promise<ModerationResult> {
const params = new URLSearchParams({
id: commentId,
moderationStatus: moderationStatus,
banAuthor: banAuthor.toString(),
});
const response = await fetch(
`${YOUTUBE_API_BASE}/comments/setModerationStatus?${params.toString()}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!response.ok) {
const errorBody = await response.text();
if (response.status === 403) {
throw new Error('You can only moderate comments on your own videos');
}
throw new Error(`Moderation failed: ${response.status} ${errorBody}`);
}
return {
success: true,
commentId,
newStatus: moderationStatus,
};
}
async function deleteComment(
accessToken: string,
commentId: string
): Promise<{ success: boolean }> {
const response = await fetch(
`${YOUTUBE_API_BASE}/comments?id=${commentId}`,
{
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
// YouTube returns 204 No Content on successful deletion
if (!response.ok && response.status !== 204) {
const errorBody = await response.text();
throw new Error(`Failed to delete comment: ${response.status} ${errorBody}`);
}
return { success: true };
}
Pagination with pageToken
YouTube uses cursor-based pagination via pageToken. This approach is more efficient than offset-based pagination for large datasets:
interface PaginatedCommentsResult {
allComments: CommentData[];
totalFetched: number;
}
async function fetchAllComments(
accessToken: string,
videoId: string,
maxComments: number = 500
): Promise<PaginatedCommentsResult> {
const allComments: CommentData[] = [];
let pageToken: string | undefined;
while (allComments.length < maxComments) {
const result = await getVideoComments(accessToken, videoId, {
limit: 100, // Maximum allowed per request
pageToken,
});
allComments.push(...result.comments);
if (!result.pagination.hasMore || !result.pagination.pageToken) {
break;
}
pageToken = result.pagination.pageToken;
// Add a small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
}
return {
allComments: allComments.slice(0, maxComments),
totalFetched: allComments.length,
};
}
// Fetch replies with pagination
async function fetchAllReplies(
accessToken: string,
parentCommentId: string,
maxReplies: number = 100
): Promise<CommentData[]> {
const allReplies: CommentData[] = [];
let pageToken: string | undefined;
while (allReplies.length < maxReplies) {
const params = new URLSearchParams({
part: 'snippet',
parentId: parentCommentId,
maxResults: '100',
});
if (pageToken) {
params.append('pageToken', pageToken);
}
const response = await fetch(
`${YOUTUBE_API_BASE}/comments?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
if (!response.ok) {
throw new Error(`Failed to fetch replies: ${response.status}`);
}
const data = await response.json();
const replies = (data.items || []).map((reply: YouTubeComment) => ({
commentId: reply.id,
comment: decodeHtmlEntities(reply.snippet.textDisplay),
created: reply.snippet.publishedAt,
from: {
id: reply.snippet.authorChannelId?.value || '',
name: reply.snippet.authorDisplayName,
picture: reply.snippet.authorProfileImageUrl,
},
likeCount: reply.snippet.likeCount || 0,
replyCount: 0,
platform: 'youtube' as const,
platformPostId: '',
replies: [],
}));
allReplies.push(...replies);
if (!data.nextPageToken) {
break;
}
pageToken = data.nextPageToken;
}
return allReplies.slice(0, maxReplies);
}
Quota Optimization Strategies for YouTube API
Optimizing your YouTube API quota usage is critical for production applications. Here are proven strategies:
1. Request Only Required Parts
The part parameter determines which resource properties are returned. Only request what you need:
// Bad: Requesting unnecessary parts
const wastefulUrl = `${YOUTUBE_API_BASE}/commentThreads?part=id,snippet,replies&videoId=${videoId}`;
// Good: Request only what you need
const efficientUrl = `${YOUTUBE_API_BASE}/commentThreads?part=snippet&videoId=${videoId}`;
2. Batch Operations with Caching
interface CacheEntry<T> {
data: T;
timestamp: number;
expiresAt: number;
}
class CommentCache {
private cache: Map<string, CacheEntry<CommentData[]>> = new Map();
private defaultTTL: number = 5 * 60 * 1000; // 5 minutes
get(videoId: string): CommentData[] | null {
const entry = this.cache.get(videoId);
if (!entry) return null;
if (Date.now() > entry.expiresAt) {
this.cache.delete(videoId);
return null;
}
return entry.data;
}
set(videoId: string, comments: CommentData[], ttl?: number): void {
const now = Date.now();
this.cache.set(videoId, {
data: comments,
timestamp: now,
expiresAt: now + (ttl || this.defaultTTL),
});
}
invalidate(videoId: string): void {
this.cache.delete(videoId);
}
}
const commentCache = new CommentCache();
async function getVideoCommentsWithCache(
accessToken: string,
videoId: string,
options: GetCommentsOptions = {}
): Promise<GetCommentsResult> {
// Check cache first (only for first page)
if (!options.pageToken) {
const cached = commentCache.get(videoId);
if (cached) {
return {
comments: cached,
pagination: { hasMore: false },
};
}
}
const result = await getVideoComments(accessToken, videoId, options);
// Cache first page results
if (!options.pageToken) {
commentCache.set(videoId, result.comments);
}
return result;
}
3. Use Webhooks for Real-time Updates
Instead of polling for new comments, consider using YouTube's push notifications when available, or implement smart polling with exponential backoff:
interface PollingConfig {
videoId: string;
initialInterval: number;
maxInterval: number;
backoffMultiplier: number;
}
async function smartPollComments(
accessToken: string,
config: PollingConfig,
onNewComments: (comments: CommentData[]) => void
): Promise<() => void> {
let currentInterval = config.initialInterval;
let lastCommentId: string | null = null;
let timeoutId: NodeJS.Timeout;
let isRunning = true;
const poll = async () => {
if (!isRunning) return;
try {
const result = await getVideoComments(accessToken, config.videoId, { limit: 10 });
if (result.comments.length > 0) {
const newestId = result.comments[0].commentId;
if (lastCommentId && newestId !== lastCommentId) {
// Find new comments
const newComments = result.comments.filter(
c => c.commentId !== lastCommentId
);
if (newComments.length > 0) {
onNewComments(newComments);
// Reset interval on new activity
currentInterval = config.initialInterval;
}
}
lastCommentId = newestId;
}
// No new comments, increase interval
currentInterval = Math.min(
currentInterval * config.backoffMultiplier,
config.maxInterval
);
} catch (error) {
console.error('Polling error:', error);
// Increase interval on error
currentInterval = Math.min(
currentInterval * config.backoffMultiplier,
config.maxInterval
);
}
timeoutId = setTimeout(poll, currentInterval);
};
// Start polling
poll();
// Return cleanup function
return () => {
isRunning = false;
clearTimeout(timeoutId);
};
}
Error Handling for YouTube Data API Comments
Robust error handling is essential for production applications. Here's a comprehensive error handler:
interface YouTubeApiError {
type: 'auth' | 'quota' | 'not-found' | 'forbidden' | 'rate-limit' | 'unknown';
message: string;
retryable: boolean;
retryAfter?: number;
}
function parseYouTubeError(errorBody: string, statusCode: number): YouTubeApiError {
const lowerBody = errorBody.toLowerCase();
// Authentication errors
if (
lowerBody.includes('invalid_grant') ||
lowerBody.includes('token has been expired') ||
lowerBody.includes('invalid credentials') ||
statusCode === 401
) {
return {
type: 'auth',
message: 'Authentication failed. Please reconnect your YouTube account.',
retryable: false,
};
}
// Quota errors
if (lowerBody.includes('quota') || lowerBody.includes('quotaexceeded')) {
return {
type: 'quota',
message: 'YouTube API quota exceeded. Quota resets at midnight Pacific Time.',
retryable: false,
};
}
// Rate limiting
if (statusCode === 429 || lowerBody.includes('rate limit')) {
return {
type: 'rate-limit',
message: 'Rate limited. Please slow down requests.',
retryable: true,
retryAfter: 60000, // 1 minute
};
}
// Not found
if (statusCode === 404 || lowerBody.includes('videonotfound')) {
return {
type: 'not-found',
message: 'Video or comment not found.',
retryable: false,
};
}
// Forbidden
if (statusCode === 403) {
if (lowerBody.includes('commentsdisabled')) {
return {
type: 'forbidden',
message: 'Comments are disabled on this video.',
retryable: false,
};
}
return {
type: 'forbidden',
message: 'You do not have permission to perform this action.',
retryable: false,
};
}
return {
type: 'unknown',
message: `YouTube API error: ${statusCode} ${errorBody}`,
retryable: statusCode >= 500,
retryAfter: statusCode >= 500 ? 5000 : undefined,
};
}
async function makeYouTubeRequest<T>(
url: string,
options: RequestInit,
maxRetries: number = 3
): Promise<T> {
let lastError: YouTubeApiError | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
return response.json();
}
const errorBody = await response.text();
lastError = parseYouTubeError(errorBody, response.status);
if (!lastError.retryable) {
throw new Error(lastError.message);
}
// Wait before retrying
const waitTime = lastError.retryAfter || (1000 * Math.pow(2, attempt));
await new Promise(resolve => setTimeout(resolve, waitTime));
} catch (error) {
if (error instanceof Error && lastError) {
throw error;
}
// Network error, retry
if (attempt === maxRetries - 1) {
throw new Error('Network error: Unable to reach YouTube API');
}
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
}
}
throw new Error(lastError?.message || 'Max retries exceeded');
}
Using Late for YouTube Comment Management
Building and maintaining YouTube API integrations requires significant effort: handling OAuth flows, managing quota limits, implementing error recovery, and keeping up with API changes. Late simplifies this entire process with a unified API that handles these complexities for you.
With Late, you can manage YouTube comments alongside comments from other platforms through a single, consistent interface:
// With Late's unified API, managing comments across platforms is straightforward
import { LateClient } from '@late/sdk';
const late = new LateClient({
apiKey: process.env.LATE_API_KEY,
});
// Fetch comments from any connected platform
const comments = await late.comments.list({
accountId: 'your-youtube-account-id',
postId: 'video-id',
limit: 50,
});
// Reply to a comment
await late.comments.reply({
accountId: 'your-youtube-account-id',
commentId: 'comment-id',
text: 'Thanks for watching!',
});
// Works the same for TikTok, Instagram, LinkedIn, and more
Late handles:
- OAuth token management: Automatic token refresh and secure storage
- Quota optimization: Smart request batching and caching
- Error handling: Standardized error responses across platforms
- Rate limiting: Built-in retry logic with exponential backoff
- Platform differences: Unified data models for comments across YouTube, TikTok, Instagram, and other platforms
Instead of building separate integrations for each platform, Late provides a single API that abstracts away the complexity while giving you full control over your social media operations.
Ready to simplify your YouTube comment management? Get started with Late and focus on building great features instead of wrestling with API quirks.

Miquel is the founder of Late, building the most reliable social media API for developers. Previously built multiple startups and scaled APIs to millions of requests.
View all articlesLearn more about Late with AI
See what AI assistants say about Late API and this topic