How to get Instagram Reels as JSON
Meta's Instagram API lists Reels for professional accounts your app can access. For a public handle you do not manage, the working call is GET /v1/instagram/channel-reels: pass a profile URL, @handle, or numeric userId, and get that account's clips tab as JSON. Billing is ceil(n × 0.3) credits on the returned reel count (2 reels → 1 credit; 5 → 2; 17 → 6), minimum 1.
Every field name and value below comes from a production response captured on 17 October 2026 against @nasa. Nothing is paraphrased from docs.
What the clips tab looks like
url=@nasa and limit=2 returned two Reels. Envelope: requested: 2, totalReturned: 2, hasMore: true, userId: "528817151". The first row, trimmed:
{
"url": "https://www.instagram.com/reel/DeTUk4sDSp-/",
"id": "DeTUk4sDSp-",
"shortcode": "DeTUk4sDSp-",
"mediaId": "4004634989362489982",
"caption": "POV: you're watching the Earth set\n\n… #NASA #ArtemisII #Artemis",
"publishedAt": "2026-10-10T15:15:02.000Z",
"durationSeconds": 43.766,
"videoUrlExpiresAt": "2026-10-18T23:01:42Z",
"thumbnailUrlExpiresAt": "2026-10-22T00:53:43Z",
"hasAudio": true,
"author": {
"id": "528817151",
"username": "nasa",
"displayName": "NASA",
"verified": true,
"followers": 104379435
},
"coauthors": [
{ "username": "nasasolarsystem" },
{ "username": "nasaartemis" }
],
"engagement": {
"likes": 693985,
"comments": 6880,
"views": 10033066,
"viewsSource": "instagram"
}
}
Top-level user on that call was NASA: id 528817151, followers: 104379435, postCount: 4967, verified: true. You do not need a second channel-details call for the profile card. fetchedAt was 2026-10-17T13:21:24.832Z. This capture took 14.5 s.
The call
JavaScript — keep mediaId and userId as strings:
const res = await fetch(
"https://api.captapi.com/v1/instagram/channel-reels?" +
new URLSearchParams({ url: "@nasa", limit: "10" }),
{ headers: { Authorization: `Bearer ${process.env.CAPTAPI_KEY}` } },
);
const { data } = await res.json();
console.log(data.user.username, data.user.followers);
for (const reel of data.reels) {
console.log(reel.publishedAt, reel.author.username, reel.engagement.views);
}
if (data.hasMore) {
// pass data.nextCursor back as cursor
}
Python:
import httpx, os
r = httpx.get(
"https://api.captapi.com/v1/instagram/channel-reels",
params={"url": "@nasa", "limit": 10},
headers={"Authorization": f"Bearer {os.environ['CAPTAPI_KEY']}"},
timeout=120,
)
data = r.json()["data"]
print(data["user"]["username"], data["user"]["followers"])
for reel in data["reels"]:
print(reel["publishedAt"], reel["author"]["username"], reel["engagement"]["views"])
print("next", data.get("nextCursor"))
Prefer userId=528817151 on the next NASA page — it skips handle→ID resolve. instagram.com/nasa/ and instagram.com/@nasa/ both parse. Pass cache=true for the 24h shared cache (0 credits on a hit). Several fetch paths run until a 100 s wall; set the client timeout at or above that.
Engagement on the Earth-set Reel was exact integers with likesIsApproximate / commentsIsApproximate / viewsIsApproximate all false and viewsSource: "instagram". Treat an approximate flag as "this number is a display round", not as missing. likeAndViewCountsDisabled was false. A null like count next to that flag being false is not shipped — if the count was not read, the flag is null (unknown), not a claim that counts are on.
The trap: reels[0].author is not the handle you asked for
The clips tab includes collabs. The second row on the same NASA call was authored by Johnson Space Center:
{
"id": "DeSM5CYPDfd",
"caption": "The journey brought them home. The discoveries will take us farther. …",
"publishedAt": "2026-10-09T19:02:56.000Z",
"author": {
"id": "673882723",
"username": "nasajohnson",
"displayName": "NASA’s Johnson Space Center"
},
"coauthors": [
{ "username": "iss" },
{ "username": "astro_jessica" },
{ "username": "nasa" }
]
}
If you credit views or likes to reels[i].author.username, this Reel is Johnson's, not NASA's. NASA is in coauthors[]. Read data.user for the account you queried. reels[].author is the clip owner. The same thing happens on brand accounts that collab with a talent page — the first row on a @cristiano listing is often the fragrance account.
Two more fields to treat as reel-level, not call-level:
videoUrlExpiresAtandthumbnailUrlExpiresAtare different clocks. On the Earth-set Reel they were 18 October 23:01 UTC (~1.4 days after fetch) and 22 October 00:53 UTC (~4.5 days). Do not collapse them to oneminand assume the poster lasts as long as the poster. Persist the shortcode and refetch when you need to play — how to read CDN expiry before a stored URL 404s.mediaId4004634989362489982is a 19-digit string.Number(mediaId)is not an identity. Same class of bug as TikTok comment ids — keep them as strings.
Empty, stale, and missing are three events
hasMore is true until the end of the list. On a degraded page it is null — unknown, not a false end. Do not stop paging because a fallback page omitted the cursor. A live miss can serve a labelled last-good page (stale, still billed on the returned count). A miss with no snapshot is 502 UPSTREAM_UNAVAILABLE at 0 credits. A deleted, renamed, or banned account is 404 PROFILE_NOT_FOUND at resolve time — not a retryable 502, so a poller does not sit on a dead handle all day.
Photos and carousels are filtered out. This endpoint is Reels only. The mixed feed is /v1/instagram/channel-posts.
The channel-reels page has the envelope, the collab rule, and the credit table. A free key is 100 credits — a limit=10 page is 3 credits.