Managing comments across multiple Instagram posts becomes overwhelming as your audience grows. Instagram comment moderation through the Graph API lets you build automated systems that fetch, filter, reply to, and moderate comments programmatically. This guide walks you through implementing a complete comment management solution using TypeScript.
Whether you're building a social media dashboard, a customer support tool, or an automated engagement system, understanding the Instagram comment API is essential. We'll cover everything from basic authentication to advanced moderation workflows with sentiment analysis.
API Access Requirements
Before you can automate Instagram comments, you need proper API access configured. The Instagram Graph API requires a Facebook Developer account and an approved app with specific permissions.
Required Permissions
Your app needs these OAuth scopes to work with comments:
const INSTAGRAM_SCOPES = [
'instagram_business_basic', // Basic account access
'instagram_business_content_publish', // Publish content
'instagram_business_manage_comments', // Read and manage comments
'instagram_business_manage_messages', // Required for private replies
];
Environment Setup
Create a .env file with your credentials:
INSTAGRAM_CLIENT_ID=your_client_id
INSTAGRAM_CLIENT_SECRET=your_client_secret
INSTAGRAM_ACCESS_TOKEN=your_long_lived_token
INSTAGRAM_USER_ID=your_business_account_id
Token Exchange Implementation
Instagram tokens start as short-lived (1 hour) and must be exchanged for long-lived tokens (60 days):
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
async function exchangeForLongLivedToken(
shortLivedToken: string,
clientSecret: string
): Promise<TokenResponse> {
const baseUrl = 'https://graph.instagram.com';
const response = await fetch(
`${baseUrl}/access_token?` +
`grant_type=ig_exchange_token&` +
`client_secret=${clientSecret}&` +
`access_token=${shortLivedToken}`,
{ method: 'GET' }
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
}
return response.json();
}
async function refreshLongLivedToken(
currentToken: string
): Promise<TokenResponse> {
const baseUrl = 'https://graph.instagram.com';
const response = await fetch(
`${baseUrl}/refresh_access_token?` +
`grant_type=ig_refresh_token&` +
`access_token=${currentToken}`,
{ method: 'GET' }
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
}
return response.json();
}
Note: Long-lived tokens must be refreshed before they expire. Set up a cron job to refresh tokens at least 7 days before expiration.
Fetching Comments on Posts
The foundation of any Instagram comment moderation system is retrieving comments efficiently. The API supports pagination for handling posts with thousands of comments.
Basic Comment Retrieval
interface InstagramUser {
id: string;
username: string;
}
interface InstagramComment {
commentId: string;
comment: string;
created: string;
from: InstagramUser;
likeCount: number;
replyCount: number;
hidden: boolean;
platform: 'instagram';
replies: InstagramComment[];
}
interface CommentResponse {
comments: InstagramComment[];
pagination: {
hasMore: boolean;
cursor?: string;
};
}
async function getMediaComments(
accessToken: string,
mediaId: string,
options?: { limit?: number; cursor?: string }
): Promise<CommentResponse> {
const baseUrl = 'https://graph.instagram.com';
const limit = Math.min(options?.limit || 25, 50);
let url = `${baseUrl}/${mediaId}/comments?` +
`fields=id,text,timestamp,username,hidden,` +
`from{id,username},like_count,` +
`replies{id,text,timestamp,username,hidden,from{id,username},like_count}&` +
`limit=${limit}&access_token=${accessToken}`;
if (options?.cursor) {
url += `&after=${options.cursor}`;
}
const response = await fetch(url);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to fetch comments: ${response.status} - ${errorText}`);
}
const data = await response.json();
// Build lookup map for complete comment data
const commentDataById = new Map<string, any>();
(data.data || []).forEach((comment: any) => {
commentDataById.set(comment.id, comment);
});
// Identify reply IDs to filter from top-level
const allReplyIds = new Set<string>();
(data.data || []).forEach((comment: any) => {
(comment.replies?.data || []).forEach((reply: any) => {
allReplyIds.add(reply.id);
});
});
// Filter to only top-level comments
const topLevelComments = (data.data || [])
.filter((comment: any) => !allReplyIds.has(comment.id));
const comments: InstagramComment[] = topLevelComments.map((comment: any) => ({
commentId: comment.id,
comment: comment.text,
created: comment.timestamp,
from: {
id: comment.from?.id,
username: comment.from?.username || comment.username,
},
likeCount: comment.like_count || 0,
replyCount: comment.replies?.data?.length || 0,
hidden: comment.hidden || false,
platform: 'instagram',
replies: (comment.replies?.data || []).map((reply: any) => {
const fullReplyData = commentDataById.get(reply.id) || reply;
return {
commentId: reply.id,
comment: fullReplyData.text || reply.text,
created: fullReplyData.timestamp || reply.timestamp,
from: {
id: fullReplyData.from?.id || reply.from?.id,
username: fullReplyData.from?.username || reply.from?.username,
},
likeCount: fullReplyData.like_count || reply.like_count || 0,
hidden: fullReplyData.hidden || false,
platform: 'instagram',
replies: [],
replyCount: 0,
};
}),
}));
return {
comments,
pagination: {
hasMore: !!data.paging?.next,
cursor: data.paging?.cursors?.after,
},
};
}
Fetching All Comments with Pagination
For posts with many comments, implement pagination to retrieve everything:
async function getAllComments(
accessToken: string,
mediaId: string,
maxComments: number = 1000
): Promise<InstagramComment[]> {
const allComments: InstagramComment[] = [];
let cursor: string | undefined;
while (allComments.length < maxComments) {
const response = await getMediaComments(accessToken, mediaId, {
limit: 50,
cursor,
});
allComments.push(...response.comments);
if (!response.pagination.hasMore || !response.pagination.cursor) {
break;
}
cursor = response.pagination.cursor;
// Rate limiting: wait between requests
await new Promise(resolve => setTimeout(resolve, 100));
}
return allComments.slice(0, maxComments);
}
Replying to Comments Programmatically
The ability to reply to Instagram comments API programmatically enables automated engagement, customer support bots, and scaled community management.
Public Reply Implementation
interface ReplyResult {
replyId: string;
success: boolean;
error?: string;
}
async function replyToComment(
accessToken: string,
commentId: string,
message: string
): Promise<ReplyResult> {
const baseUrl = 'https://graph.instagram.com';
try {
const response = await fetch(
`${baseUrl}/${commentId}/replies`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message,
access_token: accessToken,
}),
}
);
if (!response.ok) {
const errorText = await response.text();
return {
replyId: '',
success: false,
error: `Reply failed: ${response.status} - ${errorText}`,
};
}
const data = await response.json();
return {
replyId: data.id,
success: true,
};
} catch (error) {
return {
replyId: '',
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Private Reply (Direct Message)
Instagram allows sending a private DM to comment authors. This is useful for customer support scenarios:
interface PrivateReplyResult {
messageId: string;
success: boolean;
error?: string;
}
async function sendPrivateReply(
accessToken: string,
igUserId: string,
commentId: string,
message: string
): Promise<PrivateReplyResult> {
const baseUrl = 'https://graph.instagram.com';
try {
const response = await fetch(
`${baseUrl}/${igUserId}/messages`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
recipient: {
comment_id: commentId,
},
message: {
text: message,
},
}),
}
);
if (!response.ok) {
const errorText = await response.text();
// Handle specific error cases
if (errorText.includes('551') || errorText.toLowerCase().includes('already')) {
return {
messageId: '',
success: false,
error: 'A private reply has already been sent to this comment. Instagram only allows one private reply per comment.',
};
}
if (errorText.includes('10903')) {
return {
messageId: '',
success: false,
error: 'Comment is older than 7 days. Private replies must be sent within 7 days.',
};
}
return {
messageId: '',
success: false,
error: `Private reply failed: ${response.status} - ${errorText}`,
};
}
const data = await response.json();
return {
messageId: data.message_id || data.id,
success: true,
};
} catch (error) {
return {
messageId: '',
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Note: Instagram only allows ONE private reply per comment, and it must be sent within 7 days of the comment being posted. Plan your automation accordingly.
Hiding and Deleting Comments
Effective Instagram comment moderation requires the ability to hide inappropriate comments or delete your own replies.
Hide and Unhide Comments
async function hideComment(
accessToken: string,
commentId: string
): Promise<{ success: boolean; error?: string }> {
const baseUrl = 'https://graph.instagram.com';
try {
const response = await fetch(
`${baseUrl}/${commentId}?hide=true&access_token=${accessToken}`,
{ method: 'POST' }
);
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Hide failed: ${response.status} - ${errorText}`,
};
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async function unhideComment(
accessToken: string,
commentId: string
): Promise<{ success: boolean; error?: string }> {
const baseUrl = 'https://graph.instagram.com';
try {
const response = await fetch(
`${baseUrl}/${commentId}?hide=false&access_token=${accessToken}`,
{ method: 'POST' }
);
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Unhide failed: ${response.status} - ${errorText}`,
};
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Delete Comments
You can only delete comments you've made (your own replies):
async function deleteComment(
accessToken: string,
commentId: string
): Promise<{ success: boolean; error?: string }> {
const baseUrl = 'https://graph.instagram.com';
try {
const response = await fetch(
`${baseUrl}/${commentId}?access_token=${accessToken}`,
{ method: 'DELETE' }
);
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: `Delete failed: ${response.status} - ${errorText}`,
};
}
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Comment Filtering and Moderation Rules
Building an automated moderation system requires defining rules for what content to flag, hide, or respond to.
Moderation Rule Engine
interface ModerationRule {
id: string;
name: string;
type: 'keyword' | 'regex' | 'sentiment' | 'spam';
pattern?: string;
keywords?: string[];
action: 'hide' | 'flag' | 'delete' | 'reply';
replyTemplate?: string;
enabled: boolean;
}
interface ModerationResult {
commentId: string;
triggered: boolean;
matchedRules: string[];
action: 'hide' | 'flag' | 'delete' | 'reply' | 'none';
replyMessage?: string;
}
class CommentModerator {
private rules: ModerationRule[];
constructor(rules: ModerationRule[]) {
this.rules = rules.filter(r => r.enabled);
}
evaluate(comment: InstagramComment): ModerationResult {
const matchedRules: string[] = [];
let highestPriorityAction: ModerationResult['action'] = 'none';
let replyMessage: string | undefined;
const actionPriority = { delete: 4, hide: 3, reply: 2, flag: 1, none: 0 };
for (const rule of this.rules) {
const matches = this.checkRule(rule, comment.comment);
if (matches) {
matchedRules.push(rule.id);
if (actionPriority[rule.action] > actionPriority[highestPriorityAction]) {
highestPriorityAction = rule.action;
replyMessage = rule.replyTemplate;
}
}
}
return {
commentId: comment.commentId,
triggered: matchedRules.length > 0,
matchedRules,
action: highestPriorityAction,
replyMessage,
};
}
private checkRule(rule: ModerationRule, text: string): boolean {
const lowerText = text.toLowerCase();
switch (rule.type) {
case 'keyword':
return (rule.keywords || []).some(kw =>
lowerText.includes(kw.toLowerCase())
);
case 'regex':
if (!rule.pattern) return false;
try {
const regex = new RegExp(rule.pattern, 'i');
return regex.test(text);
} catch {
return false;
}
case 'spam':
return this.detectSpam(text);
default:
return false;
}
}
private detectSpam(text: string): boolean {
// Basic spam detection heuristics
const spamIndicators = [
// Excessive caps
text.length > 10 && (text.match(/[A-Z]/g)?.length || 0) / text.length > 0.7,
// Repeated characters
/(.)\1{4,}/.test(text),
// Too many links
(text.match(/https?:\/\//g)?.length || 0) > 2,
// Common spam phrases
/\b(free|winner|click here|dm me|check bio)\b/i.test(text),
// Excessive emojis
(text.match(/[\u{1F600}-\u{1F64F}]/gu)?.length || 0) > 10,
];
return spamIndicators.filter(Boolean).length >= 2;
}
}
// Example usage
const moderator = new CommentModerator([
{
id: 'profanity',
name: 'Profanity Filter',
type: 'keyword',
keywords: ['badword1', 'badword2'],
action: 'hide',
enabled: true,
},
{
id: 'competitor-links',
name: 'Competitor Links',
type: 'regex',
pattern: 'competitor\\.com',
action: 'hide',
enabled: true,
},
{
id: 'questions',
name: 'Auto-reply Questions',
type: 'regex',
pattern: '\\?$',
action: 'reply',
replyTemplate: 'Thanks for your question! Check our bio for more info or DM us.',
enabled: true,
},
{
id: 'spam-detection',
name: 'Spam Filter',
type: 'spam',
action: 'hide',
enabled: true,
},
]);
Bulk Operations and Automation
To automate Instagram comments at scale, you need efficient batch processing with proper rate limiting.
Batch Comment Processor
interface BatchResult {
processed: number;
succeeded: number;
failed: number;
errors: Array<{ commentId: string; error: string }>;
}
class CommentBatchProcessor {
private accessToken: string;
private rateLimitDelay: number;
constructor(accessToken: string, rateLimitDelay: number = 200) {
this.accessToken = accessToken;
this.rateLimitDelay = rateLimitDelay;
}
async processComments(
comments: InstagramComment[],
moderator: CommentModerator
): Promise<BatchResult> {
const result: BatchResult = {
processed: 0,
succeeded: 0,
failed: 0,
errors: [],
};
for (const comment of comments) {
const modResult = moderator.evaluate(comment);
if (!modResult.triggered) {
result.processed++;
result.succeeded++;
continue;
}
try {
await this.executeAction(modResult);
result.succeeded++;
} catch (error) {
result.failed++;
result.errors.push({
commentId: comment.commentId,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
result.processed++;
// Rate limiting
await new Promise(resolve => setTimeout(resolve, this.rateLimitDelay));
}
return result;
}
private async executeAction(modResult: ModerationResult): Promise<void> {
switch (modResult.action) {
case 'hide':
await hideComment(this.accessToken, modResult.commentId);
break;
case 'delete':
await deleteComment(this.accessToken, modResult.commentId);
break;
case 'reply':
if (modResult.replyMessage) {
await replyToComment(
this.accessToken,
modResult.commentId,
modResult.replyMessage
);
}
break;
case 'flag':
// Log for manual review
console.log(`Flagged comment ${modResult.commentId} for review`);
break;
}
}
}
Automated Monitoring Job
interface MonitoringConfig {
mediaIds: string[];
checkIntervalMs: number;
moderator: CommentModerator;
}
class CommentMonitor {
private accessToken: string;
private config: MonitoringConfig;
private processedComments: Set<string>;
private isRunning: boolean;
constructor(accessToken: string, config: MonitoringConfig) {
this.accessToken = accessToken;
this.config = config;
this.processedComments = new Set();
this.isRunning = false;
}
async start(): Promise<void> {
this.isRunning = true;
console.log('Comment monitor started');
while (this.isRunning) {
for (const mediaId of this.config.mediaIds) {
await this.checkMedia(mediaId);
}
await new Promise(resolve =>
setTimeout(resolve, this.config.checkIntervalMs)
);
}
}
stop(): void {
this.isRunning = false;
console.log('Comment monitor stopped');
}
private async checkMedia(mediaId: string): Promise<void> {
try {
const response = await getMediaComments(this.accessToken, mediaId, {
limit: 50,
});
const newComments = response.comments.filter(
c => !this.processedComments.has(c.commentId)
);
if (newComments.length === 0) return;
console.log(`Found ${newComments.length} new comments on ${mediaId}`);
const processor = new CommentBatchProcessor(this.accessToken);
await processor.processComments(newComments, this.config.moderator);
// Mark as processed
newComments.forEach(c => this.processedComments.add(c.commentId));
} catch (error) {
console.error(`Error checking media ${mediaId}:`, error);
}
}
}
Sentiment Analysis Integration
Adding sentiment analysis helps prioritize responses and identify unhappy customers.
interface SentimentResult {
score: number; // -1 to 1
label: 'positive' | 'neutral' | 'negative';
confidence: number; // 0 to 1
}
// Simple keyword-based sentiment (replace with ML model for production)
function analyzeSentiment(text: string): SentimentResult {
const positiveWords = [
'love', 'amazing', 'great', 'awesome', 'beautiful', 'perfect',
'thanks', 'thank you', 'excellent', 'wonderful', 'best', 'fantastic'
];
const negativeWords = [
'hate', 'terrible', 'awful', 'worst', 'bad', 'horrible',
'disappointed', 'angry', 'frustrated', 'poor', 'waste', 'scam'
];
const lowerText = text.toLowerCase();
const words = lowerText.split(/\s+/);
let positiveCount = 0;
let negativeCount = 0;
for (const word of words) {
if (positiveWords.some(pw => word.includes(pw))) positiveCount++;
if (negativeWords.some(nw => word.includes(nw))) negativeCount++;
}
const total = positiveCount + negativeCount;
if (total === 0) {
return { score: 0, label: 'neutral', confidence: 0.5 };
}
const score = (positiveCount - negativeCount) / total;
const confidence = Math.min(total / 5, 1);
let label: SentimentResult['label'];
if (score > 0.2) label = 'positive';
else if (score < -0.2) label = 'negative';
else label = 'neutral';
return { score, label, confidence };
}
// Enhanced moderation with sentiment
function createSentimentRule(): ModerationRule {
return {
id: 'negative-sentiment',
name: 'Negative Sentiment Detector',
type: 'sentiment',
action: 'flag',
enabled: true,
};
}
Building a Comment Dashboard
Here's a complete example combining all components into a dashboard API:
interface DashboardStats {
totalComments: number;
hiddenComments: number;
repliedComments: number;
sentimentBreakdown: {
positive: number;
neutral: number;
negative: number;
};
}
interface CommentWithAnalysis extends InstagramComment {
sentiment: SentimentResult;
moderationResult: ModerationResult;
}
class CommentDashboard {
private accessToken: string;
private igUserId: string;
private moderator: CommentModerator;
constructor(
accessToken: string,
igUserId: string,
moderator: CommentModerator
) {
this.accessToken = accessToken;
this.igUserId = igUserId;
this.moderator = moderator;
}
async getCommentsWithAnalysis(
mediaId: string
): Promise<CommentWithAnalysis[]> {
const response = await getMediaComments(this.accessToken, mediaId, {
limit: 50,
});
return response.comments.map(comment => ({
...comment,
sentiment: analyzeSentiment(comment.comment),
moderationResult: this.moderator.evaluate(comment),
}));
}
async getStats(mediaId: string): Promise<DashboardStats> {
const comments = await this.getCommentsWithAnalysis(mediaId);
const stats: DashboardStats = {
totalComments: comments.length,
hiddenComments: comments.filter(c => c.hidden).length,
repliedComments: comments.filter(c => c.replyCount > 0).length,
sentimentBreakdown: {
positive: 0,
neutral: 0,
negative: 0,
},
};
comments.forEach(c => {
stats.sentimentBreakdown[c.sentiment.label]++;
});
return stats;
}
async moderateAll(mediaId: string): Promise<BatchResult> {
const response = await getMediaComments(this.accessToken, mediaId);
const processor = new CommentBatchProcessor(this.accessToken);
return processor.processComments(response.comments, this.moderator);
}
}
Rate Limits and Quotas
Understanding Instagram's rate limits is crucial for any Instagram comment API integration.
| Endpoint | Rate Limit | Window |
|---|---|---|
| GET comments | 200 calls | 1 hour |
| POST reply | 60 calls | 1 hour |
| POST hide/unhide | 60 calls | 1 hour |
| DELETE comment | 60 calls | 1 hour |
| Private messages | 100 calls | 24 hours |
Rate Limit Handler
class RateLimitedClient {
private requestCounts: Map<string, { count: number; resetTime: number }>;
private limits: Map<string, { max: number; windowMs: number }>;
constructor() {
this.requestCounts = new Map();
this.limits = new Map([
['GET', { max: 200, windowMs: 3600000 }],
['POST', { max: 60, windowMs: 3600000 }],
['DELETE', { max: 60, windowMs: 3600000 }],
]);
}
async executeWithRateLimit<T>(
method: string,
fn: () => Promise<T>
): Promise<T> {
const limit = this.limits.get(method) || { max: 60, windowMs: 3600000 };
const key = method;
const now = Date.now();
let state = this.requestCounts.get(key);
if (!state || now > state.resetTime) {
state = { count: 0, resetTime: now + limit.windowMs };
this.requestCounts.set(key, state);
}
if (state.count >= limit.max) {
const waitTime = state.resetTime - now;
console.log(`Rate limit reached for ${method}. Waiting ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
state.count = 0;
state.resetTime = Date.now() + limit.windowMs;
}
state.count++;
return fn();
}
}
Late's Comment Management Solution
Building and maintaining a robust Instagram comment moderation system requires significant engineering effort. Token management, rate limiting, error handling, and keeping up with API changes all demand ongoing attention.
Late provides a unified API that handles these complexities for you:
- Unified Comment API: Manage comments across Instagram, TikTok, YouTube, and more through a single interface
- Automatic Token Refresh: Never worry about expired tokens again
- Built-in Rate Limiting: Smart request queuing that respects platform limits
- Webhook Support: Real-time notifications for new comments
- Moderation Tools: Pre-built filtering and auto-reply capabilities
Instead of maintaining separate integrations for each platform, Late's SDK lets you focus on building your product:
import { Late } from '@anthropic/late-sdk';
const late = new Late({ apiKey: process.env.LATE_API_KEY });
// Fetch comments from any platform with one API
const comments = await late.comments.list({
accountId: 'your-instagram-account',
postId: 'media-id',
});
// Reply works the same across platforms
await late.comments.reply({
commentId: comments[0].id,
message: 'Thanks for your comment!',
});
Check out Late's documentation to see how you can simplify your social media integrations and ship faster.

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