TL;DR
The Instagram Graph API lets you manage Instagram Business and Creator accounts programmatically. Use Facebook Login OAuth 2.0 for authentication, then publish content, retrieve insights, manage comments, and receive webhook events. Plan around the documented rate limit of 200 calls per hour per app.
Introduction
Instagram has over 2 billion monthly active users, and more than 200 million businesses use Instagram Business accounts. If you are building a social media scheduler, analytics dashboard, e-commerce workflow, or moderation tool, the Instagram Graph API provides the integration layer.
A production integration can automate publishing, comment moderation, sentiment workflows, and reporting across multiple accounts. This guide covers the implementation path:
- Configure Facebook Login OAuth.
- Retrieve the connected Instagram account ID.
- Publish photos, videos, reels, and carousels.
- Query media and account insights.
- Manage comments.
- Process real-time webhook events.
- Handle rate limits and production concerns.
💡 Apidog can help test Instagram endpoints, validate OAuth flows, inspect API responses, mock responses, and share API test scenarios with your team.
What Is the Instagram Graph API?
Instagram Graph API provides programmatic access to Instagram Business and Creator accounts through the Facebook Graph API.
Use it for:
- Publishing photos, videos, reels, and carousels
- Retrieving media insights and account analytics
- Managing comments and mentions
- Direct messaging through Instagram Graph API and Messenger Platform
- Tracking hashtags and mentions
- Managing stories
- Working with shopping and product tags
Key features
| Feature | Description |
|---|---|
| Graph-based API | Node-based resource access |
| OAuth 2.0 | Facebook Login authentication |
| Webhooks | Real-time notifications for comments and mentions |
| Rate limiting | 200 calls per hour per app |
| Content publishing | Photos, videos, reels, and carousels |
| Insights | Engagement, reach, and impression metrics |
| Moderation | Comments, mentions, and message management |
Account requirements
| Account type | API access |
|---|---|
| Business Account | Full API access |
| Creator Account | Full API access |
| Personal Account | No API access; convert first |
| Private Account | Limited insights |
API architecture
Instagram uses the Facebook Graph API URL structure:
https://graph.facebook.com/v18.0/
API versions
| Version | Status | End date | Use case |
|---|---|---|---|
| v18.0 | Current | March 2026 | New integrations |
| v17.0 | Deprecated | January 2026 | Existing integrations |
| v16.0 | Retired | Expired | Do not use |
Facebook releases new versions quarterly. Target the latest stable version for new work.
Getting Started: Authentication Setup
Step 1: Create a Facebook app
- Visit the Facebook Developers Portal.
- Sign in with your Facebook account.
- Create a Facebook App with the Business app type.
- Add the Instagram Graph API product.
Step 2: Link an Instagram Business account
Connect the Instagram account to a Facebook Page:
- Open Facebook Page Settings > Instagram.
- Select Connect Account.
- Log in to Instagram and authorize the connection.
- Confirm that the Instagram Business or Creator account is linked.
Personal Instagram accounts cannot use the Graph API. Convert the account to Business or Creator in Instagram Settings before continuing.
Step 3: Start Facebook Login
Build an OAuth authorization URL with the permissions your app needs:
const FB_APP_ID = process.env.FB_APP_ID;
const FB_REDIRECT_URI = process.env.FB_REDIRECT_URI;
function getAuthUrl(state) {
const params = new URLSearchParams({
client_id: FB_APP_ID,
redirect_uri: FB_REDIRECT_URI,
scope: [
'instagram_basic',
'instagram_content_publish',
'instagram_manage_comments',
'instagram_manage_insights',
'pages_read_engagement'
].join(','),
state
});
return `https://www.facebook.com/v18.0/dialog/oauth?${params.toString()}`;
}
Redirect the user to getAuthUrl(state). After consent, Facebook redirects back to your FB_REDIRECT_URI with an authorization code that you exchange for an access token.
Required permissions
| Permission | Description |
|---|---|
instagram_basic |
Basic profile information and media list |
instagram_content_publish |
Publish photos, videos, and carousels |
instagram_manage_comments |
Read and write comments |
instagram_manage_insights |
Access analytics data |
pages_read_engagement |
Page access for publishing |
pages_manage_posts |
Publish to the connected Page |
Step 4: Exchange for a long-lived token
Short-lived tokens expire in one hour. Exchange them for long-lived tokens, which expire after 60 days.
const FB_APP_SECRET = process.env.FB_APP_SECRET;
async function exchangeForLongLivedToken(shortLivedToken) {
const params = new URLSearchParams({
grant_type: 'fb_exchange_token',
client_id: FB_APP_ID,
client_secret: FB_APP_SECRET,
fb_exchange_token: shortLivedToken
});
const response = await fetch(
`https://graph.facebook.com/v18.0/oauth/access_token?${params}`
);
if (!response.ok) {
throw new Error(`Token exchange failed: ${await response.text()}`);
}
return response.json();
}
// Usage
const longLivedToken = await exchangeForLongLivedToken(shortLivedToken);
console.log(
`Token expires: ${new Date(longLivedToken.expires_at * 1000)}`
);
Store the token encrypted and persist its expiration date so you can alert users before it expires.
Step 5: Retrieve the Instagram Business account ID
Fetch the Instagram account connected to a Facebook Page:
async function getInstagramAccountId(pageId, accessToken) {
const params = new URLSearchParams({
fields: 'instagram_business_account',
access_token: accessToken
});
const response = await fetch(
`https://graph.facebook.com/v18.0/${pageId}?${params}`
);
if (!response.ok) {
throw new Error(`Failed to fetch Instagram account: ${await response.text()}`);
}
const data = await response.json();
return data.instagram_business_account.id;
}
// Usage
const igAccountId = await getInstagramAccountId(
'12345678',
process.env.INSTAGRAM_ACCESS_TOKEN
);
console.log(`Instagram Account ID: ${igAccountId}`);
Step 6: Create a reusable API client
Use one helper for GET, POST, and DELETE requests. The Graph API accepts request parameters as URL-encoded form data for POST requests.
const IG_BASE_URL = 'https://graph.facebook.com/v18.0';
async function instagramRequest(endpoint, { method = 'GET', ...params } = {}) {
const url = new URL(`${IG_BASE_URL}${endpoint}`);
const accessToken = process.env.INSTAGRAM_ACCESS_TOKEN;
if (method === 'GET') {
url.searchParams.set('access_token', accessToken);
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
}
}
const options = { method };
if (method !== 'GET') {
const body = new URLSearchParams({
access_token: accessToken
});
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
body.set(key, String(value));
}
}
options.body = body;
}
const response = await fetch(url, options);
const data = await response.json();
if (!response.ok) {
throw new Error(
`Instagram API Error: ${data.error?.message ?? 'Unknown error'}`
);
}
return data;
}
// Usage
const account = await instagramRequest('/me');
console.log(`Instagram Account: ${account.username}`);
Content Publishing
Content publishing uses two steps:
- Create a media container with
/{ig-account-id}/media. - Publish that container with
/{ig-account-id}/media_publish.
Publish a photo
async function publishPhoto(igAccountId, photoData) {
const container = await instagramRequest(`/${igAccountId}/media`, {
method: 'POST',
image_url: photoData.imageUrl,
caption: photoData.caption,
location_id: photoData.locationId,
is_carousel_item: 'false'
});
return instagramRequest(`/${igAccountId}/media_publish`, {
method: 'POST',
creation_id: container.id
});
}
// Usage
const post = await publishPhoto('17841400000000000', {
imageUrl: 'https://example.com/image.jpg',
caption: 'Excited to announce our new product! 🚀 #launch #innovation',
locationId: '123456789'
});
console.log(`Published media ID: ${post.id}`);
Publish a video or reel
Videos must finish processing before publishing. Poll the media container status before calling media_publish.
async function waitForVideoProcessing(creationId, maxAttempts = 30) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const status = await instagramRequest(`/${creationId}`, {
fields: 'status_code'
});
if (status.status_code === 'FINISHED') {
return;
}
if (status.status_code === 'EXPIRED') {
throw new Error('Video processing expired');
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
throw new Error('Video processing timeout');
}
async function publishVideo(igAccountId, videoData) {
const container = await instagramRequest(`/${igAccountId}/media`, {
method: 'POST',
video_url: videoData.videoUrl,
cover_url: videoData.coverUrl,
caption: videoData.caption,
media_type: videoData.mediaType ?? 'REELS',
share_to_feed: 'true'
});
await waitForVideoProcessing(container.id);
return instagramRequest(`/${igAccountId}/media_publish`, {
method: 'POST',
creation_id: container.id
});
}
// Usage
const reel = await publishVideo('17841400000000000', {
videoUrl: 'https://example.com/video.mp4',
coverUrl: 'https://example.com/cover.jpg',
caption: 'A product demo in 30 seconds.',
mediaType: 'REELS'
});
console.log(`Published reel ID: ${reel.id}`);
Publish a carousel
Create a container for every child item, create a carousel container that references them, then publish the carousel container.
async function publishCarousel(igAccountId, carouselData) {
const children = [];
for (const item of carouselData.items) {
const container = await instagramRequest(`/${igAccountId}/media`, {
method: 'POST',
[item.type === 'video' ? 'video_url' : 'image_url']: item.url,
caption: item.caption || '',
is_carousel_item: 'true'
});
children.push(container.id);
}
const carouselContainer = await instagramRequest(`/${igAccountId}/media`, {
method: 'POST',
media_type: 'CAROUSEL',
children: children.join(','),
caption: carouselData.caption
});
return instagramRequest(`/${igAccountId}/media_publish`, {
method: 'POST',
creation_id: carouselContainer.id
});
}
// Usage
const carousel = await publishCarousel('17841400000000000', {
caption: 'Product showcase 2026',
items: [
{
type: 'image',
url: 'https://example.com/img1.jpg',
caption: 'Product 1'
},
{
type: 'image',
url: 'https://example.com/img2.jpg',
caption: 'Product 2'
},
{
type: 'video',
url: 'https://example.com/vid1.mp4',
caption: 'Demo'
}
]
});
console.log(`Published carousel ID: ${carousel.id}`);
Media types
| Media type | Parameters | Use case |
|---|---|---|
IMAGE |
image_url, caption
|
Photo posts |
VIDEO |
video_url, cover_url, caption
|
Video posts |
REELS |
video_url, cover_url, caption, share_to_feed
|
Reels |
CAROUSEL |
children, caption
|
Multiple media items |
Retrieving Media and Insights
Get published media
async function getUserMedia(igAccountId, limit = 25) {
return instagramRequest(`/${igAccountId}/media`, {
fields: [
'id',
'caption',
'media_type',
'media_url',
'permalink',
'timestamp',
'like_count',
'comments_count'
].join(','),
limit
});
}
// Usage
const media = await getUserMedia('17841400000000000');
for (const item of media.data) {
console.log(`${item.media_type}: ${item.caption}`);
console.log(`Likes: ${item.like_count}, Comments: ${item.comments_count}`);
console.log(`URL: ${item.permalink}`);
}
Get media insights
async function getMediaInsights(mediaId) {
return instagramRequest(`/${mediaId}/insights`, {
fields: 'impressions,reach,engagement,saved,video_views,profile_visits,follows'
});
}
// Usage
const insights = await getMediaInsights('17890000000000000');
for (const metric of insights.data) {
console.log(`${metric.name}: ${metric.values[0].value}`);
}
Available media metrics
| Metric | Description | Media types |
|---|---|---|
impressions |
Total views | All |
reach |
Unique accounts reached | All |
engagement |
Likes, comments, and saves | All |
saved |
Times saved | All |
video_views |
Video views of 3+ seconds | Video, Reels |
plays |
Total video plays | Video, Reels |
profile_visits |
Profile visits from a post | All |
follows |
Follows from a post | All |
comments |
Comment count | All |
like_count |
Like count | All |
Get account insights
async function getAccountInsights(
igAccountId,
metricNames,
since = null,
until = null
) {
const params = {
metric: metricNames.join(','),
period: 'day'
};
if (since) params.since = since;
if (until) params.until = until;
return instagramRequest(`/${igAccountId}/insights`, params);
}
// Usage: get 30 days of metrics
const accountInsights = await getAccountInsights(
'17841400000000000',
[
'impressions',
'reach',
'profile_views',
'email_contacts',
'website_clicks'
],
'2026-02-23',
'2026-03-25'
);
for (const metric of accountInsights.data) {
console.log(`${metric.name}:`);
for (const value of metric.values) {
console.log(` ${value.end_time}: ${value.value}`);
}
}
Account-level metrics
| Metric | Description |
|---|---|
impressions |
Total profile and content views |
reach |
Unique accounts reached |
profile_views |
Profile visits |
website_clicks |
Link-in-bio clicks |
email_contacts |
Email button taps |
phone_call_clicks |
Phone button taps |
text_message_clicks |
SMS button taps |
get_directions_clicks |
Address clicks |
follower_count |
Total followers |
audience_city |
Follower cities |
audience_country |
Follower countries |
audience_gender_age |
Demographic breakdown |
Comment Management
Get comments for a media item
async function getMediaComments(mediaId, limit = 50) {
return instagramRequest(`/${mediaId}/comments`, {
fields: 'id,text,timestamp,username,hidden',
limit
});
}
// Usage
const comments = await getMediaComments('17890000000000000');
for (const comment of comments.data) {
console.log(`@${comment.username}: ${comment.text}`);
console.log(`Hidden: ${comment.hidden}`);
}
Reply to a comment
async function replyToComment(mediaId, commentId, replyText) {
return instagramRequest(`/${mediaId}/comments`, {
method: 'POST',
response_to: commentId,
message: replyText
});
}
// Usage
const reply = await replyToComment(
'17890000000000000',
'17900000000000000',
'Thank you for your interest! Check your DM for details.'
);
console.log(`Reply posted: ${reply.id}`);
Hide a comment
async function hideComment(commentId) {
return instagramRequest(`/${commentId}`, {
method: 'POST',
hide: 'true'
});
}
// Usage
await hideComment('17900000000000000');
console.log('Comment hidden');
Delete a comment
async function deleteComment(commentId) {
await instagramRequest(`/${commentId}`, {
method: 'DELETE'
});
console.log('Comment deleted');
}
Webhooks
Webhooks reduce polling and let your application react to comments, mentions, and message reactions in real time.
Configure webhook subscriptions
async function subscribeToWebhooks(appId, accessToken) {
const response = await fetch(
`https://graph.facebook.com/v18.0/${appId}/subscriptions`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
object: 'instagram',
callback_url: 'https://myapp.com/webhooks/instagram',
verify_token: process.env.WEBHOOK_VERIFY_TOKEN,
access_token: accessToken,
fields: ['comments', 'mentions', 'message_reactions']
})
}
);
return response.json();
}
Verify and handle webhook events
Return the challenge during the verification request. For incoming events, validate the event object and acknowledge the request after processing.
const express = require('express');
const app = express();
app.get('/webhooks/instagram', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === process.env.WEBHOOK_VERIFY_TOKEN) {
console.log('Webhook verified');
return res.status(200).send(challenge);
}
return res.status(403).send('Verification failed');
});
app.post('/webhooks/instagram', express.json(), async (req, res) => {
const body = req.body;
if (body.object !== 'instagram') {
return res.status(404).send('Not found');
}
for (const entry of body.entry) {
for (const change of entry.changes) {
switch (change.field) {
case 'comments':
await handleNewComment(change.value);
break;
case 'mentions':
await handleMention(change.value);
break;
case 'message_reactions':
await handleReaction(change.value);
break;
}
}
}
return res.status(200).send('OK');
});
async function handleNewComment(data) {
console.log(`New comment on media ${data.media_id}`);
console.log(`From: ${data.from_id}`);
console.log(`Text: ${data.text}`);
if (isSpam(data.text)) {
await hideComment(data.id);
}
}
Webhook fields
| Field | Trigger |
|---|---|
comments |
New comment or reply |
mentions |
A user mentions the account |
message_reactions |
Reaction to a story |
story_status |
Story reply or view |
Rate Limiting
Understand the limits
Instagram Graph API enforces:
- 200 calls per hour per app, shared across users
- 200 Business Discovery calls per hour per user
- Content publishing limits based on action type
Exceeding a limit results in HTTP 400 with error subcode 613.
Apply rate-limit best practices
- Cache responses instead of repeatedly fetching unchanged data.
- Use field expansion to reduce request counts.
- Prefer webhooks over polling.
- Queue requests when processing many accounts.
- Retry transient failures with exponential backoff.
async function makeRateLimitedRequest(
endpoint,
params = {},
maxRetries = 3
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await instagramRequest(endpoint, params);
} catch (error) {
const isRateLimited =
error.message.includes('429') || error.message.includes('613');
if (!isRateLimited || attempt === maxRetries) {
throw error;
}
const delay = 2 ** attempt * 1000;
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
Troubleshooting Common Issues
OAuth token expired
Symptoms: Invalid OAuth access token errors.
Fix:
- Track the 60-day token expiration time.
- Alert users before expiration.
- Implement token refresh before expiry.
- Re-authenticate the user if the token has expired.
Media publishing fails
Symptoms: The publishing request returns an error.
Fix:
- Ensure the image or video URL is publicly accessible without authentication.
- Verify image format is JPEG or PNG and smaller than 8 MB.
- Verify video format is MP4, smaller than 1 GB, and shorter than 90 seconds.
- Poll video processing status before publishing the container.
Insights are unavailable
Symptoms: The Insights API returns empty data.
Fix:
- Confirm that the account is Business or Creator, not Personal.
- Allow 24–48 hours for insights to populate.
- Check that the account has sufficient activity.
Production Deployment Checklist
Before going live:
- [ ] Convert all test accounts to Business or Creator accounts.
- [ ] Implement OAuth 2.0 with long-lived tokens.
- [ ] Encrypt tokens at rest.
- [ ] Track expiration and implement token refresh.
- [ ] Expose webhook endpoints over HTTPS.
- [ ] Add request queuing and rate-limit handling.
- [ ] Implement structured API error handling.
- [ ] Log API requests and responses securely.
- [ ] Create content moderation workflows.
- [ ] Test across multiple account types.
Real-World Use Cases
Social media scheduling tool
A marketing platform can automate posting across 50+ client accounts.
Implementation approach:
- Build a content calendar with drag-and-drop scheduling.
- Store approved media URLs and captions.
- Create a publishing job for each scheduled item.
- Publish photos, videos, reels, and carousels through the content publishing flow.
- Add hashtag suggestions based on content.
Outcome: Less manual publishing work and a more consistent posting schedule.
Customer service automation
An e-commerce brand can respond to common questions through comment webhooks.
Implementation approach:
- Subscribe to the
commentswebhook field. - Detect keywords such as price, availability, and shipping.
- Reply with product links for known questions.
- Route complex questions to a human support queue.
- Hide spam or inappropriate comments automatically.
Outcome: Faster responses while reserving human support time for complex requests.
Conclusion
The Instagram Graph API provides programmatic access to Instagram Business and Creator account workflows.
Key implementation takeaways:
- Authenticate with Facebook Login OAuth 2.0 and use 60-day long-lived tokens.
- Publish photos, videos, reels, and carousels through media containers.
- Use media and account insights for engagement and reach reporting.
- Use webhooks for real-time comment and mention handling.
- Design around the 200-calls-per-hour-per-app rate limit.
- Use API testing and collaboration tools such as Apidog to validate requests and OAuth flows.
FAQ
How do I get access to the Instagram API?
Create a Facebook Developer account, create a Business app, add the Instagram Graph API product, and authenticate users through Facebook Login with the required permissions.
Can I post to Instagram automatically?
Yes. Use the Content Publishing API to publish photos, videos, reels, and carousels for Business and Creator accounts.
What Instagram account types support the API?
Business and Creator accounts have full API access. Personal accounts have limited or no API access.
How do I get Instagram comments?
Use the /{media-id}/comments endpoint to retrieve comments for a specific media item. Use webhooks for real-time comment notifications.
What are Instagram rate limits?
The Instagram Graph API allows 200 calls per hour per app. Some endpoints also have additional per-user limits.
Can I publish Stories through the API?
Yes. Stories can be published through the same content publishing flow used for feed posts.
How do I access Instagram Insights?
Request instagram_manage_insights during OAuth, then use the Insights endpoint to retrieve media and account metrics.
Can I reply to comments automatically?
Yes. Use the Comments API to create replies. This is commonly used for automated customer service workflows.
Top comments (0)