At a glance
Status, checked 2026-09-18: the field rules below follow the IG Media reference as Meta published it on 2026-09-18. caption and media_product_type are marked there for Facebook Login only, so confirm both with your own Instagram Login token.
- Use it when
- you are writing the
fields=list for/me/mediaor/<IG_ID>/mediaongraph.instagram.comand need to know which field holds the picture, the video cover, the post link or the carousel items. - Do not use it when
- the owner will not create a Meta app or keep a token alive. A hosted feed returns posts without any
fields=list; see the feed API comparison. - What the human does
- nothing for the field list itself. The account must already be a Business or Creator account that granted
instagram_business_basicto your app (access tokens). - Next step
- run the request below with your token, check whether
captionandmedia_product_typecome back, then convert each item withtoPost().
Name every field you render
Without fields=, the media edge answers with bare ids. The edge is GET https://graph.instagram.com/v25.0/<IG_ID>/media, and an item carries nothing but its id until you list more (Meta: get started with Instagram Login, checked 2026-09-18).
Most examples call /me/media, where me is the account the token was issued for; for your own account both paths list the same posts.
Why v25.0: Meta's Instagram examples use v25.0 (released 2026-02-18, available until 2028-07-29); the newest Graph API version is v26.0 (2026-07-29) (Meta: Graph API changelog, checked 2026-09-18). This page matches those examples. Moving to v26.0 is a one-line change once you have compared both responses for your account.
Terminal (token read from your server environment)
curl -sG "https://graph.instagram.com/v25.0/me/media" \
--data-urlencode "fields=id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp,username,like_count,comments_count,children{media_type,media_url,thumbnail_url}" \
--data-urlencode "limit=3" \
--data-urlencode "access_token=$IG_ACCESS_TOKEN"
Two names in that list, caption and media_product_type, may be refused or silently left out for an Instagram Login token. If the call errors, remove them one at a time and log which ones return.
Field reference
Thirteen fields cover a typical feed. Apart from id, a field is returned only when you name it.
| Field | Returned | Notes |
|---|---|---|
id | Always | Media id. Use it as the key when you merge pages or cache posts. |
caption | Documented for Facebook Login only; test it | Post text. Treat it as optional and default to an empty string. |
media_type | When named | IMAGE, VIDEO or CAROUSEL_ALBUM. A reel is reported as VIDEO. |
media_product_type | Documented for Facebook Login only; test it | AD, FEED, STORY or REELS. The one documented reel flag. |
media_url | When named, except for copyright-flagged media | Signed link to the photo, or to the video file for VIDEO. It expires; see media URL expired. |
thumbnail_url | VIDEO only | Cover frame of a video. Use it as the grid tile for videos and reels. |
permalink | Top-level posts; does not work on carousel children | The post on instagram.com. Link carousel items to the parent post. |
timestamp | When named | Publication time with a +0000 offset, for example 2026-09-12T07:41:00+0000. |
username | When named | Handle of the account that owns the post. |
children | CAROUSEL_ALBUM only | Ids only, unless expanded as children{media_type,media_url,thumbnail_url}. |
like_count | When named; not verified by us with Instagram Login | Number of likes. Keep the UI working when it is missing. |
comments_count | When named; not verified by us with Instagram Login | Number of comments. Same caution as like_count. |
is_shared_to_feed | Reels only | A reel-only flag about feed placement. Its value on other media is undefined, so it is no reel detector. |
Sources: Meta: IG Media reference, checked 2026-09-18; Meta: IG Media children, checked 2026-09-18.
The +0000 offset has no colon, which is outside the ECMAScript date format, so rewrite it as +00:00 before calling new Date().
No default or maximum limit for this edge appears in the Meta pages we read on 2026-09-18. Pass the number of posts you actually draw and follow paging.next when you need more.
Reels, videos and carousels
Telling a reel from another video
Reels and ordinary videos share media_type: "VIDEO". Only media_product_type separates them, and the IG Media reference labels it this way:
Available for Instagram API with Facebook Login only
With Instagram Login a reel badge is therefore not guaranteed. If your token returns media_product_type, filter on REELS. If it does not, draw every VIDEO the same way and leave the reel label off. A guess from the shape of permalink relies on undocumented URL patterns.
Carousel items
On a CAROUSEL_ALBUM, the children edge lists the slides as bare ids. Expanding it inline as children{media_type,media_url,thumbnail_url} returns each slide in the same response (Meta: IG Media children, checked 2026-09-18), which saves one extra request per slide.
Slides have no working permalink (Meta: IG Media reference, checked 2026-09-18), so every slide should open the parent post. A video slide has its own thumbnail_url, like a top-level video.
When media_url is missing
Instagram leaves media_url out when a post contains copyrighted material or was flagged for a copyright violation (Meta: IG Media reference, checked 2026-09-18). Skip that post, or show a text card that links to permalink. Never render an empty <img>.
Example response
A photo, a reel and a two-slide carousel, followed by the cursor block that pages the list.
Example response, trimmed (ids, URLs, cursors and the token are placeholders; caption and media_product_type appear only if your token returns them)
{
"data": [
{
"id": "17900000000000001",
"caption": "Example caption for a photo post.",
"media_type": "IMAGE",
"media_product_type": "FEED",
"media_url": "https://scontent.cdninstagram.com/v/example/photo-1.jpg?stp=example&oe=6AB0F580",
"permalink": "https://www.instagram.com/p/EXAMPLE1/",
"timestamp": "2026-09-12T07:41:00+0000",
"username": "example.account",
"like_count": 412,
"comments_count": 23
},
{
"id": "17900000000000002",
"caption": "Example caption for a reel.",
"media_type": "VIDEO",
"media_product_type": "REELS",
"media_url": "https://scontent.cdninstagram.com/v/example/reel-1.mp4?stp=example&oe=6AB0F580",
"thumbnail_url": "https://scontent.cdninstagram.com/v/example/reel-1-cover.jpg?stp=example&oe=6AB0F580",
"permalink": "https://www.instagram.com/reel/EXAMPLE2/",
"timestamp": "2026-09-10T16:05:00+0000",
"username": "example.account",
"like_count": 1287,
"comments_count": 64
},
{
"id": "17900000000000003",
"media_type": "CAROUSEL_ALBUM",
"media_product_type": "FEED",
"permalink": "https://www.instagram.com/p/EXAMPLE3/",
"timestamp": "2026-09-08T11:20:00+0000",
"username": "example.account",
"children": {
"data": [
{
"media_type": "IMAGE",
"media_url": "https://scontent.cdninstagram.com/v/example/slide-1.jpg?stp=example&oe=6AB0F580",
"id": "17900000000000031"
},
{
"media_type": "VIDEO",
"media_url": "https://scontent.cdninstagram.com/v/example/slide-2.mp4?stp=example&oe=6AB0F580",
"thumbnail_url": "https://scontent.cdninstagram.com/v/example/slide-2-cover.jpg?stp=example&oe=6AB0F580",
"id": "17900000000000032"
}
]
}
}
],
"paging": {
"cursors": {
"before": "EXAMPLE_BEFORE_CURSOR",
"after": "EXAMPLE_AFTER_CURSOR"
},
"next": "https://graph.instagram.com/v25.0/17841400000000000/media?access_token=EXAMPLE_TOKEN&fields=id%2Cmedia_type&limit=3&after=EXAMPLE_AFTER_CURSOR"
}
}
Pagination is cursor-based: paging.cursors holds before and after, and paging.next is the URL of the following page. A page may hold fewer items than limit, so stop when paging.next is absent, not when a page comes back short (Meta: Graph API paginated results, checked 2026-09-18).
When paging.next includes access_token, it is a secret. Follow it on the server and never hand it to browser code.
Map the fields to ElectricBlaze schema v1
Schema v1 is a platform-neutral post shape: type tells a component what to draw, and format carries the platform's own name for the post. Converting Graph API items once keeps components free of Instagram field names.
| Schema v1 | Rule | |
|---|---|---|
id | id | Copy unchanged. |
| (none) | source, status | Constants: "instagram" and "published". |
media_type | type | IMAGE to image, VIDEO to video, CAROUSEL_ALBUM to carousel. |
media_product_type | format | REELS gives "reel". Any other value, or no value, gives "post". |
thumbnail_url or media_url | thumbnailUrl | thumbnail_url for a video, media_url for a photo; for a carousel, the same rule on the first slide. Required for image, video and carousel posts. |
permalink | url | Link to the post on instagram.com. |
caption | text | Empty string when absent. |
timestamp | publishedAt | Insert the colon in the offset, then store ISO 8601 in UTC. |
children.data[] | media[] | One entry per slide with url and mime. A single photo or video gives one entry from the item itself. |
like_count, comments_count | stats.likes, stats.comments | Only when returned. |
| The whole item | raw | Keeps fields the schema does not name, such as is_shared_to_feed. |
Instagram sends no MIME type, and none of the fields above carries a pixel size. The converter derives mime from media_type; aspectRatio and media[].width stay unset unless you measure the files yourself.
lib/instagram-to-post.ts
// Converts one /me/media item into an ElectricBlaze schema v1 post, or null when there is nothing to draw.
type IgItem = {
id: string;
media_type: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
media_url?: string; // absent for copyright-flagged media
thumbnail_url?: string; // VIDEO only
};
type IgMedia = IgItem & {
caption?: string; // documented for Facebook Login only
media_product_type?: string; // documented for Facebook Login only
permalink: string;
timestamp: string; // "2026-09-12T07:41:00+0000"
like_count?: number;
comments_count?: number;
children?: { data: IgItem[] };
};
const TYPE = { IMAGE: "image", VIDEO: "video", CAROUSEL_ALBUM: "carousel" } as const;
const coverOf = (m?: IgItem) => (m?.media_type === "VIDEO" ? m.thumbnail_url : m?.media_url);
// Instagram sends no MIME type: these two values are assumptions.
const toMedia = (m: IgItem) =>
m.media_url ? [{ url: m.media_url, mime: m.media_type === "VIDEO" ? "video/mp4" : "image/jpeg" }] : [];
export function toPost(m: IgMedia) {
const items: IgItem[] = m.media_type === "CAROUSEL_ALBUM" ? m.children?.data ?? [] : [m];
const thumbnailUrl = coverOf(items[0]);
if (!thumbnailUrl) return null;
return {
id: m.id,
source: "instagram",
type: TYPE[m.media_type],
format: m.media_product_type === "REELS" ? "reel" : "post",
status: "published",
url: m.permalink,
text: m.caption ?? "",
media: items.flatMap(toMedia),
thumbnailUrl,
publishedAt: new Date(m.timestamp.replace(/([+-]\d{2})(\d{2})$/, "$1:$2")).toISOString(),
stats: { likes: m.like_count, comments: m.comments_count },
raw: m,
};
}
Returning null is deliberate: schema v1 rejects an image, video or carousel post without thumbnailUrl, so a copyright-flagged item is dropped instead of breaking the grid.
What ElectricBlaze data contains today
Two ElectricBlaze products expose Instagram data, and they are at different stages:
- The widget shows real posts today. Its data is the Graph API response as it comes, a
{"data": [...], "paging": {...}}envelope. Each post hasid,caption,media_type,media_url,permalink,timestampandusername. There is nomedia_product_type, so a reel is only aVIDEO, and nochildren, so carousel slides are not expanded. The demo feed also lacksthumbnail_url. We read this on 2026-09-17 from the demo feed and the renderer code. - The CLI (0.1.2) writes demo posts that already follow schema v1, including
format: "reel"and multi-itemmedia[]. They are demo data. Whether real posts from the 0.2 JSON API will carry reel flags and carousel slides is not established yet.
Reel badges or swipeable carousels today need your own Graph API code with the expansion above, and reel badges also need a token that returns media_product_type.