Walkthrough: Instagram post metrics
Track views, likes and comments on many Instagram posts and reels, from your token to batches, retries, video links and billing.
This walkthrough is for teams that track the public numbers of many Instagram posts and reels, often thousands a day, for example to reward creators on how their posts perform. It takes you from a token to a loop that reads posts in batches, stays inside the rate limits and stores each result.
Your app sends post URLs, the API reads each post when you call, and you store the counts with the time you read them:
1. Get a token
Book a call (opens in a new tab) to get your tokens. You get one for staging (sdp_test_…) and one for production (sdp_…), and each works only on its own host:
| Environment | API_BASE_URL |
|---|---|
| Staging | https://social-data-platform.staging.eu.vlayer.xyz |
| Production | https://social-data-platform.eu.vlayer.xyz |
Build against staging, then switch the base URL and the token together. Keep the token on your server, never in a browser or mobile app; see Environments and authentication.
The curl examples below write the two values as {API_BASE_URL} and <API_TOKEN>. The Node examples are one ES module, run for example with npx tsx track.mts, that starts with this setup:
const API_BASE_URL = process.env.API_BASE_URL;
const API_TOKEN = process.env.API_TOKEN;
if (!API_BASE_URL || !API_TOKEN) throw new Error("Set API_BASE_URL and API_TOKEN");2. Read one post
The API takes a post link, https://www.instagram.com/p/<code>/, or a reel link, https://www.instagram.com/reel/<code>/. Share tags such as ?igsh= are ignored. Other forms return 422 VALIDATION_ERROR, including /reels/<code>/, /<username>/reel/<code>/, /<username>/p/<code>/, /tv/<code>/ and a link without https://.
curl -G {API_BASE_URL}/v1/stats \
-H "Authorization: Bearer <API_TOKEN>" \
--data-urlencode "url=https://www.instagram.com/reel/ABC123/?igsh=xyz"const params = new URLSearchParams({ url: "https://www.instagram.com/reel/ABC123/?igsh=xyz" });
const res = await fetch(`${API_BASE_URL}/v1/stats?${params}`, {
headers: { Authorization: `Bearer ${API_TOKEN}` },
signal: AbortSignal.timeout(120_000),
});
console.log(res.status, await res.json());{
"platform": "instagram",
"contentType": "reel",
"postUrl": "https://www.instagram.com/reel/ABC123/",
"owner": {
"handle": "creator_jane",
"accountId": "270702400",
"secUid": null,
"displayName": "Creator Jane"
},
"post": {
"caption": "Summer drop",
"postedAt": "2026-09-22T18:30:00.000Z",
"thumbnailUrl": "https://...",
"mediaUrls": ["https://..."]
},
"stats": { "views": 125000, "likes": 5432, "comments": 312, "shares": null, "saves": null },
"source": "provider"
}Key stored posts on the platform plus the shortcode, the last segment of the path (ABC123 above), and send one form of each post's URL. postUrl keeps the form you sent: /p/ABC123/ and /reel/ABC123/ can be the same reel but give different postUrls and contentTypes, and each is read and billed. To confirm the post belongs to a creator, compare owner.accountId with their account id, not the handle; see Post metrics.
Responses usually take a few seconds. Slow cases, such as a reduced-detail read or a post that turns out to be gone, can take up to a minute or more. Keep the 120-second client timeout from the example: the API closes a request after 120 seconds, so a shorter timeout only gives up on reads that would still have been answered.
3. Read the counts by post type
contentType comes from the URL, not from the post: a /reel/ link is reel and a /p/ link is post, whatever the post holds. A /p/ link can be a photo, a carousel or a video, and views and mediaUrls depend on which it is:
| The post is | views | mediaUrls |
|---|---|---|
A reel, or a video at a /p/ link | The public view count. Can be null for some videos | The video file |
| A photo | Always null: Instagram shows no public views for photos | The photo |
| A carousel | Always null for the carousel as a whole. Each video inside it has its own views in post.carouselVideos (below) | The cover image only. Each video's file is in post.carouselVideos |
-
likesandcommentsare returned for every type.likesisnullwhen the author hides the like count. -
sharesandsavesare alwaysnullon Instagram, which does not show them publicly. -
post.carouselVideoslists every video inside a carousel, in slide order, and is an empty array for anything else:"carouselVideos": [ { "position": 1, "shortcode": "ABC123video1", "views": 821, "mediaUrl": "https://..." }, { "position": 3, "shortcode": "ABC123video3", "views": 465, "mediaUrl": "https://..." } ]viewsis Instagram's own view count for that one video. It runs lower than the views Instagram shows on reels, so compare carousel videos with each other, not with reels, and check it against a few of your own carousels. It can benullwhen Instagram doesn't return it.mediaUrlis a signed link to the video file, likemediaUrlsbelow. -
Treat
nullas unknown, never as zero. Do not overwrite a count you stored earlier withnull: keep the last number you have and read the post again on your next cycle. The response does not say whether a/p/link is a photo, a carousel or a video (a non-emptypost.carouselVideostells you it is a carousel with videos), so anullviewsthere can be permanent.
Views on photos, and on a carousel as a whole, are private to the creator's account; see Social Data API and Vouch.
Some posts, such as age-restricted ones, can only be read with reduced detail. The response is still 200, with source: "fallback": the counts are there when available, but owner, caption and postedAt are null and mediaUrls is empty. Its counts can be up to 6 hours old, rather than 15 minutes; see Data freshness. Do not use such a read for an ownership check. See Reduced-detail reads.
4. Download video files promptly
For a reel or video, mediaUrls[0] is usually a link to the video file on Instagram's own servers. Occasionally no video link is available and it holds the cover image instead, so check the file type before treating it as a video. The API returns the link; it does not store or host the file. The link is signed and expires, typically within a day or two, so download the file when you read the post. thumbnailUrl is signed the same way.
For example:
curl -L -o ABC123.mp4 "<mediaUrls[0] from the response>"A link that has expired fails to download. Read the post again for a fresh one; that read is billed like any other. For videos inside a carousel, the links are in post.carouselVideos[].mediaUrl. A reduced-detail read has none.
5. Read many posts in one call
Send up to 25 URLs in one request. Results come back in the order you sent them, each with ok set to true or false.
curl -X POST {API_BASE_URL}/v1/stats/batch \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://www.instagram.com/reel/ABC123/",
"https://www.instagram.com/p/GONE/"
]
}'{
"results": [
{
"url": "https://www.instagram.com/reel/ABC123/",
"ok": true,
"platform": "instagram",
"contentType": "reel",
"postUrl": "https://www.instagram.com/reel/ABC123/",
"owner": { "handle": "creator_jane", "accountId": "270702400", "secUid": null, "displayName": "Creator Jane" },
"post": { "caption": "Summer drop", "postedAt": "2026-09-22T18:30:00.000Z", "thumbnailUrl": "https://...", "mediaUrls": ["https://..."] },
"stats": { "views": 125000, "likes": 5432, "comments": 312, "shares": null, "saves": null },
"source": "provider"
},
{
"url": "https://www.instagram.com/p/GONE/",
"ok": false,
"error": { "code": "POST_NOT_FOUND", "message": "The post no longer exists at that URL." }
}
],
"summary": { "total": 2, "succeeded": 1, "failed": 1 }
}A post that cannot be read does not fail the batch: the batch answers 200, and that result carries its error code in error.code. That includes an unsupported URL, which comes back as ok: false with error.code: "VALIDATION_ERROR" rather than as a 422. A batch reads up to 8 posts at a time and stops after 100 seconds, so a few slow posts can hold up the rest. Posts not finished by then come back with BATCH_TIMEOUT; send them again in a smaller batch or with GET /v1/stats.
This loop, after the setup in step 1, reads a list of posts 25 at a time and sends at most one batch every 31 seconds: 50 URLs a minute, inside the default limit of 60. Every send waits for its turn, retries included, so a retry never goes over the limit. A Retry-After, on an error or on a 200, pushes the next send back by at least that long. A batch that still fails after five attempts is kept for later, and the loop goes on with the next one. A 401 or another 4xx stops the loop, since every batch would fail the same way:
const BATCH_SIZE = 25;
const BATCH_INTERVAL_MS = 31_000;
const RETRYABLE_CODES = new Set([
"SCRAPING_RATE_LIMITED",
"SCRAPING_ERROR",
"SCRAPING_UNAVAILABLE",
"UPSTREAM_TIMEOUT",
"INTERNAL_ERROR",
"BATCH_TIMEOUT",
]);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
const postUrls = [
"https://www.instagram.com/reel/ABC123/",
"https://www.instagram.com/p/DEF456/",
// ...
];
// Every send, retries included, waits for this time and moves it on.
let nextSendAt = 0;
const notBefore = (ms: number) => (nextSendAt = Math.max(nextSendAt, Date.now() + ms));
async function send(urls: string[]): Promise<Response> {
await sleep(nextSendAt - Date.now());
nextSendAt = Date.now() + BATCH_INTERVAL_MS;
return fetch(`${API_BASE_URL}/v1/stats/batch`, {
method: "POST",
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({ urls }),
signal: AbortSignal.timeout(120_000),
});
}
// The batch's results, or null when every attempt failed.
async function readBatch(urls: string[], attempts = 5) {
for (let attempt = 1; attempt <= attempts; attempt++) {
let res: Response;
try {
res = await send(urls);
} catch {
continue; // A connection error or the client timeout: try again.
}
const retryAfter = Number(res.headers.get("Retry-After")) || 0;
notBefore(retryAfter * 1000);
if (res.ok) return (await res.json()).results;
if (res.status !== 429 && res.status < 500) throw new Error(`Batch rejected: ${res.status}`);
notBefore(1000 * 2 ** attempt * (1 + Math.random()));
}
return null;
}
const readAgainLater: string[] = [];
for (let i = 0; i < postUrls.length; i += BATCH_SIZE) {
const urls = postUrls.slice(i, i + BATCH_SIZE);
const results = await readBatch(urls);
if (!results) {
readAgainLater.push(...urls);
continue;
}
const readAt = new Date().toISOString();
for (const r of results) {
if (r.ok) console.log(readAt, r.postUrl, r.stats);
else if (RETRYABLE_CODES.has(r.error.code)) readAgainLater.push(r.url);
else console.warn("Not readable:", r.url, r.error.code);
}
}
console.log("Read again in a later batch:", readAgainLater);Replace the console lines with writes to your own store. Two URLs in one batch with the same postUrl are read once and billed once; a /p/ and a /reel/ link to the same post are read and billed twice.
6. Stay within the limits
The default limits are per client IP and counted per minute:
| Limit | Default |
|---|---|
GET /v1/stats and POST /v1/stats/batch, together | 60 requests per minute |
| URLs in batches | 60 per minute, every URL counting as one |
| URLs in one batch | 1 to 25 |
| One batch | 100 seconds |
A request over a limit is answered with 429 RATE_LIMITED and a Retry-After header, in seconds, saying when the current one-minute window ends. Nothing in it was read and it is not billed. Wait that long, then send it again; the loop in step 5 does this.
Plan on about 60 posts a minute from one IP at the default limits: up to about 86,000 a day if the reads are spread evenly, so 50,000 posts a day is about 35 a minute. Batches save round trips, not limit. For more, ask for higher limits with the peak requests per minute you expect, and ramp up in steps as described in High-volume integration.
Counts may be up to 15 minutes old, so reading the same post more often can return the same numbers and is still billed. Read each post at most once every 15 minutes; see Data freshness.
7. Retry what is worth retrying
The loop in step 5 already retries a rejected batch and collects the posts that failed with a retryable code. The same split applies to single reads and to each result in a batch:
- Worth retrying:
429after itsRetry-After,500,502,503and504, connection errors and client timeouts. In a batch result:SCRAPING_RATE_LIMITED,SCRAPING_ERROR,SCRAPING_UNAVAILABLE,UPSTREAM_TIMEOUT,INTERNAL_ERRORandBATCH_TIMEOUT. - Not worth retrying unchanged:
401,403andVALIDATION_ERRORmean the token or the request is wrong; fix it first.POST_NOT_FOUND,POST_NOT_ACCESSIBLEandPOST_AGE_RESTRICTEDare about the post: stop reading it on every cycle and check it again much later, for example once a day.
For single reads, High-volume integration has a tested retry function, and Errors has every code and how long to wait. Log the X-Request-Id header of each response and quote it when you contact us about a request.
8. Understand what is billed
Each successful single read is one billable unit, and so is each distinct post a batch reads successfully. A reduced-detail read, or one with null counts, is a successful read and is billed. Failed and timed-out posts, and requests answered with an error such as 429, are free. Re-reading a post in a separate request is billed each time, even when its counts have not changed.
Volumes and pricing are agreed on the call. Your token can report its own usage, by token, route and day; see Usage and billing.
Social Data API and Vouch
The Social Data API reads what anyone can see on a public post, for every post you track. What only the creator can see on that same post comes from Vouch: the creator signs in to their own Instagram account and proves the post's stats.
| On a post, you need | Where it comes from |
|---|---|
| Likes and comments, views on reels and videos, and views on each video inside a carousel | Social Data API, on every post, as on this page |
| Numbers only the creator can see, such as views on photos and on a carousel as a whole | Vouch, through the creator's own login; each data source lists what it returns |
The two work on the same post: track the public counts here on every post, at scale, and ask the creator for a Vouch proof on the posts where you need what only they can see. On Instagram, that proof is Instagram Proof of Post Stats (opens in a new tab). The data sources list has the rest, including stats for TikTok videos and for YouTube videos and Shorts.
Last updated on