At a glance
Status, checked 2026-09-18: requests to the Basic Display API have failed since 2024-12-04 (Meta developer blog, 2024-09-04, checked 2026-09-18); Instagram Login tokens stop working after 60 days unless refreshed (Meta: access_token reference, checked 2026-09-18); throttled calls return error 80002. For the ElectricBlaze widget, three checks on the page itself are listed at the end.
- Use it when
- a feed built on
graph.instagram.comreturns an error, an emptydataarray, posts without images or captions, or images that break some time after the page was built. - Do not use it when
- the code still calls the Instagram Basic Display API. There is nothing to fix there; migrate as described in Basic Display alternative.
- What the human does
- the account owner logs in again after an expired or invalidated token, confirms the account is Business or Creator, and in Development mode accepts the Instagram Tester invite.
- Next step
- run the check script below with your token. It prints the HTTP status, the usage header, and the error code with its subcode, which is enough to find your row in the table.
Run one request and read all of it
Before changing code, make the call your feed makes and read the raw answer, headers included. The URL uses v25.0 because the Instagram examples from Meta do; v26.0 has been out since 2026-07-29 (Meta: Graph API changelog, checked 2026-09-18). Pin the version you tested and upgrade on purpose.
# -D - prints the response headers before the JSON body
curl -sS -D - "https://graph.instagram.com/v25.0/me/media?fields=id,media_type,media_url,permalink,timestamp&limit=5&access_token=$IG_ACCESS_TOKEN"
The same check in Node 18+, with a verdict instead of raw JSON:
scripts/check-instagram.mjs
// Usage: IG_ACCESS_TOKEN=... node scripts/check-instagram.mjs
const url = new URL("https://graph.instagram.com/v25.0/me/media");
url.searchParams.set("fields", "id,caption,media_type,media_url,permalink,timestamp");
url.searchParams.set("limit", "10");
url.searchParams.set("access_token", process.env.IG_ACCESS_TOKEN ?? "");
const res = await fetch(url);
const body = await res.json();
console.log("HTTP", res.status);
console.log("X-Business-Use-Case-Usage:", res.headers.get("x-business-use-case-usage") ?? "(absent)");
if (body.error) {
const { type, code, error_subcode, message } = body.error;
const hints = {
"190/463": "token expired: the owner logs in again, then schedule refreshes",
"190/460": "password changed: the owner logs in again",
"190/467": "token invalid: check the stored value, then log in again",
};
console.log(type, code, error_subcode ?? "-", message);
console.log("->", hints[`${code}/${error_subcode}`] ?? (code === 80002 ? "throttled: cache and call less often" : "see the table"));
} else {
const items = body.data ?? [];
console.log("items:", items.length, "| more pages:", Boolean(body.paging?.next));
console.log("no media_url:", items.filter((m) => !m.media_url).map((m) => m.id));
console.log("no caption field:", items.filter((m) => !("caption" in m)).length); // also counts posts with no caption
}
Symptom, cause, fix
Take the first row that matches. Rows that name a code or a message come from the error object; the others come from the shape of data or the setup.
| Symptom | Cause | Fix |
|---|---|---|
OAuthException, code 190, error_subcode 463 | The long-lived token reached 60 days without a refresh | The owner logs in again; then schedule monthly refreshes (access tokens) |
| Code 190, subcode 460 | The account password changed | The owner logs in again |
| Code 190, subcode 467 | The token is invalid for another reason | Check the stored value is complete and belongs to this app, then get a new one through Instagram Login |
Every item has only id | No fields= parameter in the request | List the fields: fields=id,media_type,media_url,permalink,timestamp |
data is [] although fields were requested | The account has no posts, or the token belongs to another account than you think | Call /v25.0/me?fields=username with the same token and compare the handle |
One post has no media_url | Meta leaves it out for copyrighted or copyright-flagged media, for example a reel that uses licensed music | Skip the item or show a link to its permalink; it is not an error |
No caption or media_product_type | Both are marked as Facebook Login only in the IG Media reference | Test with your token; render without them when absent |
| Images answer 403 "URL signature expired" | The signed link passed the expiry encoded in its oe parameter | Fetch fresh URLs before serving cached HTML (media URL expired) |
Error 80002, high values in X-Business-Use-Case-Usage | The app used up its Instagram Platform call budget | Cache on the server and call once per refresh interval, not once per page view |
| "Insufficient developer role" | Development mode, and the account lacks the Instagram Tester role (reported by a secondary source) | Invite the account as an Instagram Tester; the owner accepts under Settings, Apps and websites, Tester invites in Instagram |
| Code written before December 2024 fails on every call | It targets the Instagram Basic Display API, shut down on 2024-12-04 | Move to the Instagram API with Instagram Login (Basic Display alternative) |
The authorize URL still requests business_basic | That scope value was deprecated on 2025-01-27 | Request instagram_business_basic |
| The owner's account is personal | Only professional accounts are served | The owner switches it to Business or Creator in the Instagram app |
Sources: Meta: Graph API error handling, checked 2026-09-18; Meta: get started with Instagram Login, checked 2026-09-18; Meta: IG Media reference, checked 2026-09-18; verbb/social-feeds issue 21 (2026-01), checked 2026-09-18 (secondary); Meta: rate limiting, checked 2026-09-18; Chatwoot docs on Instagram Business Login, checked 2026-09-18 (secondary); Meta developer blog, 2024-09-04, checked 2026-09-18; Meta: Instagram API with Instagram Login, checked 2026-09-18; Meta: Instagram Platform overview, checked 2026-09-18.
Reading the error object
Every failed call carries an error object. Branch on code and error_subcode; the message text embeds timestamps and varies from call to call.
An expired long-lived token (example values)
{
"error": {
"message": "Error validating access token: Session has expired on Saturday, 12-Sep-26 09:15:42 PDT. The current time is Friday, 18-Sep-26 03:02:11 PDT.",
"type": "OAuthException",
"code": 190,
"error_subcode": 463,
"fbtrace_id": "AbCdEfGhIjK_example"
}
}
- Code 190 means the token is unusable: 463 expired, 460 password changed, 467 invalid (Meta: Graph API error handling, checked 2026-09-18). All three end with the owner logging in again.
- Do not branch on
typealone.OAuthExceptionalso shows up on errors that have nothing to do with tokens. - Log
fbtrace_idwith the time of the call. It identifies the request if you ever report the problem to Meta.
Empty or partial data
An empty or short feed with HTTP 200 carries no error object. Three causes explain it:
- No fields requested. Without
fields=, the media edge returns each item as an id and nothing more (Meta: get started with Instagram Login, checked 2026-09-18). The renderer has no image to draw. - Another account, or no posts. A token belongs to exactly one account. Ask
/me?fields=usernamewhich one before debugging the rest. - Pagination stopped early. A page can hold fewer items than
limit, so a short page is not the end of the feed; stop only whenpaging.nextis absent (Meta: Graph API paginated results, checked 2026-09-18).
A missing field on one item is often expected. media_url is omitted when the media contains copyrighted material or was flagged for it (Meta: IG Media reference, checked 2026-09-18). thumbnail_url exists only on VIDEO items (Meta: IG Media reference, checked 2026-09-18). Carousel children come back as bare ids until you expand them as children{media_type,media_url,thumbnail_url} (Meta: IG Media children, checked 2026-09-18).
Available for Instagram API with Facebook Login only
Many tutorials request caption from graph.instagram.com regardless. We make no promise in either direction: test with your own token and keep the layout intact when the field is absent. Media fields covers each field in detail.
Throttling: error 80002
Instagram Platform calls, messaging aside, share a budget of 4800 × the number of impressions per 24 hours (Meta: rate limiting, checked 2026-09-18). Past it, calls fail with 80002. The X-Business-Use-Case-Usage response header reports how much of the budget is used, so log it on every call and slow down before the limit, not after.
Codes 4, 17 and 613 are general Graph API limits. Back off on them too, but for Instagram Platform calls look for 80002 first.
One call per refresh window is enough for a feed. Serve page views from a server-side cache; a browser calling Instagram directly would also expose the token.
Checks for the ElectricBlaze widget
For the ElectricBlaze embed, the token rows above do not apply to your site. Three page-side checks do:
- The id is the container's first class. The loader takes the widget id from the first class and ignores the rest, so
class="electricblaze-id-w42 feed"mounts andclass="feed electricblaze-id-w42"does not. - The Content Security Policy lets the widget through.
script-srcmust allowhttps://s.electricblaze.com.connect-srcmust allowhttps://api.electricblaze.com,https://proxy.electricblaze.com, which serves the data of connected accounts, andhttps://s.electricblaze.comfor demo data. The browser console names any other blocked host and directive. - The state class tells you where it stopped. The loader sets
electricblaze-state-loading,-mounting,-mountedor-erroron the container. A container left inelectricblaze-state-errordid not mount: check the widget id and the/v1/packagerequest in the network panel.
Content-Security-Policy: script-src 'self' https://s.electricblaze.com; connect-src 'self' https://api.electricblaze.com https://proxy.electricblaze.com https://s.electricblaze.com; img-src 'self' https: data:
The loader also picks up containers added after page load, such as after a client-side route change, and ignores a second copy of widget.js. What the widget displays once a Free plan passes 1000 views in a month is not established yet; this page will say when it is.