Automate Instagram analytics using the Graph API's Insights endpoints: GET /{ig-user-id}/insights for account-level metrics (impressions, reach, profile_views with period=day) and GET /{ig-media-id}/insights for post-level metrics (impressions, reach, likes, comments, shares, saved). The instagram_business_manage_insights scope requires Meta App Review (2-4 weeks). Several metrics were deprecated in January 2025 — video_views, website_clicks, phone_call_clicks no longer return data.
API Quick Reference
OAuth 2.0
200 calls/hour per user
JSON
Available
Understanding the Instagram Insights API
The Instagram Graph API v25.0 provides two levels of analytics: media-level insights for individual posts (impressions, reach, likes, comments, shares, saved) and account-level insights for the overall profile (impressions, reach, profile_views with period=day or period=week). Both are accessed through GET requests with an access token that has the instagram_business_manage_insights scope.
Important: several metrics were deprecated in January 2025 starting with API version v21. The following no longer return data for any request: video_views (for non-Reels content), email_contacts, profile_views (time series), website_clicks, phone_call_clicks, and text_message_clicks. If your existing automation references any of these, it will receive empty or error responses. Replace video_views with plays for Reels content.
The biggest access hurdle is App Review. The instagram_business_manage_insights permission requires submitting a separate review request with a screencast demonstrating your app's use of insights data. This review typically takes 2-4 weeks. While waiting, you can test with accounts that you've added as Test Users in your App Dashboard. Official docs: https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-user/insights
https://graph.instagram.com/v25.0Setting Up Instagram Analytics API Authentication
The Insights API requires OAuth 2.0 long-lived User Access Tokens. You need two scopes: instagram_business_basic for account info and instagram_business_manage_insights for analytics data. Both require separate App Review submissions. During development, add test accounts via App Dashboard > Roles > Test Users.
- 1Go to developers.facebook.com and create a Business type App
- 2Add the Instagram product to your app
- 3In App Review > Permissions and Features, request instagram_business_basic and instagram_business_manage_insights
- 4For App Review, prepare a screencast showing how your app displays insights data to users
- 5Implement the OAuth flow with scope=instagram_business_basic,instagram_business_manage_insights
- 6Redirect to: https://www.facebook.com/v25.0/dialog/oauth?client_id={app-id}&redirect_uri={uri}&scope=instagram_business_basic,instagram_business_manage_insights
- 7Exchange the authorization code for a short-lived token at https://graph.facebook.com/v25.0/oauth/access_token
- 8Exchange for a long-lived 60-day token: GET https://graph.instagram.com/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={secret}&fb_exchange_token={short-token}
1import requests2import os34APP_ID = os.environ['INSTAGRAM_APP_ID']5APP_SECRET = os.environ['INSTAGRAM_APP_SECRET']6ACCESS_TOKEN = os.environ['IG_ACCESS_TOKEN']7IG_USER_ID = os.environ['IG_USER_ID']89def get_long_lived_token(short_lived_token):10 resp = requests.get(11 'https://graph.instagram.com/access_token',12 params={13 'grant_type': 'fb_exchange_token',14 'client_id': APP_ID,15 'client_secret': APP_SECRET,16 'fb_exchange_token': short_lived_token,17 }18 )19 resp.raise_for_status()20 return resp.json()['access_token']2122def verify_insights_access():23 """Test that the token has instagram_business_manage_insights scope."""24 resp = requests.get(25 f'https://graph.instagram.com/v25.0/{IG_USER_ID}/insights',26 params={27 'metric': 'impressions',28 'period': 'day',29 'access_token': ACCESS_TOKEN,30 }31 )32 if resp.status_code == 403:33 raise PermissionError('Missing instagram_business_manage_insights scope. App Review required.')34 resp.raise_for_status()35 print('Insights access confirmed')36 return TrueSecurity notes
- •Store access tokens in environment variables or a secrets manager, never in source code
- •The instagram_business_manage_insights scope grants read access to all post and account metrics — treat it as sensitive data
- •Never log raw API responses that contain user metrics — they may be considered PII in some jurisdictions
- •Implement token refresh automation (every 50 days) before the 60-day expiry
- •Restrict your App's OAuth redirect URIs to only your production and staging domains
Key endpoints
/{ig-user-id}/insightsReturns account-level metrics aggregated over a time period. Available metrics: impressions, reach, profile_views. Use period=day for daily breakdowns or period=week/days_28 for aggregates. Note: profile_views (time series) was deprecated in January 2025 — only the scalar form works now.
| Parameter | Type | Required | Description |
|---|---|---|---|
metric | string | required | Comma-separated list. Available (non-deprecated): impressions, reach, profile_views. Use 'impressions,reach,profile_views' for a full daily report. |
period | string | required | Time aggregation: 'day' (daily), 'week' (7-day sum), 'days_28' (28-day sum), 'month', 'lifetime'. |
since | string | optional | Unix timestamp or ISO date string for the start of the range (e.g., '2026-05-01'). |
until | string | optional | Unix timestamp or ISO date string for the end of the range. |
Response
1{"data": [{"name": "impressions", "period": "day", "values": [{"value": 1420, "end_time": "2026-05-06T07:00:00+0000"}, {"value": 1385, "end_time": "2026-05-07T07:00:00+0000"}], "title": "Impressions", "id": "17841405822304915/insights/impressions/day"}], "paging": {"previous": "https://...", "next": "https://..."}}/{ig-media-id}/insightsReturns metrics for a specific post, Reel, or Story. Available metrics vary by media type. For feed posts and Reels: impressions, reach, likes, comments, shares, saved. For Reels specifically: plays is available. Deprecated: video_views (use plays for Reels).
| Parameter | Type | Required | Description |
|---|---|---|---|
metric | string | required | For feed posts: impressions,reach,likes,comments,shares,saved. For Reels: impressions,reach,plays,likes,comments,shares,saved. Do NOT include video_views or website_clicks (deprecated Jan 2025). |
period | string | optional | Use 'lifetime' for total metrics on a post. This is the most common period for media insights. |
Response
1{"data": [{"name": "impressions", "period": "lifetime", "values": [{"value": 4230}], "title": "Impressions", "id": "17895695668004550/insights/impressions/lifetime"}, {"name": "reach", "period": "lifetime", "values": [{"value": 3100}], "title": "Reach", "id": "17895695668004550/insights/reach/lifetime"}, {"name": "saved", "period": "lifetime", "values": [{"value": 87}], "title": "Saves"}]}/{ig-user-id}/mediaReturns a list of the account's recent media objects with basic stats. Use this to get media IDs for subsequent insights queries. Fields id, caption, media_type, timestamp, like_count, and comments_count are available without the insights scope.
| Parameter | Type | Required | Description |
|---|---|---|---|
fields | string | optional | Use 'id,caption,media_type,timestamp,like_count,comments_count' to get media list with basic engagement data. |
limit | number | optional | Max posts to return per page. Default 12, max 100. Use limit=20 for a weekly top-posts report. |
Response
1{"data": [{"id": "17895695668004550", "caption": "Beautiful sunset", "media_type": "IMAGE", "timestamp": "2026-05-05T14:30:00+0000", "like_count": 342, "comments_count": 18}], "paging": {"cursors": {"after": "abc123"}}}Step-by-step automation
Fetch Account-Level Insights for the Report Period
Why: Account-level metrics give you the overall reach and visibility of your Instagram presence, which is the top-level KPI for any analytics report.
Call GET /{ig-user-id}/insights with metric=impressions,reach,profile_views and period=day. Specify since/until to define your report window. The response includes a daily time series for each metric. Extract and aggregate these into summary totals for your report.
1curl -s "https://graph.instagram.com/v25.0/${IG_USER_ID}/insights?metric=impressions,reach,profile_views&period=day&since=2026-04-30&until=2026-05-07&access_token=${ACCESS_TOKEN}"Pro tip: profile_views is only available with period=day or period=week — not lifetime. Request 8 days of data (not 7) to ensure you have a full 7-day window accounting for timezone offsets in the API's response.
Expected result: Object with total impressions, reach, and profile_views for the specified period, e.g. {impressions: 9850, reach: 6200, profile_views: 430}.
Get Recent Posts and Their Media IDs
Why: You need media IDs to fetch per-post insights; the /media endpoint gives you recent posts along with basic counts without requiring the insights scope.
Call GET /{ig-user-id}/media with fields=id,caption,media_type,timestamp,like_count,comments_count. Limit to your top 20 most recent posts for a weekly report — fetching insights for every post ever would exhaust your 200 calls/hour quota. Sort by like_count to get your top performers.
1curl -s "https://graph.instagram.com/v25.0/${IG_USER_ID}/media?fields=id,caption,media_type,timestamp,like_count,comments_count&limit=20&access_token=${ACCESS_TOKEN}"Pro tip: Only fetch insights for your top 10 posts by engagement — getting insights for all 20 costs 20 API calls, but your bottom 10 posts rarely need deep insight data for a weekly report.
Expected result: Array of up to 20 posts sorted by engagement, each with id, like_count, comments_count, media_type, and timestamp.
Fetch Per-Post Insights for Top Posts
Why: Post-level insights (impressions, reach, saves, shares) reveal which content actually drove discovery versus just getting engagement from existing followers.
For each media ID from Step 2, call GET /{ig-media-id}/insights with the appropriate metrics for its type. Use impressions,reach,likes,comments,shares,saved for all post types; add plays for Reels. Avoid the deprecated metrics: video_views, website_clicks, phone_call_clicks. Add a small delay between calls to avoid burning rate limit.
1curl -s "https://graph.instagram.com/v25.0/${MEDIA_ID}/insights?metric=impressions,reach,likes,comments,shares,saved&period=lifetime&access_token=${ACCESS_TOKEN}"Pro tip: The saves metric is often the most valuable signal for content quality — it indicates users want to refer back to the post. Sort your report by saves/reach ratio to find your highest-quality content.
Expected result: Array of posts enriched with insight metrics: each object contains the original post data plus impressions, reach, likes, comments, shares, saved (and plays for Reels).
Generate and Deliver the Analytics Report
Why: Raw API data is not a report — aggregating, calculating engagement rate, and formatting it for delivery is what makes automation valuable.
Calculate key metrics: total engagement (likes + comments + shares + saves), engagement rate (engagement/reach), and save rate. Format as a structured report and deliver to email, Slack, or Google Sheets. Scheduling this step as a weekly cron job (e.g., every Monday at 9am) creates an automated weekly analytics digest.
1# After collecting data, send to Slack2curl -X POST "${SLACK_WEBHOOK_URL}" \3 -H 'Content-type: application/json' \4 --data '{"text":"Instagram Weekly Report: 9,850 impressions, 6,200 reach, top post saved 87 times"}'Pro tip: Calculate save rate (saves/reach × 100) in addition to engagement rate — it's the best leading indicator of content quality since saves represent intent to return.
Expected result: A formatted analytics report delivered to Slack, email, or logged to console with account-level totals and top post breakdowns.
Complete working code
This script fetches a complete weekly Instagram analytics report: account-level metrics (impressions, reach, profile_views) and post-level metrics for the top 10 most recent posts. Outputs a structured report and optionally posts it to Slack. Run as a weekly cron job.
1#!/usr/bin/env python32"""Instagram Weekly Analytics Report - run as weekly cron job."""3import os4import time5import json6import logging7import requests8from datetime import datetime, timedelta910logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')11log = logging.getLogger(__name__)1213IG_USER_ID = os.environ['IG_USER_ID']14ACCESS_TOKEN = os.environ['IG_ACCESS_TOKEN']15SLACK_WEBHOOK = os.environ.get('SLACK_WEBHOOK_URL')16BASE = 'https://graph.instagram.com/v25.0'1718def ig_get(path, params):19 params['access_token'] = ACCESS_TOKEN20 r = requests.get(f'{BASE}{path}', params=params, timeout=30)21 r.raise_for_status()22 return r.json()2324def get_account_insights():25 since = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')26 until = datetime.now().strftime('%Y-%m-%d')27 data = ig_get(f'/{IG_USER_ID}/insights', {28 'metric': 'impressions,reach,profile_views',29 'period': 'day',30 'since': since,31 'until': until,32 })33 return {item['name']: sum(v['value'] for v in item['values']) for item in data['data']}3435def get_top_posts(limit=20):36 data = ig_get(f'/{IG_USER_ID}/media', {37 'fields': 'id,caption,media_type,timestamp,like_count,comments_count',38 'limit': limit,39 })40 posts = data['data']41 posts.sort(key=lambda p: p.get('like_count', 0) + p.get('comments_count', 0), reverse=True)42 return posts[:10]4344def get_post_insights(media_id, media_type):45 metrics = 'impressions,reach,likes,comments,shares,saved'46 if media_type == 'VIDEO':47 metrics += ',plays'48 try:49 data = ig_get(f'/{media_id}/insights', {'metric': metrics, 'period': 'lifetime'})50 return {item['name']: item['values'][0]['value'] for item in data['data']}51 except Exception as e:52 log.warning(f'Failed to get insights for {media_id}: {e}')53 return {}5455def generate_report():56 log.info('Fetching account insights...')57 account = get_account_insights()58 log.info(f'Account: {account}')59 log.info('Fetching recent posts...')60 posts = get_top_posts()61 enriched = []62 for post in posts:63 insights = get_post_insights(post['id'], post.get('media_type', 'IMAGE'))64 enriched.append({**post, **insights})65 time.sleep(0.3)66 enriched.sort(key=lambda p: p.get('reach', 0), reverse=True)67 total_eng = sum(p.get('likes',0)+p.get('comments',0)+p.get('shares',0)+p.get('saved',0) for p in enriched)68 return {'account': account, 'top_posts': enriched[:5], 'total_engagement': total_eng}6970def send_slack(report):71 if not SLACK_WEBHOOK:72 print(json.dumps({k: v for k, v in report.items() if k != 'top_posts'}, indent=2))73 return74 acc = report['account']75 top = report['top_posts'][0] if report['top_posts'] else {}76 text = (77 f'*Instagram Weekly Report*\n'78 f'Impressions: {acc.get("impressions",0):,} Reach: {acc.get("reach",0):,} '79 f'Profile Views: {acc.get("profile_views",0):,}\n'80 f'Total Engagement: {report["total_engagement"]:,}\n'81 f'Top Post: {top.get("reach",0):,} reach | {top.get("saved",0)} saves'82 )83 requests.post(SLACK_WEBHOOK, json={'text': text}, timeout=10).raise_for_status()84 log.info('Report sent to Slack')8586if __name__ == '__main__':87 report = generate_report()88 send_slack(report)89 print('Done')Error handling
{"error":{"code":10,"message":"Application does not have permission for this action","type":"OAuthException"}}The instagram_business_manage_insights permission has not been granted to your app via Meta App Review, or the app is in Development mode and the account is not a Test User.
Submit instagram_business_manage_insights for App Review at developers.facebook.com. Include a screencast demonstrating the user value. While waiting, add test accounts in App Dashboard > Roles > Test Users. Review typically takes 2-4 weeks.
Do not retry — resolve the permission issue first.
{"error":{"message":"(#100) metric is invalid","code":100,"type":"OAuthException"}}You are requesting a deprecated metric. As of January 2025 (v21+), these metrics were removed: video_views (for non-Reels), email_contacts, website_clicks, phone_call_clicks, text_message_clicks, text_message_click_profile_visits.
Remove deprecated metrics from your requests. For Reels video metrics, use 'plays' instead of 'video_views'. Remove website_clicks and phone_call_clicks entirely — there are no replacements for these in the current API.
Do not retry — update your metric list.
{"error":{"code":17,"message":"User request limit reached","type":"OAuthException"}}Exceeded the 200 API calls per hour limit for the user+app pair. This commonly happens when fetching insights for many posts individually without throttling.
Add 300-500ms delays between per-post insight calls. Limit to top 10 posts instead of fetching all media. Check X-Business-Use-Case-Usage headers and pause when call_count exceeds 80%.
Exponential backoff starting at 60 seconds. The rolling window resets gradually over 1 hour.
{"error":{"code":190,"message":"Invalid OAuth access token - Cannot parse access token","type":"OAuthException"}}The long-lived access token has expired (60-day limit) or the user revoked your app's permissions.
Refresh via GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token={token}. If fully expired, the user must re-authorize via OAuth.
Do not retry — refresh the token.
Rate Limits for Instagram Insights API
| Scope | Limit | Window |
|---|---|---|
| Per user (app+user pair) | 200 API calls | per hour (rolling) |
| Per-post insights calls | 1 call per post | Fetching insights for 10 posts = 10 of your 200 hourly calls |
1import time2import requests34def rate_limited_get(url, params, max_retries=3):5 for attempt in range(max_retries):6 resp = requests.get(url, params=params, timeout=30)7 usage_header = resp.headers.get('X-Business-Use-Case-Usage', '{}')8 if resp.status_code == 400:9 code = resp.json().get('error', {}).get('code')10 if code in (17, 4, 32):11 wait = 60 * (2 ** attempt)12 print(f'Rate limited (code {code}). Waiting {wait}s')13 time.sleep(wait)14 continue15 return resp16 raise Exception('Rate limit retries exhausted')- Fetch insights for only your top 10-15 posts per report run — not every post ever published
- Add 300-500ms sleep between per-post insight calls to stay well within the 200/hour limit
- Cache account-level insights results for at least 30 minutes — they don't change faster than that
- Run analytics reports as weekly or daily jobs rather than real-time pulls to minimize API calls
- Use the paging cursors from the /media endpoint to process posts in batches if needed for larger accounts
Security checklist
- Store access tokens in environment variables — never in code, logs, or version control
- The insights scope grants access to sensitive performance data — do not share raw API responses with unauthorized users
- Treat instagram_business_manage_insights token as highly privileged — it gives read access to all account metrics
- Implement token refresh automation before the 60-day expiry; use a cron job or APScheduler
- Log analytics fetch operations with timestamps for audit purposes
- Do not store raw insights data in unencrypted databases — treat metrics as business-sensitive data
Automation use cases
Weekly Performance Email Digest
beginnerEvery Monday morning, automatically fetch the past week's metrics and email a formatted report with top posts, engagement rates, and week-over-week comparisons.
Google Sheets Analytics Dashboard
intermediateAppend daily account metrics to a Google Sheets spreadsheet to build a running analytics history that stakeholders can view and filter without API access.
Multi-Account Agency Report
advancedAggregate analytics across multiple client Instagram accounts and generate a single consolidated report comparing performance across all managed accounts.
Content Performance Scorer
intermediateAfter each post is published, automatically fetch its insights 24 hours later and score content quality (weighted by saves, reach, and engagement rate) to inform future content decisions.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (limited); Starter from $19.99/monthZapier can trigger analytics fetches on a schedule and pipe Instagram insights data into Google Sheets, Airtable, or Slack with no code.
- + No coding required
- + Direct Google Sheets integration
- + Easy scheduling
- - Limited to surface-level metrics
- - Paid for multi-step automation
- - Less control over metric selection
Make (formerly Integromat)
Free tier (1,000 ops/month); Core from $9/monthMake's Instagram module can schedule weekly analytics pulls and route data to spreadsheets, databases, or notification services with visual workflow building.
- + More affordable than Zapier
- + Better data transformation
- + Flexible output targets
- - Steeper learning curve
- - Limited free operations
- - Requires Meta App Review regardless
n8n
Free (self-hosted); Cloud from $20/monthn8n's self-hosted workflow automation can schedule Instagram analytics pulls and deliver reports with full control over metric selection and report formatting.
- + Free self-hosted
- + Full API access via HTTP nodes
- + No per-operation cost
- - Requires server setup
- - More configuration than Zapier
- - Instagram node less complete than direct API
Best practices
- Never request deprecated metrics (video_views, website_clicks, phone_call_clicks) — they were removed in January 2025 and return errors on v21+
- Use period=day with since/until date range for account insights rather than period=lifetime — the lifetime period is not available for account-level metrics
- Limit per-post insights pulls to your top 10-15 posts by engagement to conserve your 200 calls/hour quota
- Calculate save rate (saves/reach) as a primary content quality signal alongside engagement rate
- Store analytics data in your own database over time — the API does not provide historical data beyond 30 days for most metrics
- Run your analytics job at least 24 hours after publishing a post — impressions and reach continue accumulating for hours after publish
- Add a 300-500ms delay between per-post insights calls to avoid hitting rate limits during bulk report generation
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building an automated Instagram analytics reporter using the Instagram Graph API v25.0. I fetch account insights via GET /{ig-user-id}/insights?metric=impressions,reach,profile_views&period=day and post insights via GET /{ig-media-id}/insights?metric=impressions,reach,likes,comments,shares,saved&period=lifetime. Help me: 1) calculate week-over-week percentage changes by comparing two consecutive 7-day windows, 2) build a save rate and engagement rate calculator, 3) format the output as a clean Slack Block Kit message with sections for account summary and top posts table.
Build a React analytics dashboard for Instagram. It should display: account-level metrics (impressions, reach, profile_views) in KPI cards with 7-day and 28-day totals; a line chart of daily impressions and reach over the past 30 days (using Recharts); a posts table sorted by reach showing thumbnail, caption preview, reach, saves, and engagement rate; and a 'Refresh' button that triggers a Supabase Edge Function to fetch fresh data from the Instagram Graph API. Store historical data in Supabase so users can view trends over time.
Frequently asked questions
Is the Instagram Analytics API free?
Yes — the Instagram Graph API is completely free with no per-request charges. The only cost is time: you need a Meta developer account (free) and App Review approval for the instagram_business_manage_insights permission (free but takes 2-4 weeks with a screencast submission). Some third-party data providers charge $200-$1,400/month, but direct API access costs nothing.
Why are my website_clicks and video_views metrics returning errors?
These metrics were deprecated in January 2025 starting with API version v21. Specifically removed: video_views (for non-Reels), email_contacts, website_clicks, phone_call_clicks, text_message_clicks, and profile_views as a time series. Remove these from your metric parameter. For Reels video views, use 'plays' instead. For link clicks, there is no direct replacement in the current API.
What happens when I hit the 200 calls/hour rate limit?
You'll receive HTTP 400 with error code 17 (User request limit reached). The 200 calls/hour is a rolling window per user+app pair — not a fixed hourly reset. Monitor the X-Business-Use-Case-Usage response header (present on every response), which shows call_count, total_cputime, and total_time as percentages. When call_count exceeds 80%, start backing off with exponential delay starting at 60 seconds.
How far back can I fetch historical Instagram analytics data?
Account-level insights (impressions, reach) are available for up to 30 days via the since/until parameters. Post-level insights with period=lifetime return cumulative totals since the post was published (no time-series breakdown for individual posts). Instagram does not provide historical data older than 30 days for account metrics. This is why you should store analytics data in your own database if you need long-term trend analysis.
Can I get follower demographic data (age, gender, location) from the API?
Not via the current public Graph API. Demographic insights (age, gender, country, city breakdowns) were previously available but were removed from the self-serve API. They are available in Meta Business Suite and Instagram Professional Dashboard natively, but not accessible via API for third-party apps. Third-party data providers like Phyllo may offer access, but at significant cost ($200+/month).
Can RapidDev build a custom Instagram analytics dashboard?
Yes — RapidDev has built analytics tools and reporting dashboards for agencies and brands using the Instagram Graph API. We handle the full stack: Meta App setup and App Review, data pipeline, historical storage in Supabase or PostgreSQL, and a React dashboard with charts and automated weekly reports. Book a free consultation at rapidevelopers.com.
Does the API provide Instagram Story analytics?
Yes, but with limitations. Story insights are available via GET /{ig-media-id}/insights with metrics like impressions, reach, exits, and taps_forward/taps_back. However, Stories expire after 24 hours, so you must fetch their insights before they expire — after that, the media ID returns a 400 error. Set up an automated fetch job that runs 23 hours after each Story is published to capture its final metrics before expiry.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation