# ElectricBlaze: full developer documentation
> ElectricBlaze shows social media feeds on websites. Instagram works today through an embeddable widget; the `electricblaze` CLI adds a feed component with demo data to a project, and a JSON API for real posts ships in 0.2.
---
# ElectricBlaze for developers and AI coding agents
**Status, checked 2026-09-18:** the developer tools cover Instagram only. The Instagram widget embed shows real posts today. The `electricblaze` CLI 0.1.2 on npm adds a component with demo data; its JSON API ships in 0.2. TikTok and YouTube in the CLI and API are planned.
> ElectricBlaze shows social media feeds on websites. This section is written for developers and for the coding agents that build sites for them: exact status, commands, files and sources.
Canonical: https://electricblaze.com/developer/
## At a glance
- **Use it when:** you are an agent or a developer adding a social feed to a site and want a working grid now, real posts without a Meta app, and a data contract that will not change under you.
- **Do not use it when:** you need TikTok or YouTube data in code today, or a public Instagram JSON feed today. Neither exists yet; [the comparison](https://electricblaze.com/developer/instagram-feed/comparison.html) names providers that have the latter.
- **What the human does:** connects the Instagram account in the dashboard at [a.electricblaze.com](https://a.electricblaze.com/) when real posts are needed. Everything else can be done by an agent.
- **Next step:** start at the [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/) or run `npx electricblaze@0.1.2 add instagram-feed --json`.
## Start here
- [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/): what works today, framework guides, API reference.
- [Compare Instagram feed providers](https://electricblaze.com/developer/instagram-feed/comparison.html): nine options with dated facts, including where we are behind.
- [Next.js guide](https://electricblaze.com/developer/instagram-feed/nextjs.html), the most common stack for agent-built sites.
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html) and [the agent skill](https://electricblaze.com/developer/instagram-feed/skill.html).
## Install in one line
| What | Command |
|---|---|
| Component and demo data | `npx electricblaze@0.1.2 add instagram-feed --json` |
| Agent skill for Claude Code and Codex | `npx electricblaze skill` |
| Agent skill for any agent | `npx skills add ElectricBlaze/electricblaze` |
| Through shadcn | `npx shadcn@latest add ElectricBlaze/electricblaze/instagram-feed` |
| Claude Code plugin | `/plugin marketplace add ElectricBlaze/electricblaze` |
| Real posts today | Paste the widget snippet from the dashboard |
Widget snippet (the id comes from the dashboard):
```html
```
## For agents: files you can fetch
Every page in this section exists twice: as HTML and as Markdown. Add `.md` to the address, or send `Accept: text/markdown` to the HTML address.
Machine-readable entry points:
```json
{
"llms_txt": "https://electricblaze.com/llms.txt",
"llms_full_txt": "https://electricblaze.com/llms-full.txt",
"markdown_twin": "https://electricblaze.com/developer/instagram-feed/nextjs.md",
"feed_schema": "https://electricblaze.com/developer/instagram-feed/schema.json",
"sample_feed": "https://electricblaze.com/developer/instagram-feed/sample-feed.json",
"changelog_atom": "https://electricblaze.com/developer/changelog.xml",
"npm": "https://www.npmjs.com/package/electricblaze",
"source": "https://github.com/ElectricBlaze/electricblaze"
}
```
Each page starts with a status line and its check date. Facts about Meta's API carry a link to the page they came from and the date it was read.
## Rules we ask agents to follow
1. Do not write a client-side fetch to graph.instagram.com with an access token; it leaks the token to every visitor.
2. Do not scrape instagram.com. Use the official API, the widget, or the CLI demo data.
3. Real posts need the account owner. Say so, and hand them the link to the dashboard.
4. Pin the CLI version and read `next` in the JSON output. Exit code 2 means "not available yet", not failure.
5. Keep the "demo feed" label until real data flows. A grid of sample posts must not pass for the client's account.
## What we are and are not
ElectricBlaze started as embeddable social widgets. We are building a data layer for feeds on top of them: one schema for every network, with the platform APIs, tokens and caching on our side.
Today the developer tools cover Instagram. Widgets for other networks are on the main site and are outside this section. Where a page here says "0.2" or "planned", the thing does not exist yet.
Source code of the CLI, the schema and the skill: [github.com/ElectricBlaze/electricblaze](https://github.com/ElectricBlaze/electricblaze), MIT license. Security reports: contact@electricblaze.com.
## FAQ
### What is ElectricBlaze?
A service that shows social media feeds on websites through embeddable widgets. For developers it adds a CLI with a typed Instagram component and demo data; the JSON API for code ships in 0.2.
### Is there an ElectricBlaze API?
The JSON API ships in 0.2. Today the CLI writes a typed component with demo data, and the widget shows real posts.
### Is ElectricBlaze on npm?
Yes, as `electricblaze`, published from GitHub Actions with provenance. Run it with `npx electricblaze`.
### Which social networks are supported?
The widgets on the [main site](https://electricblaze.com/) cover Instagram, TikTok, YouTube, Facebook, X, Pinterest, Vimeo and Twitch. The developer tools in this section cover Instagram only; TikTok and YouTube support in the CLI and API is planned.
## Related pages
- [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/)
- [Comparison](https://electricblaze.com/developer/instagram-feed/comparison.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Changelog](https://electricblaze.com/developer/changelog.html)
- [Pricing](https://electricblaze.com/pricing.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/
---
# Instagram feed on a website, without owning the Meta integration
**Status, checked 2026-09-18:** real Instagram posts work today through the ElectricBlaze widget embed. The `electricblaze` CLI 0.1.2 adds a typed component with demo data. The JSON API for that component, with `login` and `connect`, ships in 0.2.
> Everything ElectricBlaze offers developers and coding agents for Instagram, with the state of each part on the date of the last check.
Canonical: https://electricblaze.com/developer/instagram-feed/
## At a glance
- **Use it when:** a site needs recent Instagram posts or reels and nobody wants to create a Meta app, run a token refresh job or cache images.
- **Do not use it when:** you need a public JSON feed with a reel flag today; see [the comparison](https://electricblaze.com/developer/instagram-feed/comparison.html) for providers that have one.
- **What the human does:** the Instagram account owner signs in to ElectricBlaze and connects a Business or Creator account. That is the only step an agent cannot do.
- **Next step:** open the guide for your framework below, or run `npx electricblaze@0.1.2 add instagram-feed --json`.
## What works today and what ships in 0.2
| Part | State | What it gives you |
|---|---|---|
| Widget embed | Works today | Real posts from a connected account, laid out in the dashboard, two lines of HTML |
| CLI `add`, `preview`, `doctor`, `skill` | Works today, demo data | Component, types, 12 demo posts, checks with next steps |
| Feed schema v1 | Published | [schema.json](https://electricblaze.com/developer/instagram-feed/schema.json), [sample-feed.json](https://electricblaze.com/developer/instagram-feed/sample-feed.json) |
| Agent skill | Published | Workflow and dated API facts for Claude Code, Codex and others |
| JSON API, `login`, `connect`, `list` | Ships in 0.2 | Real posts in the generated component, by API key |
| TikTok, YouTube in the CLI and API | Planned | Nothing yet; `add tiktok-feed` exits 2. Their embeddable widgets are on the main site |
The same state as data, for agents that prefer to parse it:
Status on 2026-09-18:
```json
{
"checked": "2026-09-18",
"instagram": {
"widget_embed": "available",
"cli": { "version": "0.1.2", "data": "demo" },
"json_api": "planned for 0.2",
"schema": "https://electricblaze.com/developer/instagram-feed/schema.json"
},
"tiktok": "planned",
"youtube": "planned"
}
```
## Why not call the Instagram API directly
You can, and the [own-code route](https://electricblaze.com/developer/instagram-feed/nextjs.html#own-code) is documented on every framework page. It carries four standing costs:
- Only professional accounts are served: the Instagram API with Instagram Login serves Instagram professional accounts (Business or Creator); personal accounts are not supported ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18). The Basic Display API that once served personal accounts was shut down on 2024-12-04.
- A long-lived token lasts 60 days and has to be refreshed by a job you run ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18).
- Image links from `media_url` expire, and Meta publishes no lifetime for them. A cached or static page ends up with broken images.
- Reading someone else's account, for a client site, requires Advanced Access: App Review plus Business Verification ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18).
With ElectricBlaze the account owner connects Instagram once in our dashboard. Your code never holds a token and never talks to Meta.
## Guides by framework
| Framework | CLI kit | Guide |
|---|---|---|
| Next.js (App Router, Pages Router, static export) | `next`: Server Component | [Next.js guide](https://electricblaze.com/developer/instagram-feed/nextjs.html) |
| React on Vite (Lovable, Bolt, v0) | `data` | [React guide](https://electricblaze.com/developer/instagram-feed/react.html) |
| React Router v7 and Remix | `data` | [React Router guide](https://electricblaze.com/developer/instagram-feed/react-router.html) |
| Astro | `data` | [Astro guide](https://electricblaze.com/developer/instagram-feed/astro.html) |
| SvelteKit | `data` | [SvelteKit guide](https://electricblaze.com/developer/instagram-feed/sveltekit.html) |
| Nuxt | `data` | [Nuxt guide](https://electricblaze.com/developer/instagram-feed/nuxt.html) |
| Plain HTML, GitHub Pages, shared hosting | `html`: script and CSS | [Static site guide](https://electricblaze.com/developer/instagram-feed/static-site.html) |
Every guide shows the widget embed for real posts today, the CLI kit for the design, and the own-code route with its obligations.
## Instagram API reference
Precise answers to the questions that stop an integration, each with Meta sources and check dates:
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html): short-lived, long-lived, `ig_refresh_token`, a refresh schedule.
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html): what `/media` returns, reels, carousel children, thumbnails.
- [App Review and access levels](https://electricblaze.com/developer/instagram-feed/app-review.html): when your own feed needs no review, when a client site does.
- [Basic Display API alternative](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html): what replaced it and how fields map.
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html): error 190, empty data, throttling, missing fields.
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html): "URL signature expired" and how to keep images alive.
## Tools and machine-readable files
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html): commands, exit codes, the files it writes, how to verify a release.
- [The agent skill](https://electricblaze.com/developer/instagram-feed/skill.html): install it once, and the agent follows the same workflow in every project.
- [Feed schema v1](https://electricblaze.com/developer/instagram-feed/schema.html): TypeScript types, JSON Schema, a 12-post sample.
- [Changelog](https://electricblaze.com/developer/changelog.html) with an Atom feed.
- [llms.txt](https://electricblaze.com/llms.txt) lists every developer page; each page also has a Markdown twin at the same address with `.md`.
## FAQ
### Can I show an Instagram feed on a website without a Meta developer account?
Yes. With the ElectricBlaze widget the account owner connects Instagram in our dashboard and you paste two lines of HTML. Any hosted provider works the same way; your own code does not.
### Does ElectricBlaze have a JSON API for Instagram?
Not publicly yet. The CLI writes a typed component with demo data today, and the JSON API that feeds it real posts ships in 0.2.
### Which Instagram accounts can be connected?
The Instagram API serves professional accounts, Business or Creator. Personal accounts lost API access when Basic Display was shut down on 2024-12-04.
### Does it work with reels?
The widget shows reels as videos without a separate mark. Schema v1 and the CLI component have `format: "reel"`; real reel data arrives with the 0.2 API.
### Is there a free plan?
Yes: 5 widgets and 1000 views a month, refreshed every 24 hours. See [pricing](https://electricblaze.com/pricing.html).
## Related pages
- [Compare providers](https://electricblaze.com/developer/instagram-feed/comparison.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Developers home](https://electricblaze.com/developer/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/
---
# Instagram feed APIs and widgets for developers, compared in September 2026
**Status, checked 2026-09-18:** facts below were read from each vendor's public pricing and documentation on 2026-09-18. ElectricBlaze is one of the options: its widget shows real posts today, its JSON API ships in 0.2, and several data features are behind the leaders.
> Nine ways to get Instagram posts onto a site, compared on what a developer or a coding agent checks first: JSON access, reels and carousels, image links that last, refresh, limits and price. Every cell comes from the vendor's own pages, read on 2026-09-18.
Canonical: https://electricblaze.com/developer/instagram-feed/comparison.html
## At a glance
- **Use it when:** you are choosing how to show an Instagram feed on a site and need to justify the choice with dated facts: public JSON or embed, reels, image hosting, limits and price.
- **Do not use it when:** you need data from several networks with moderation and analytics. The aggregators (Juicer, Curator, Flockler, EmbedSocial) are compared here only on Instagram.
- **What the human does:** whichever vendor you pick, the Instagram account owner connects the account once. With your own code they also create a Meta app and hand over a token.
- **Next step:** pick from [the decision list](https://electricblaze.com/developer/instagram-feed/comparison.html#pick) below, then open the framework guide: [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html), [Astro](https://electricblaze.com/developer/instagram-feed/astro.html), [plain HTML](https://electricblaze.com/developer/instagram-feed/static-site.html).
## Data access: JSON, reels, carousels, images
This is what decides whether a static or cached site keeps working. The questions: can code fetch JSON without a secret, can it tell a reel from a video, does it get the items of a carousel, and do image links outlive the cache.
| Provider | JSON access | Reels marked | Carousel items | Images |
|---|---|---|---|---|
| ElectricBlaze widget | Embed only; its data URL is signed and expires within hours (observed) | No: a reel arrives as `VIDEO` (observed) | No: cover only (observed) | Not established yet for connected accounts (open question) |
| ElectricBlaze CLI | Typed component with demo data; JSON API with a key ships in 0.2 | `format: "reel"` in schema v1 | `media[]` in schema v1 | Demo media pinned on jsDelivr |
| Behold | Public `https://feeds.behold.so/{feedId}`, no key; optional domain whitelist | Yes, `isReel` | Yes, `children[]` with sizes | Own CDN, WebP at 400, 700, 1000 and 2000 px |
| Feedframer | REST, GraphQL and RSS with an `api_key` the docs call safe for the browser; CORS open | Yes, `mediaType` includes `REELS` | Yes, `children[]` | "Media is hosted by Feedframer"; sizes on paid plans |
| Simpleembed | Public `https://simpleembed.com/api/feeds/{FEED_ID}`, CORS open | No flag | Count only (`childCount`) | Proxied `imageUrl` that the docs say does not expire; videos give a poster only |
| Curator.io | `/v1/feeds/{FEED_ID}/posts` with a feed public key or an API key; API on the Business plan | No flag (`has_video`) | `images[]` / `media[]` | Own CDN, three sizes with dimensions |
| Juicer | `https://api.juicer.io/v1/feeds/{id}/posts` with a secret Bearer key; syncs cost credits | No flag | `media[]` | Not found in the docs |
| EmbedSocial | API with a key on Premium and Enterprise; endpoint docs are inside the dashboard | Not found | Not found | Not found |
| Flockler | `https://api.flockler.app/v2/{siteUuid}/posts` with an API key; Premium and Agency plans | Not documented (`postType`) | `attachments[]` | Not documented |
| Your own code | `graph.instagram.com` with a 60-day token, server side only | `media_product_type`, documented for Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18) | Only if `children` is expanded in `fields=` | Instagram CDN links that expire; Meta publishes no lifetime |
Sources: [Behold docs](https://behold.so/docs/json-feeds/), [Behold API reference](https://behold.so/docs/api-feeds-reference/), read 2026-09-18; [Feedframer docs](https://feedframer.com/docs/rest-api), read 2026-09-18; [Simpleembed docs](https://simpleembed.com/docs/embedding), read 2026-09-18; [Curator API](https://curator.io/docs/api/posts), read 2026-09-18; [Juicer API](https://developers.juicer.io/introduction), read 2026-09-18; [EmbedSocial API help](https://embedsocial.com/help/en/articles/3793192-do-you-offer-api-key-and-api-documentation), read 2026-09-18; [Flockler API](https://developers.flockler.com/api), read 2026-09-18; ElectricBlaze: observed in `widget.js` and our public endpoints, 2026-09-17 and 2026-09-18.
## Plans: refresh, free tier, limits, price
Prices are the lowest paid plan as shown to a US visitor. "At the limit" matters more than the limit: a feed that turns empty looks like a bug on the client's site.
| Provider | Refresh | Free tier | At the free limit | Lowest paid | Client connect link |
|---|---|---|---|---|---|
| ElectricBlaze | Free every 24 hours, Start every hour | 5 widgets, 1000 views a month, credit link | Not established yet (open question) | $5 per month, billed yearly | Not established yet (open question) |
| Behold | Free daily, Starter hourly, Pro and Business 5 min, Scale 1 min | 1 feed, 1,200 views a month, 6 posts | Account paused until next month | $10 a month | Yes, an auth URL per account |
| Feedframer | Free daily, paid hourly | 1 account, 6 posts, unlimited requests | Not found | $6 a month or $57.60 a year | Not found |
| Simpleembed | About once a day | 1 feed, unlimited views, watermark | Not found | $7 a month | Yes, a shareable auth link |
| Curator.io | Free 24 hours, Pro 60 min, Business 15 min | 3 sources, 2,000 views, "Powered by Curator" | Upgrade prompts; a paused account keeps showing old posts | Pro, shown as $23 and $25 a month (yearly and monthly) | Not found |
| Juicer | Free 24 hours, Lite 60 min, Pro 10 min | 1 feed, 300 views a month, branding | Not found | $15 a month ($13.50 yearly); API plan $99 | A single-use OAuth link through the API |
| EmbedSocial | Free 24 hours, Pro 3 hours, Premium 30 min | 1 source, 500 views a month | Widget stops until the 1st of the month | €29 a month; API from €99 | Not found |
| Flockler | 5 to 15 minutes | None, 14-day trial | n/a | $129 a month ($110 yearly); API on custom Premium | Not found |
| Your own code | Your choice; limit Instagram Platform calls (except messaging) are limited to 4800 x number of impressions per 24 hours; usage is reported in the X-Business-Use-Case-Usage header; throttling returns error code 80002 | Free | n/a | Engineering time | Your own OAuth flow |
Sources: [ElectricBlaze pricing](https://electricblaze.com/pricing.html), read 2026-09-18; [Behold pricing](https://behold.so/pricing), [connecting accounts](https://behold.so/docs/api-connecting-accounts/), read 2026-09-18; [Feedframer pricing](https://feedframer.com/pricing), read 2026-09-18; [Simpleembed](https://simpleembed.com), read 2026-09-18; [Curator pricing](https://curator.io/pricing), [limits](https://help.curator.io/what-happens-when-you-exceed-limits-b392k), read 2026-09-18; [Juicer pricing](https://juicer.io/pricing), read 2026-09-18; [EmbedSocial pricing](https://embedsocial.com/pricing/), [view limits](https://embedsocial.com/help/en/articles/8099930-is-there-a-limit-on-widget-views-on-my-website), read 2026-09-18; [Flockler pricing](https://flockler.com/pricing), read 2026-09-18; [Meta: rate limiting](https://developers.facebook.com/docs/graph-api/overview/rate-limiting), checked 2026-09-18.
SnapWidget and Elfsight also show Instagram feeds, but only as embeds. Elfsight's developer API manages widgets and returns no posts, so neither fits code that needs data.
## Which one to pick
- **A static or cached site that needs public JSON with reels and lasting images today:** Behold or Feedframer. Both mark reels, expand carousels and serve images from their own CDN.
- **The cheapest public JSON with lasting images:** Simpleembed, if you can live without a reel flag and carousel items.
- **A styled feed on any site without writing a renderer, with several widgets on the free plan:** the ElectricBlaze widget. It is an embed, not a data API, today.
- **A site built with a coding agent that should get a typed component now and real data later:** the ElectricBlaze CLI with its [agent skill](https://electricblaze.com/developer/instagram-feed/skill.html). It runs on demo data until 0.2; use the widget for real posts meanwhile.
- **Many networks, moderation, a social wall:** Juicer, Curator, Flockler or EmbedSocial. Their JSON APIs sit on higher plans.
- **Full control and no vendor:** your own code. Budget for a [token refresh job](https://electricblaze.com/developer/instagram-feed/access-token.html) and an [image cache](https://electricblaze.com/developer/instagram-feed/media-url-expired.html).
## Where ElectricBlaze stands, plainly
We publish our weak spots because an agent will find them anyway.
- The widget works on real accounts today and is configured in a dashboard: layout, columns, captions, slider or gallery. The Start plan is $5 per month, billed yearly.
- The widget's data does not mark reels and does not expand carousels. Behold and Feedframer do.
- There is no public JSON feed yet. The JSON API behind the CLI component ships in 0.2.
- The CLI, the schema, the agent skill and the framework guides on this site are built for coding agents. Among the others, Juicer ships an MCP server and Behold ships React packages on npm.
Questions we have not answered yet, and will not claim until we do:
- **For a connected account, do media_url and thumbnail_url point at Instagram CDN links that expire, or at copies on our own storage?** Agents reject a feed whose image links expire when the site is static or cached. It was a dealbreaker in every simulated session.
- **Can a project be given a stable feed URL by widget id, instead of the signed URL that expires within hours?** Without a stable URL there is nothing for an agent to put into code.
- **Can a public feed URL be restricted to a list of domains?** Asked by agents as the safe replacement for a token in client-side code.
- **What happens when the free 1,000 views are used up: does the widget stop, show old data, or go blank?** Behold pauses the account, and an agent treats that as the section going empty.
- **Can an account owner connect through a link we send them, without sharing a password and without an account on our side?** The agency case: 40 client sites, no Meta app per client.
- **Does the backend receive media_product_type and carousel children from Meta and drop them, or does it never request them?** Reels and carousels are an explicit requirement in agent sessions; Behold exposes both.
- **Is there a status page or an incident history?** Checked when an agent decides whether a vendor can be depended on.
## How we compared
We read each vendor's pricing page and public developer documentation on 2026-09-18, from a US location. A cell says "not found" when the public pages do not answer the question; it does not mean the feature is missing. Prices change often; follow the source links before you commit.
The criteria come from simulated coding-agent sessions in which an agent had to add an Instagram feed to a Next.js or Astro site. The deciding questions were always the same: expiring image links, the reel flag, a key that can live in the browser, the refresh interval, and the free limit behaviour.
The checklist as data, for agents that want to score a vendor:
```json
{
"public_json_without_secret": "Can a static site fetch the feed with no server?",
"reels_flag": "Can code tell a reel from a video?",
"carousel_items": "Are carousel children in the response?",
"stable_images": "Do image URLs outlive a cached page?",
"refresh_interval": "How old can a post be on the page?",
"free_limit_behaviour": "What happens when the free quota runs out?",
"client_connect": "Can a client connect the account through a link we send?"
}
```
## FAQ
### What is the best Instagram feed API for developers in 2026?
For public JSON with reels and lasting images, Behold and Feedframer led on 2026-09-18. For an embed with no renderer to write, the ElectricBlaze widget. For full control, your own code with a token refresh job.
### Is there a Behold alternative for developers?
Feedframer is the closest on data: reels, carousel items, hosted images. Simpleembed is cheaper with fewer fields. ElectricBlaze adds an agent skill and a CLI but has no public JSON until 0.2.
### Which providers keep image links working on a cached site?
Behold, Feedframer, Simpleembed and Curator say so in their docs. Juicer, EmbedSocial and Flockler do not document it. For ElectricBlaze it is an open question.
### Can I avoid a Meta developer app?
Yes, with any hosted provider in this list: the account owner connects Instagram through the provider. Only your own code needs a Meta app.
### How often is this page checked?
The date in the status line is the date of the last full check. Every cell links to the page it came from.
## Related pages
- [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Basic Display alternative](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
- [Agent skill](https://electricblaze.com/developer/instagram-feed/skill.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/comparison.html
---
# Instagram feed in Next.js: App Router, Pages Router and static export
**Status, checked 2026-09-18:** the ElectricBlaze widget shows real posts in Next.js today. `npx electricblaze add instagram-feed` writes a Server Component with demo data; the JSON API that feeds it real posts ships in 0.2.
> Three working ways to put Instagram posts on a Next.js site in 2026, what each one needs from the account owner, and the code for each.
Canonical: https://electricblaze.com/developer/instagram-feed/nextjs.html
## At a glance
- **Use it when:** the site runs on Next.js 13.4 or newer (App Router or Pages Router, on Vercel or as a static export) and needs a grid of recent Instagram posts or reels.
- **Do not use it when:** you already maintain a Meta app with a token refresh job and an image cache. Then keep your own code and read [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) before launch.
- **What the human does:** the account owner signs in at [a.electricblaze.com](https://a.electricblaze.com/), connects a Business or Creator Instagram account and copies the widget id. Nobody creates a Meta developer app.
- **Next step:** run `npx electricblaze@0.1.2 add instagram-feed --json` for the demo component, or paste the widget snippet below when the owner has a widget id.
## Pick a route
All three routes end with posts on the page. They differ in who touches Meta, where the HTML is rendered and what breaks after launch.
| Route | Real posts | Rendered | The human does | Breaks when |
|---|---|---|---|---|
| Widget embed | Today | In the browser, by `widget.js` | Connects the account in the ElectricBlaze dashboard, copies the widget id | The Free plan passes 1,000 views a month ([pricing](https://electricblaze.com/pricing.html)) |
| CLI component | Demo data today, real posts in 0.2 | On the server, your JSX and CSS | Nothing until 0.2 | Not yet: it reads a bundled JSON file |
| Your own Graph API code | Today | On the server, your JSX and CSS | Creates a Meta app, converts to a professional account, hands over a token | The token is not refreshed within 60 days, or cached image links expire |
If the site must show real posts this week and the owner will not create a Meta app, use the widget. If the design matters more than the data, start with the CLI component and switch it to real data in 0.2. If you need fields the widget does not expose, such as the items inside a carousel, write your own code.
## Route 1: the widget embed, real posts today
The owner creates an Instagram Feed widget in the dashboard and gets a snippet with a widget id. Outside React the snippet is two lines:
```html
```
In Next.js load the script with `next/script` and keep the container as a plain element. This works in the App Router and in the Pages Router, and in a Server Component, because `next/script` handles the client part itself.
components/InstagramWidget.tsx:
```tsx
import Script from "next/script";
// The widget id comes from the ElectricBlaze dashboard (Add to website).
export function InstagramWidget({ widgetId }: { widgetId: string }) {
return (
<>
{/* Keep electricblaze-id-... as the FIRST class: the loader reads className.split(" ")[0]. */}
>
);
}
```
app/page.tsx:
```tsx
import { InstagramWidget } from "@/components/InstagramWidget";
export default function Home() {
return (
On Instagram
);
}
```
Three details of the loader matter in a React app. We read them from `widget.js` version 0.0.2 on 2026-09-18:
- **Client-side navigation works.** The loader scans the document when the DOM is ready, then watches it with a MutationObserver (childList, subtree), so a widget on a page reached through `` still mounts.
- **The id is the first class.** `className="electricblaze-id-abc123 my-grid"` works, `className="my-grid electricblaze-id-abc123"` renders nothing.
- **Loading states are classes.** The loader adds electricblaze-state-loading, -mounting, -mounted or -error to the container. Reserve height with CSS on `.electricblaze-state-loading` to avoid layout shift.
Layout, columns, captions and the slider or gallery mode are set in the dashboard, not in code. The published plans: Free has 5 widgets, 1000 views a month and a refresh every 24 hours; Start is $5 per month, billed yearly and refreshes every hour.
## Route 2: a Server Component from the CLI
One command writes a typed component, a loader and twelve demo posts. It never prompts, never overwrites files you edited, and prints JSON when asked:
```sh
npx electricblaze@0.1.2 add instagram-feed --json
```
Output in a Next.js project with the `@/*` alias (trimmed):
```json
{
"ok": true,
"framework": "next",
"files": [
{ "path": "components/eb/InstagramFeed.tsx", "status": "written" },
{ "path": "components/eb/InstagramFeed.module.css", "status": "written" },
{ "path": "lib/eb/instagram.ts", "status": "written" },
{ "path": "lib/eb/feed.d.ts", "status": "written" },
{ "path": "lib/eb/demo/instagram.json", "status": "written" },
{ "path": ".env.example", "status": "written" }
],
"demo": true,
"notes": [
"Render it in any App Router page: import InstagramFeed from \"@/components/eb/InstagramFeed\"; then "
]
}
```
app/page.tsx:
```tsx
import InstagramFeed from "@/components/eb/InstagramFeed";
export default function Home() {
return ;
}
```
`InstagramFeed` is an async Server Component: no client bundle, no `useEffect`, plain `` tags with width and height. Every post follows [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html): `type` says what to render, `format` says whether Instagram calls it a post or a reel. A "demo feed" label stays on while `feed.demo` is true.
Leave `ELECTRICBLAZE_API_KEY` unset until 0.2. With a key the loader calls the 0.2 endpoint, which does not answer yet, and throws. `npx electricblaze doctor` says so too.
## Pages Router and static export
With `output: 'export'` Next.js renders every page at build time and ships plain files. Both ElectricBlaze routes work there:
- The widget runs in the browser, so a static export on GitHub Pages, S3 or shared hosting shows fresh posts without a rebuild.
- The CLI component renders at build time. With demo data that is all it needs. With the 0.2 API, posts change when the site is rebuilt, so schedule a rebuild or use the widget.
In the Pages Router use the same `InstagramWidget` component. For the CLI component, import `getInstagramFeed` from `lib/eb/instagram.ts` in `getStaticProps` and pass `feed.posts` to your own markup.
## Route 3: your own code on the Instagram API
This is the route an agent usually writes first. It works, and it comes with four obligations that the snippet does not show:
1. The account must be a Business or Creator account; Meta closed the Basic Display API for personal accounts on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
2. Someone creates a Meta app and produces a long-lived token. It lasts 60 days and must be refreshed before it expires ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). See [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
3. `media_url` links point at Instagram's CDN and stop working after a while, so an ISR page cached for days shows broken images. See [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html).
4. Telling a reel from another video needs `media_product_type`, and the IG Media reference from Meta marks it, like `caption`, as available with Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18). Test both fields with your own token. See [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).
lib/instagram.ts:
```ts
import "server-only";
// caption is documented for Facebook Login only (checked 2026-09-18): test it with your token.
const FIELDS = "id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,children{media_type,media_url,thumbnail_url}";
export async function getInstagramPosts(limit = 12) {
const url = new URL("https://graph.instagram.com/me/media");
url.searchParams.set("fields", FIELDS);
url.searchParams.set("limit", String(limit));
url.searchParams.set("access_token", process.env.IG_ACCESS_TOKEN!);
// Revalidate well inside the life of a media_url link.
const res = await fetch(url, { next: { revalidate: 3600 } });
if (!res.ok) throw new Error(`Instagram API ${res.status}: ${await res.text()}`);
return (await res.json()).data as Array>;
}
```
Keep the token on the server. `import "server-only"` makes the build fail if a client component imports this file, and the token never reaches the browser.
## What goes wrong in Next.js
- **`next/image` rejects the host.** Instagram and demo image hosts are not in `images.remotePatterns`. Either add them or use ``, as the CLI component does.
- **The widget shows nothing.** The container lost its first class, or a Content Security Policy blocks `s.electricblaze.com` and `api.electricblaze.com`. Add both to `script-src` and `connect-src`.
- **Posts froze after a deploy.** A fully static page fetched once at build time. Use `revalidate`, a rebuild schedule or the widget.
- **Error 190 after two months.** The long-lived token expired. See [feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html) for the error codes.
## FAQ
### Does the ElectricBlaze widget work with the Next.js App Router?
Yes. Load `widget.js` with `next/script` and render a `div` whose first class is `electricblaze-id-` plus the widget id. The loader watches the DOM, so client-side navigation mounts it too.
### Can I show an Instagram feed in Next.js without a Meta developer app?
Yes, with the widget: the account owner connects Instagram in the ElectricBlaze dashboard and you paste the snippet. Your own Graph API code always needs a Meta app and a token.
### Is the CLI component showing real posts?
Not yet. It renders twelve demo posts and labels them. The JSON API that feeds it real posts ships in 0.2; until then use the widget for real posts.
### Does it work with output: 'export'?
Yes. The widget runs in the browser and needs no server. The CLI component renders at build time.
### Can I filter reels only?
In the CLI component pass `formats={["reel"]}`. In your own code the reel flag is `media_product_type` equal to `REELS`, a field Meta documents for Facebook Login only; check that your token returns it.
## Related pages
- [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/nextjs.html
---
# Instagram feed in Astro: widget, build-time JSON and an on-demand endpoint
**Status, checked 2026-09-18:** real Instagram posts reach an Astro site through the ElectricBlaze widget today. For Astro, `npx electricblaze add instagram-feed` writes demo JSON and types only; the JSON API with real data ships in 0.2.
> How to show Instagram posts on an Astro 5, 6 or 7 site deployed to Cloudflare or Netlify, what survives a fully static build, and where the Instagram token is allowed to live.
Canonical: https://electricblaze.com/developer/instagram-feed/astro.html
## At a glance
- **Use it when:** the site is built with Astro 5 or newer, deploys to Cloudflare, Netlify or any static host, and one section should list recent Instagram posts.
- **Do not use it when:** real posts must be baked into the prerendered HTML today. The widget draws in the browser and the CLI JSON is sample data, so before 0.2 only your own build-time code does that.
- **What the human does:** the site owner creates an Instagram Feed widget at [a.electricblaze.com](https://a.electricblaze.com/), connects the Instagram account there and sends you the widget id. No Meta developer app is involved.
- **Next step:** paste the widget component below if a widget id exists. Otherwise run `npx electricblaze@0.1.2 add instagram-feed --json` and build the grid from the demo JSON.
## Three options for Astro
Astro renders at two moments: during the build and, with an adapter, on each request. The widget adds a third moment, the visitor's browser. Each option below lives in one of them.
| Option | Posts | Runs at | Needs from the owner | Weak spot |
|---|---|---|---|---|
| ElectricBlaze widget | Real, today | Visitor's browser | A widget id from the dashboard | Posts are not in the static HTML; the Free plan includes 1000 views a month ([pricing](https://electricblaze.com/pricing.html)) |
| CLI demo JSON | Sample data until 0.2 | Build | Nothing | Twelve fixed posts from a fictional coffee roaster |
| Your own Graph API code | Real, today | Build or request | A Meta app, a professional account, a token | The token lasts 60 days and `media_url` links expire |
Pick the widget when the owner wants real posts soon and nobody will look after a Meta app. Pick the demo JSON when the layout comes first and the account can wait. Pick your own code when posts have to exist in the HTML at build time.
## The widget as an Astro component
The owner copies a two-line embed from the dashboard:
```html
```
Wrap it in a component and mark the script `is:inline`. Astro processes and bundles `
```
Drop `` into any page or layout. The grid appears once the page has loaded. Columns, rows, captions and the choice between gallery and slider are chosen in the dashboard, so the component takes no layout props.
The loader reads the id from the first class only. Your own classes may follow it, never precede it.
## View Transitions with ClientRouter
With `` from `astro:transitions` in the layout, Astro fetches the next page and replaces the whole `` instead of doing a full load. We read `widget.js` 0.0.2 on 2026-09-18 to check how it copes:
- It scans the document when the DOM is ready, then watches it with a MutationObserver (childList, subtree). The observer sits on `document` itself, so a swapped-in `` that carries a widget container is noticed and mounted.
- Astro may execute an inline script again when a visitor comes back to a page. That costs nothing: the loader checks for `window.electricblaze` first and a second copy exits.
You therefore need neither `data-astro-rerun` nor `transition:persist` for the widget. Moving the `
```
src/components/InstagramWidget.tsx:
```tsx
type Props = { widgetId: string; className?: string };
export function InstagramWidget({ widgetId, className }: Props) {
// widget.js reads the id from the first class only: extra classes go after it.
const classes = ["electricblaze-id-" + widgetId, className].filter(Boolean).join(" ");
return ;
}
```
src/pages/Index.tsx:
```tsx
import { InstagramWidget } from "@/components/InstagramWidget";
export default function Index() {
return (
On Instagram
);
}
```
Lovable maps `@/` to `src/`. In a bare `npm create vite` project, import the component with a relative path; that is how we built it with the Vite 8 React template and TypeScript 6 (`tsc -b && vite build`, 2026-09-18).
React mounts the `div` after the first scan by the loader, and that is fine. We read `widget.js` 0.0.2 on 2026-09-18: it keeps a MutationObserver on the document and mounts any matching element that shows up later, including after an in-app route change.
- **The widget id is public.** It ends up in the rendered markup anyway, so `VITE_EB_WIDGET_ID` in `.env` is fine. The Instagram credentials never touch your code.
- **Pass layout classes after the id.** The component above does this for you. A `className` that starts with `mt-6` or any other class leaves the container empty.
- **React 19 alternative.** A component may render ``; React moves it into `` and loads each `src` once ([react.dev](https://react.dev/reference/react-dom/components/script), checked 2026-09-18). The `index.html` tag avoids the React version question.
- **Hold the space.** Give `.electricblaze-state-loading` a `min-height`, and the page will not jump when the posts arrive.
Grid size, captions and slider mode are widget settings in the dashboard, not props. Per the pricing page, a view is counted when a page containing the widget is opened and the widget loads. Free allows 1000 of those a month and refreshes every 24 hours; Start costs $5 per month, billed yearly and refreshes every hour ([pricing](https://electricblaze.com/pricing.html)).
## Demo data from the CLI, your component
Version 0.1.2 has no React component template. When `package.json` lists `vite`, `add` chooses the `data` kit and writes two files: the schema types and twelve demo posts.
```sh
npx electricblaze@0.1.2 add instagram-feed --json
```
Output in a Vite project with a `src/` folder (`dir` removed, second note trimmed):
```json
{
"ok": true,
"component": "instagram-feed",
"source": "instagram",
"framework": "data",
"files": [
{ "path": "src/lib/eb/feed.d.ts", "status": "written" },
{ "path": "src/lib/eb/demo/instagram.json", "status": "written" }
],
"demo": true,
"notes": [
"vite detected: no component template for it yet, so only the feed JSON and types are written. Build the component against them (see the skill), or pass --framework=html for the plain HTML kit.",
"Read src/lib/eb/demo/instagram.json in the framework's loader ... Types: src/lib/eb/feed.d.ts."
],
"next": "To show real posts today, connect the account in the ElectricBlaze widget and paste its embed snippet (https://electricblaze.com). The JSON API for this component (npx electricblaze connect instagram) ships in 0.2. Until then this is a demo feed."
}
```
The component is yours. This one fits in 25 lines and takes its types from `feed.d.ts`:
src/components/InstagramGrid.tsx:
```tsx
import type { Feed } from "../lib/eb/feed";
import demo from "../lib/eb/demo/instagram.json";
const feed = demo as Feed;
export function InstagramGrid({ limit = 8, formats }: { limit?: number; formats?: string[] }) {
const posts = feed.posts.filter((p) => !formats || formats.includes(p.format)).slice(0, limit);
return (
{feed.demo && demo feed}
);
}
```
Vite imports JSON without a plugin. The demo feed mixes square, 4:5, 9:16 and 16:9 media, so the grid needs `object-fit: cover` on the images. `formats={["reel"]}` keeps reels only, because each post carries `format` next to `type` ([schema v1](https://electricblaze.com/developer/instagram-feed/schema.html)).
Keep the label while `feed.demo` is `true`. Only a connected account, which needs the 0.2 API, sets it to `false`.
## Your own token: a Supabase Edge Function
Many Lovable projects already have a Supabase backend, which makes an Edge Function the closest server. It keeps the token out of the bundle. The Meta side of the work stays with you:
1. The Instagram account must be professional. The API with Instagram Login serves Business and Creator accounts ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18); the Basic Display API that served personal accounts ended on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
2. A long-lived token stays valid for 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). Someone renews it and stores the new value with `supabase secrets set` before it runs out. See [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
3. Image links age. `media_url` points at Instagram's CDN, so serve fresh links on every call and never cache the JSON for days. See [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html).
Supabase now documents functions as a default export with a `fetch` handler wrapped in `withSupabase`, not `Deno.serve` ([Supabase AI prompt for Edge Functions](https://supabase.com/docs/guides/ai-tools/ai-prompts/edge-functions), checked 2026-09-18). The wrapper also answers CORS preflight requests.
supabase/functions/instagram-feed/index.ts:
```ts
import { withSupabase } from "npm:@supabase/server@^1";
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export default {
// "publishable": the caller must send the project's publishable key, as supabase-js does.
fetch: withSupabase({ auth: "publishable" }, async () => {
const url = new URL("https://graph.instagram.com/me/media");
url.searchParams.set("fields", FIELDS);
url.searchParams.set("limit", "12");
url.searchParams.set("access_token", Deno.env.get("IG_ACCESS_TOKEN") ?? "");
const res = await fetch(url);
if (!res.ok) {
console.error("Instagram API", res.status, await res.text());
return Response.json({ error: "instagram_unavailable" }, { status: 502 });
}
const { data } = await res.json();
return Response.json({ posts: data });
}),
};
```
supabase/config.toml:
```toml
[functions.instagram-feed]
verify_jwt = false
```
```sh
supabase secrets set IG_ACCESS_TOKEN=your-long-lived-token
supabase functions deploy instagram-feed
```
A publishable key is not a JWT, so the gateway check is off and `withSupabase` validates the key instead.
src/hooks/useInstagramPosts.ts:
```ts
import { useEffect, useState } from "react";
import { supabase } from "@/integrations/supabase/client";
export type IgPost = {
id: string;
permalink: string;
media_type: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
media_url?: string;
thumbnail_url?: string;
caption?: string;
};
export function useInstagramPosts() {
const [posts, setPosts] = useState([]);
useEffect(() => {
supabase.functions.invoke<{ posts: IgPost[] }>("instagram-feed").then(({ data, error }) => {
if (error) console.error("instagram-feed", error);
else setPosts(data?.posts ?? []);
});
}, []);
return posts;
}
```
Lovable keeps the Supabase client in `src/integrations/supabase/client.ts`; other projects import their own `createClient` instance. The hook type-checks against `@supabase/supabase-js` 2.116. For a video, render `thumbnail_url`; for other types, `media_url`.
Every page view becomes one Graph API call. Meta allows 4800 calls per 24 hours multiplied by the account's impressions ([Meta: rate limiting](https://developers.facebook.com/docs/graph-api/overview/rate-limiting), checked 2026-09-18), so a busy page on a small account can run out. Cache the response for minutes, not days.
## When the feed does not show
- **Empty `div`, no console error.** Some other class comes before `electricblaze-id-...`. Only the first class is read.
- **The token appears in `dist/assets`.** Client code read it through `import.meta.env`. Treat it as leaked: create a new token, delete the old value from `.env`, and move the call into the Edge Function.
- **401 from the function.** `verify_jwt` is still on while the app sends a publishable key rather than a user JWT. Set it to `false` in `config.toml` and deploy again.
- **The function log shows an expired-token error.** Sixty days passed without renewal. [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html) lists the Graph API errors.
## FAQ
### Can a React app call the Instagram API straight from the browser?
Only by handing the access token to every visitor, because Vite inlines `VITE_` variables into the bundle. Call the API from an Edge Function, or use the widget, which keeps the Instagram connection on the ElectricBlaze side.
### Does the widget survive client-side routing in a Vite SPA?
Yes. `widget.js` watches the DOM and mounts containers that appear after a navigation. Load it once in `index.html`; a second copy of the script does nothing.
### Does npx electricblaze add instagram-feed generate a React component?
Not in 0.1.2. In a Vite project it writes `feed.d.ts` and a demo feed, and the `InstagramGrid` component on this page reads both. Real posts through the CLI arrive with the JSON API in 0.2.
### Is the widget id a secret?
No. Every site that embeds a widget prints its id in the page. Putting it in `.env` is a convenience, not protection.
### Do I need Supabase for an Instagram feed in Lovable?
No. The widget and the CLI kit need no backend. Supabase matters only for your own Graph API code.
## Related pages
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [React Router and Remix](https://electricblaze.com/developer/instagram-feed/react-router.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [CLI reference](https://electricblaze.com/developer/instagram-feed/cli.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/react.html
---
# Instagram feed in React Router framework mode and Remix: loader, clientLoader, prerender
**Status, checked 2026-09-18:** the ElectricBlaze widget shows real posts in React Router v7 and v8 apps today, in every render mode. `npx electricblaze add instagram-feed` writes demo JSON and types for a route `loader`; the API for real posts ships in 0.2.
> A React Router app can render on the server, at build time or only in the browser. Which of the three your config picks decides where Instagram data may load and where a token may live.
Canonical: https://electricblaze.com/developer/instagram-feed/react-router.html
## At a glance
- **Use it when:** the app runs React Router v7 or v8 in framework mode (it has a `react-router.config.ts`) or is still on Remix v2, and one route should list recent Instagram posts.
- **Do not use it when:** a server `loader` of yours already calls the Instagram API with a token that someone renews. Keep it. Library-mode apps built on `createBrowserRouter` without `react-router.config.ts` belong on the [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html) page.
- **What the human does:** for real posts, the account owner connects Instagram to an ElectricBlaze widget at [a.electricblaze.com](https://a.electricblaze.com/) and shares the widget id. For demo data, nothing at all.
- **Next step:** run `npx electricblaze@0.1.2 add instagram-feed --json` in the project root, then return the demo feed from the route `loader` shown below.
## Render mode decides where data loads
Framework mode has three ways to produce HTML, all set in `react-router.config.ts`. Each mode permits a different data function. That function is the only place a secret could hide.
| Mode | Config | Data function | When it runs | Instagram token |
|---|---|---|---|---|
| Server rendering | `ssr: true` (default) | `loader` | Every request, on the server | Allowed, inside a `.server.ts` module |
| Prerender | `prerender: ["/"]` | `loader` | Once, during `react-router build` | Only in CI; posts freeze until the next build |
| SPA mode | `ssr: false` without `prerender` | `clientLoader` | In the browser | Never |
Sources: [React Router: Pre-Rendering](https://reactrouter.com/how-to/pre-rendering), checked 2026-09-18; [React Router: Data Loading](https://reactrouter.com/start/framework/data-loading), checked 2026-09-18.
> a `loader` is permitted on the root route only
>
> React Router docs on `ssr: false` without `prerender`, [Pre-Rendering](https://reactrouter.com/how-to/pre-rendering), checked 2026-09-18
The widget fetches in the browser, so all three modes can use it. The CLI demo feed is a local file that any data function can return. Hand-written Graph API code belongs in a server `loader` and nowhere else.
React Router v8 came out on 2026-06-17. Everything this page uses behaves the same in v7 and v8: `loader`, `clientLoader`, `prerender`, route types from `./+types/...` and `.server` modules. The same release declared Remix v2 end of life ([Remix blog](https://remix.run/blog/react-router-v8), checked 2026-09-18). We built the examples below with react-router 8.4.0 and Vite 8.3 on 2026-09-18: `react-router typegen`, `tsc` and `react-router build` with a prerendered `/` all pass.
## The widget in a route component
Inject `widget.js` from an effect instead of the `Layout` in `root.tsx`. Effects run after hydration, so the loader never adds posts to server-rendered markup that React is still matching against its own tree.
app/components/InstagramWidget.tsx:
```tsx
import { useEffect } from "react";
const LOADER = "https://s.electricblaze.com/widget.js";
export function InstagramWidget({ widgetId }: { widgetId: string }) {
useEffect(() => {
// Runs after hydration, so widget.js never edits markup React is still matching.
if (document.querySelector(`script[src="${LOADER}"]`)) return;
const script = document.createElement("script");
script.src = LOADER;
script.async = true;
document.body.append(script);
}, []);
// The widget id must be the first class.
return ;
}
```
Once loaded, the script stays on the page and watches the DOM. A route reached through `` that renders another `InstagramWidget` mounts without a second script tag, and a duplicate tag would be ignored anyway.
- **Class order matters.** `className="electricblaze-id-abc123 feed"` mounts. `className="feed electricblaze-id-abc123"` stays an empty box, because the loader takes the id from the first class.
- **Prerendered pages keep working.** The `div` is in the static HTML and the posts arrive in the browser, so a page built once still shows current posts.
- **Styling lives in the dashboard.** Columns, gaps, captions and the slider are widget settings. Free covers 5 widgets and 1000 views per month with data refreshed every 24 hours; see [pricing](https://electricblaze.com/pricing.html) for Start.
## Demo data from the CLI in a loader
The default template lists `react-router` in `package.json`, so `add` picks the `data` kit. That template has no `src/` folder. The two files therefore land in `lib/eb/` at the project root, next to `app/`, not inside it.
```sh
npx electricblaze@0.1.2 add instagram-feed --json
```
Output in a project from the default React Router template (`dir`, `next` and the second note removed):
```json
{
"ok": true,
"component": "instagram-feed",
"source": "instagram",
"framework": "data",
"files": [
{ "path": "lib/eb/feed.d.ts", "status": "written" },
{ "path": "lib/eb/demo/instagram.json", "status": "written" }
],
"demo": true,
"notes": [
"react-router detected: no component template for it yet, so only the feed JSON and types are written. Build the component against them (see the skill), or pass --framework=html for the plain HTML kit."
]
}
```
Return the feed from the route `loader`. The component then receives typed `loaderData`, whether the route renders per request or once at build time.
app/routes/home.tsx:
```tsx
import type { Route } from "./+types/home";
import type { Feed } from "../../lib/eb/feed";
import demo from "../../lib/eb/demo/instagram.json";
import { InstagramWidget } from "~/components/InstagramWidget";
export async function loader() {
const feed = demo as Feed;
return { feed: { ...feed, posts: feed.posts.slice(0, 8) } };
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { feed } = loaderData;
return (
{feed.demo &&
);
}
```
react-router.config.ts:
```ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
// Render "/" to static HTML at build time; its loader runs once, during the build.
prerender: ["/"],
} satisfies Config;
```
The template maps `~/` to `app/`, so the JSON outside `app/` needs a relative import; its `tsconfig.json` already enables `resolveJsonModule`. In SPA mode, rename `loader` to `clientLoader` and export a `HydrateFallback` for the first paint. The JSON is bundled either way.
Posts follow [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html). Show a "demo feed" label while `feed.demo` is `true`; only a connected account turns it off, and that arrives with the 0.2 API.
## Your own Graph API token in a .server.ts module
Calling Instagram yourself is a server-rendering job. It brings the Meta paperwork along:
- Only Business and Creator accounts get this API ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18). Personal accounts lost API access when Basic Display shut down on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
- The long-lived token dies after 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). A refresh works only while the token is at least 24 hours old and not yet expired ([Meta: refresh_access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token), checked 2026-09-18). Details: [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
- `media_product_type`, the field that separates reels from other videos, comes back only when requested by name ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18).
app/lib/instagram.server.ts:
```ts
// .server.ts: the build fails if client code imports this file, so the token stays on the server.
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export type IgMedia = {
id: string;
caption?: string;
media_type: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
media_product_type?: "FEED" | "REELS" | "STORY" | "AD";
media_url?: string;
thumbnail_url?: string;
permalink: string;
timestamp: string;
};
export async function fetchInstagramMedia(limit = 12): Promise {
const token = process.env.IG_ACCESS_TOKEN;
if (!token) throw new Error("IG_ACCESS_TOKEN is not set");
const url = new URL("https://graph.instagram.com/me/media");
url.searchParams.set("fields", FIELDS);
url.searchParams.set("limit", String(limit));
url.searchParams.set("access_token", token);
const res = await fetch(url);
if (!res.ok) throw new Error(`Instagram API ${res.status}: ${await res.text()}`);
return (await res.json()).data;
}
```
app/routes/instagram.tsx:
```tsx
import type { Route } from "./+types/instagram";
import { fetchInstagramMedia } from "~/lib/instagram.server";
export async function loader() {
return { media: await fetchInstagramMedia(12) };
}
// Short cache: media_url links from Instagram expire.
export function headers() {
return { "Cache-Control": "public, max-age=600" };
}
export default function Instagram({ loaderData }: Route.ComponentProps) {
return (
{loaderData.media.map((m) => (
))}
);
}
```
The `.server` suffix is the guard. React Router refuses to build when a `.server` module ends up in the client module graph, so a stray import cannot ship the token. Never read the token inside `clientLoader`: that function is part of the browser bundle.
Leave this route out of `prerender`. A prerendered page freezes the `media_url` values from build day, and Instagram CDN links stop working after a while. If the page must be static, download the images during the build, as the [static site workflow](https://electricblaze.com/developer/instagram-feed/static-site.html#own-code) does. The `headers` export only works with a runtime server; the docs forbid it under `ssr: false`.
## Remix v2
The CLI detects `@remix-run/react` and writes the same `data` kit. Remix v2 no longer gets security updates since the React Router v8 release, and the upgrade path runs through React Router v7. Until then, the widget component above works unchanged, and the demo loader reads like this:
app/routes/_index.tsx:
```tsx
import { useLoaderData } from "@remix-run/react";
import type { Feed } from "../../lib/eb/feed";
import demo from "../../lib/eb/demo/instagram.json";
export async function loader() {
return { feed: demo as Feed };
}
export default function Index() {
const { feed } = useLoaderData();
return
{feed.posts.length} posts from @{feed.origin.name}
;
}
```
## Failure modes
- **Build error about a route `loader` with `ssr: false`.** In SPA mode only the root route and prerendered paths may export `loader`. Switch to `clientLoader` or add the path to `prerender`.
- **Broken thumbnails some time after deploy.** A prerendered route kept Instagram links from build day. Rebuild on a schedule, download the files at build time, or use the widget.
- **The token shows up in the browser bundle.** It was named with a `VITE_` prefix or read in client code. Keep `IG_ACCESS_TOKEN` in the server environment of the host and read it only from a `.server.ts` file.
- **The widget container stays empty after navigation.** Something put another class in front of `electricblaze-id-`.
## FAQ
### Should an Instagram feed use loader or clientLoader in React Router?
`loader` when the route renders on the server or at build time; `clientLoader` in SPA mode. An access token only ever belongs in a server `loader`.
### Can I prerender a React Router page with an Instagram feed?
Yes, with the widget, which loads posts in the browser, and with the demo feed. With your own API code the image links in the prerendered HTML expire, so download the images during the build.
### Does the CLI generate a React Router component?
No. Version 0.1.2 writes `lib/eb/feed.d.ts` and `lib/eb/demo/instagram.json`; the route module on this page renders them.
### Does this code change in React Router v8?
No. Loaders, `clientLoader`, `prerender` and `.server` modules work the same in v7 and v8. v8 needs Node 22.22 or newer and React 19.2.7 or newer ([upgrade guide](https://reactrouter.com/upgrading/v7), checked 2026-09-18).
### Does it work in Remix v2?
Yes: the same loader with `useLoaderData` from `@remix-run/react`. Plan the move to React Router, because Remix v2 is past end of life.
## Related pages
- [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Static HTML site](https://electricblaze.com/developer/instagram-feed/static-site.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/react-router.html
---
# Instagram feed on a static HTML site: shared hosting, GitHub Pages, Netlify Drop
**Status, checked 2026-09-18:** the ElectricBlaze widget shows real posts on any static page today: paste two lines of HTML and upload. `npx electricblaze add instagram-feed` writes a plain HTML kit with demo posts; real data through the JSON API ships in 0.2.
> A static site has no server to hide a token on. Three setups still work in September 2026: an embed, a local kit with demo posts, and a scheduled job that commits JSON and images to the repository.
Canonical: https://electricblaze.com/developer/instagram-feed/static-site.html
## At a glance
- **Use it when:** the site is hand-written HTML, CSS and JavaScript with no build step, served from shared hosting, GitHub Pages or a Netlify Drop folder, and it needs a strip of recent Instagram posts.
- **Do not use it when:** the site lives in a GitHub repository and someone will own a Meta app and renew its token every 60 days. The scheduled Action at the end of this page then shows real posts without any third-party service.
- **What the human does:** the owner creates an Instagram Feed widget at [a.electricblaze.com](https://a.electricblaze.com/), connects the account and copies the embed code. For the GitHub Action route they create a Meta app and a token instead.
- **Next step:** paste the embed code where the grid belongs and upload the page. For a local demo first, run `npx electricblaze@0.1.2 add instagram-feed --json` inside the site folder.
## Three setups compared
Every setup below keeps the Instagram token out of the page source. A static site cannot bend that rule: a token inside a `
```
Put the `div` where the posts should appear. The `script` can follow it directly or sit before ``. One copy of the script serves every widget on the page.
- **Shared hosting (cPanel, FTP).** Edit the HTML file and upload it over the old one. The server needs no configuration, because the browser fetches the script from `s.electricblaze.com`.
- **GitHub Pages.** Commit the edited HTML to the publishing branch; the next Pages build serves it.
- **Netlify Drop.** Deploy the updated folder the same way the first version went up.
The loader looks for an element whose first class starts with `electricblaze-id-`. A site template that prepends its own class, as in `class="block electricblaze-id-..."`, gets an empty box. The loader keeps observing the page after load, so a container that a tab or accordion script inserts later still mounts.
Posts are drawn by JavaScript in the visitor's browser. They are not in the HTML file, so crawlers that skip scripts do not see captions.
The Free plan includes 5 widgets on unlimited websites and 1000 views a month, with an ElectricBlaze credit link under the widget. Start drops the credit for $5 per month, billed yearly ([pricing](https://electricblaze.com/pricing.html)).
## The CLI kit: demo posts, no account
Run the CLI inside the site folder. With no `package.json` there, it writes the plain HTML kit. In a folder that has one, `--framework=html` forces the same kit.
```sh
cd my-site
npx electricblaze@0.1.2 add instagram-feed --json
```
Output in a folder without package.json (`dir` and `notes` removed):
```json
{
"ok": true,
"component": "instagram-feed",
"source": "instagram",
"framework": "html",
"files": [
{ "path": "electricblaze/instagram-feed.js", "status": "written" },
{ "path": "electricblaze/instagram-feed.css", "status": "written" },
{ "path": "electricblaze/instagram.json", "status": "written" },
{ "path": "electricblaze/instagram.demo.js", "status": "written" },
{ "path": "electricblaze/feed.d.ts", "status": "written" }
],
"demo": true,
"snippet": [
"",
"",
"",
""
],
"next": "To show real posts today, connect the account in the ElectricBlaze widget and paste its embed snippet (https://electricblaze.com). The JSON API for this component (npx electricblaze connect instagram) ships in 0.2. Until then this is a demo feed."
}
```
Paste the four `snippet` lines into the page and upload the `electricblaze/` folder next to it. Node runs on your computer only; the host serves plain files. The container accepts four attributes:
- `data-limit="8"` sets how many posts to show.
- `data-formats="reel"` keeps reels only; `post` keeps everything else.
- `data-header="false"` hides the avatar and handle row.
- `data-src="feed.json"` reads a feed from a URL instead of the demo script. The file must follow [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html).
While `feed.demo` is `true`, the kit prints a small "demo feed" tag. Leave it in place until the posts are real.
## Your own code: a scheduled GitHub Action
A static site can show real posts without a hosted service if Instagram is called from CI instead of from the browser. GitHub Actions runs a script on a timer. The token sits in repository secrets. The script writes a JSON file and the images, and the job commits them next to the pages.
Three things come first:
1. A Business or Creator account connected to a Meta app. The Instagram API does not serve personal accounts ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18).
2. A long-lived token saved as the repository secret `IG_ACCESS_TOKEN`. It lasts 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). The job cannot store a refreshed token by itself, since `GITHUB_TOKEN` has no permission for secrets. Renew it by hand before day 60, or give a separate job a fine-grained token that may run `gh secret set`. See [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
3. Files instead of links. `media_url` and `thumbnail_url` point at Instagram's CDN and expire, so the script downloads every image into the repository. Background: [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html).
.github/workflows/instagram.yml:
```yaml
name: Instagram feed
on:
schedule:
- cron: "23 */6 * * *" # UTC, every 6 hours, away from the top of the hour
workflow_dispatch:
permissions:
contents: write
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: node scripts/instagram.mjs
env:
IG_ACCESS_TOKEN: ${{ secrets.IG_ACCESS_TOKEN }}
- run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add instagram
git diff --staged --quiet || git commit -m "Refresh Instagram feed"
git push
```
scripts/instagram.mjs:
```js
// Runs in GitHub Actions only. The token never reaches the site's files.
import { existsSync } from "node:fs";
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
const OUT = "instagram";
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
const api = new URL("https://graph.instagram.com/me/media");
api.searchParams.set("fields", FIELDS);
api.searchParams.set("limit", "12");
api.searchParams.set("access_token", process.env.IG_ACCESS_TOKEN ?? "");
const res = await fetch(api);
if (!res.ok) throw new Error(`Instagram API ${res.status}: ${await res.text()}`);
const { data } = await res.json();
await mkdir(OUT, { recursive: true });
const posts = [];
for (const m of data) {
// A video's still image is thumbnail_url; skip anything without an image URL.
const src = m.media_type === "VIDEO" ? m.thumbnail_url : m.media_url;
if (!src) continue;
const file = `${OUT}/${m.id}.jpg`;
if (!existsSync(file)) {
const img = await fetch(src);
if (!img.ok) continue;
await writeFile(file, Buffer.from(await img.arrayBuffer()));
}
posts.push({
id: m.id,
url: m.permalink,
image: file,
caption: m.caption ?? "",
reel: m.media_product_type === "REELS",
publishedAt: m.timestamp,
});
}
// Delete images of posts that dropped out of the latest 12.
const keep = new Set(posts.map((p) => `${p.id}.jpg`));
for (const name of await readdir(OUT)) {
if (name.endsWith(".jpg") && !keep.has(name)) await rm(`${OUT}/${name}`);
}
// No timestamp here: a run with no new posts leaves the files unchanged.
await writeFile(`${OUT}/feed.json`, JSON.stringify({ posts }, null, 2) + "\n");
```
We ran the script against a stubbed API on 2026-09-18. A second run with the same posts left every file byte for byte identical, so the commit step had nothing to commit. Images already on disk are not downloaded twice, and images of old posts are deleted.
`instagram/feed.json` as the script writes it:
```json
{
"posts": [
{
"id": "17912345678901234",
"url": "https://www.instagram.com/reel/EXAMPLE/",
"image": "instagram/17912345678901234.jpg",
"caption": "How we cup a new lot in under 60 seconds.",
"reel": true,
"publishedAt": "2026-09-17T08:30:00+0000"
}
]
}
```
index.html:
```html
```
The page builds DOM nodes rather than an HTML string, so a caption can never inject markup. To reuse the CLI kit instead, write the file in schema v1 and point `data-src` at it.
### Schedule rules and deploys
- The `cron` line runs in UTC, and five minutes is the shortest interval GitHub accepts. Runs can be delayed under load, most often at the start of the hour, hence minute 23.
- The job needs `permissions: contents: write` to push its commit.
- Schedules run on the latest commit of the default branch. In a public repository GitHub switches them off after 60 days without repository activity, and a quiet account commits nothing, so check the Actions tab now and then ([GitHub Docs: schedule](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule), checked 2026-09-18).
- Hosts that build from the repository through their own Git integration pick up the push like any other. Netlify Drop has no repository, so this route needs a Git-connected site.
GitHub Pages is the exception. Per the Pages troubleshooting docs, commits pushed with `GITHUB_TOKEN` "do not trigger a GitHub Pages build" ([GitHub Docs](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site), checked 2026-09-18). Set the Pages source to GitHub Actions and deploy from the same job:
.github/workflows/instagram.yml, GitHub Pages version of the job:
```yaml
permissions:
contents: write
pages: write
id-token: write
jobs:
refresh:
runs-on: ubuntu-latest
environment:
name: github-pages
steps:
# checkout, setup-node, the script and the commit step from above, then:
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: .
- uses: actions/deploy-pages@v5
```
`upload-pages-artifact` leaves out `.git`, `.github` and other dotfiles by default, and the token never touches the uploaded folder.
## FAQ
### Can I put my Instagram access token in the JavaScript of a static site?
No. Anyone can copy it from View Source or the network panel and call the API as the account until it expires. Keep it with ElectricBlaze (widget) or in GitHub repository secrets (Action).
### Does the ElectricBlaze widget work on GitHub Pages?
Yes. It is a `div` and a `script` tag; the page stays static, and the posts load in the visitor's browser.
### Do I need Node.js on the web server?
No. The CLI runs once on your computer and writes plain files. The widget needs only its two lines of HTML.
### How often does the GitHub Action route update the feed?
As often as the `cron` line says, never more than once every 5 minutes. The example runs every 6 hours and commits only when posts change.
### Will search engines index the Instagram posts?
Not with the widget or the small renderer above; both insert posts with JavaScript. To get captions into the HTML, let the Action write the markup into the page instead of a JSON file.
## Related pages
- [CLI reference](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/static-site.html
---
# Instagram feed in SvelteKit: widget, server load and prerendering
**Status, checked 2026-09-18:** the ElectricBlaze widget is how a SvelteKit app shows real Instagram posts today. In SvelteKit projects `npx electricblaze add instagram-feed` writes demo JSON and types only; real data for them waits for the 0.2 JSON API.
> A SvelteKit 2 and Svelte 5 guide to Instagram posts: where the widget script belongs, how to read the demo feed in a load function, and which environment module works when a route is prerendered.
Canonical: https://electricblaze.com/developer/instagram-feed/sveltekit.html
## At a glance
- **Use it when:** the app runs SvelteKit 2 with Svelte 5 runes, on any adapter including `adapter-static`, and a route or layout should show recent Instagram posts.
- **Do not use it when:** reels must look different from ordinary videos with real data today. The widget renderer does not read `media_product_type`, so it shows a reel as a plain video; request that field in your own Graph API code instead.
- **What the human does:** the account owner signs in at [a.electricblaze.com](https://a.electricblaze.com/), sets up an Instagram Feed widget for their account and shares its id with you. That is the whole manual part.
- **Next step:** with a widget id, add the Svelte component below to a route. Without one, run `npx electricblaze@0.1.2 add instagram-feed --json` and render the demo feed from a load function.
## Choosing an approach
SvelteKit can produce a feed at build time, on each request, or hand it to the browser. The approaches below map onto those modes.
| Approach | Data | Rendered by | Owner's task | Limitation |
|---|---|---|---|---|
| Widget in `` | Real posts now | The loader script, in the browser | Share a widget id | Reels appear as plain videos; the Free plan counts 1000 views a month ([pricing](https://electricblaze.com/pricing.html)) |
| Demo feed in `+page.server.ts` | Twelve sample posts | Server or prerender | None | Real data needs the 0.2 API |
| Own Graph API call | Real posts now | Server or prerender | Meta app, token, refresh job | Token expiry after 60 days, expiring image links |
If no one on the project will own a Meta app, take the widget. If you are designing the section before the owner signs up, begin with the demo feed. Write your own call when you need Instagram fields the widget leaves out.
## The widget in a Svelte component
The dashboard gives the owner an embed like this one:
```html
```
In SvelteKit, split it in two. The script tag goes into ``; the container stays in the markup.
src/lib/components/InstagramWidget.svelte:
```svelte
```
Render `` in any `+page.svelte` or `+layout.svelte`. To load the script on every route instead, put the same `
{#if data.feed.demo}
```
The load function runs on the server, and `export const prerender = true` turns the route into a static file at build time. `let { data }: PageProps = $props()` is the Svelte 5 form; `PageProps` exists since SvelteKit 2.16.
Posts follow [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html). Show `thumbnailUrl`, link to `url`, badge the `video` and `carousel` types, and keep the "demo feed" label while `data.feed.demo` is `true`. There is no key to set yet: `npx electricblaze connect instagram` answers `not_available_yet` with exit code 2 until 0.2.
## Own Graph API code and environment modules
You can skip ElectricBlaze and call Instagram from a server load. The owner then needs a Meta app and a professional account ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18). Access for personal accounts through Basic Display ended on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
The token is the hard part. A long-lived one is valid for 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18), and some job has to refresh it; see [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html). Keep the request in `src/lib/server/`, a folder SvelteKit never lets client code import.
src/lib/server/instagram.ts:
```ts
import { IG_TOKEN } from "$env/static/private";
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export async function fetchInstagram(fetch: typeof globalThis.fetch, limit = 9) {
const url = new URL("https://graph.instagram.com/me/media");
url.searchParams.set("fields", FIELDS);
url.searchParams.set("limit", String(limit));
url.searchParams.set("access_token", IG_TOKEN);
const res = await fetch(url);
if (!res.ok) throw new Error(`Instagram API ${res.status}`);
return (await res.json()).data as Array>;
}
```
Call it from `+page.server.ts` as `await fetchInstagram(fetch)`. Which environment module to import depends on how that route renders:
- **Prerendered route.** Use `$env/static/private`. SvelteKit 2 refuses to read dynamic variables during prerendering ([SvelteKit 2 migration guide](https://svelte.dev/docs/kit/migrating-to-sveltekit-2), read 2026-09-18). Posts freeze at build time, so schedule rebuilds.
- **Route rendered per request.** Use `$env/dynamic/private` and read `env.IG_TOKEN`. Static values are written into the bundle at build time, so only the dynamic module picks up a refreshed token without a rebuild.
On a per-request route, add `setHeaders({ "cache-control": "public, max-age=600" })` in the load function so repeat visits can come from a cache instead of Instagram.
Image links from `media_url` point at Instagram's CDN and expire, which leaves a prerendered page with broken images after a while. [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) covers why and what to do.
## SvelteKit problems and fixes
- **Build error on `$env/static/private`.** A `.svelte` file or a universal `+page.ts` imported it. Move the code into `+page.server.ts` or `src/lib/server/`. That error is the guard that keeps the token out of the browser.
- **Prerendering fails on `$env/dynamic/private`.** Switch that route to the static module, or stop prerendering it.
- **The widget renders nothing.** Either the first class is not `electricblaze-id-...`, or `kit.csp` in `svelte.config.js` blocks it. Allow `https://s.electricblaze.com` under `script-src`, plus `https://api.electricblaze.com` and `https://proxy.electricblaze.com` under `connect-src`.
- **Content jumps when the grid appears.** Reserve height for `.electricblaze-state-loading` with `:global(...)` or a global stylesheet. Svelte drops scoped selectors for classes it never renders.
This page targets SvelteKit 2, the stable line on 2026-09-18. SvelteKit 3 is a [release candidate](https://svelte.dev/blog/sveltekit-3-release-candidate) that renames `$lib` to `#lib` and moves environment variables into `src/env.ts`, so adjust imports if you run it.
## FAQ
### Should the widget script go in svelte:head or app.html?
Either works. `` loads it together with the component; `src/app.html` loads it on every page. The loader ignores a second copy, so having both does no harm.
### Does the widget survive client-side navigation in SvelteKit?
Yes. The loader observes the document, so a container that appears after a route change gets mounted and one that disappears gets released.
### Can I use $env/dynamic/private in a prerendered SvelteKit route?
No. SvelteKit 2 blocks dynamic environment variables while prerendering. Use `$env/static/private` there and rebuild when the token changes.
### Does npx electricblaze add instagram-feed create a Svelte component?
No. In a SvelteKit project it writes `src/lib/eb/feed.d.ts` and `src/lib/eb/demo/instagram.json`. The grid is yours to write, about twenty lines.
### Does the Instagram feed work with adapter-static?
Yes. The widget needs no server, and a prerendered load function reads the demo JSON during the build.
## Related pages
- [Nuxt 4](https://electricblaze.com/developer/instagram-feed/nuxt.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [React and Vite](https://electricblaze.com/developer/instagram-feed/react.html)
- [Token lifetime and refresh](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Reels and carousel fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/sveltekit.html
---
# Instagram feed in Nuxt 4: widget, server route and route rules
**Status, checked 2026-09-18:** in Nuxt, real Instagram posts come from the ElectricBlaze widget today. `npx electricblaze add instagram-feed` gives a Nuxt project sample data plus types, not a Vue component; real data through the JSON API arrives in 0.2.
> What goes where in a Nuxt 4 project with the `app/` directory: the widget script, the demo feed, a private Instagram token, and the cache rules that decide how fresh the posts are.
Canonical: https://electricblaze.com/developer/instagram-feed/nuxt.html
## At a glance
- **Use it when:** the project runs Nuxt 4 (source in `app/`) with server rendering, `nuxt generate` or a mix of both, and a page needs recent Instagram posts.
- **Do not use it when:** the grid must be server-rendered Vue markup with real posts right now. The widget draws on the client and the CLI data is a demo, so that case needs your own server route and a Meta app.
- **What the human does:** the account owner opens [a.electricblaze.com](https://a.electricblaze.com/), builds an Instagram Feed widget, links their Instagram account to it and passes the widget id to the developer. No Meta app or token changes hands.
- **Next step:** with a widget id, copy the component below. Without one, run `npx electricblaze@0.1.2 add instagram-feed --json` and serve the demo feed from `server/api/`.
## Where each option runs
A Nuxt page can get posts from the browser, from a Nitro server route, or from a file read at build time. The table matches the three options to those places.
| Option | Real posts | Runs in | Owner provides | Watch out for |
|---|---|---|---|---|
| Widget via `useHead` | Yes, today | The browser | A widget id | Needs ``; Free plan: 5 widgets, 1000 views a month ([pricing](https://electricblaze.com/pricing.html)) |
| Demo JSON from the CLI | No, demo until 0.2 | Nitro or the build | Nothing | Files land in `lib/eb/`, outside `app/` |
| Server route on the Graph API | Yes, today | Nitro | Meta app, token, refresh | Token lifetime and expiring image links |
Start with the widget unless you have a reason not to. It is the only option with real posts that asks nothing technical of the owner. The server route suits teams that already operate a Meta app.
## Widget component with useHead and ClientOnly
This is the embed code the dashboard hands out:
```html
```
In Nuxt, the script moves into `useHead` and the container into a component template.
app/components/InstagramWidget.vue:
```vue
```
Use it in a page as ``. Nuxt auto-imports everything in `app/components/`, so no import line is needed.
### Why the container sits in `ClientOnly`
The loader adds electricblaze-state-loading, -mounting, -mounted or -error to the container, then fills it with the grid. If that happens before Vue hydrates the page, Vue meets markup it did not render and can report a hydration mismatch. `` creates the container only after hydration. The `#fallback` slot holds its place in the server HTML; give `.ig-placeholder` a height to avoid a jump.
The loader still finds the late container, because it keeps a MutationObserver on the whole document. The same observer handles `` navigation: a container on the next page mounts without a reload.
To load the script on every page, list it under `app.head.script` in `nuxt.config.ts` instead. Loading it twice is harmless; the second copy stops at its `window.electricblaze` check.
Gallery or slider, columns, rows and captions are configured in the dashboard. The Vue component has no props for them.
## Demo feed through server/api/ and useAsyncData
The CLI has no Vue template yet. When it finds `nuxt` in `package.json`, it writes the feed JSON and its types and nothing more.
The files land in `lib/eb/` at the project root, not in `app/`. The CLI only checks for a `src/` directory, and Nuxt 4 keeps its source in `app/`. In Nuxt 4, `~` points at `app/` and `~~` at the root, so app code imports them as `~~/lib/eb/...`.
```sh
npx electricblaze@0.1.2 add instagram-feed --json
```
Result from 0.1.2 in a Nuxt 4 project (`dir` and notes trimmed):
```json
{
"ok": true,
"framework": "data",
"files": [
{
"path": "lib/eb/feed.d.ts",
"status": "written"
},
{
"path": "lib/eb/demo/instagram.json",
"status": "written"
}
],
"demo": true,
"next": "To show real posts today, connect the account in the ElectricBlaze widget and paste its embed snippet (https://electricblaze.com). The JSON API for this component (npx electricblaze connect instagram) ships in 0.2. Until then this is a demo feed."
}
```
Serve the file from a server route. The page fetches `/api/instagram` and never learns where the data came from, so the source can change later without touching Vue code.
server/api/instagram.get.ts:
```ts
import type { Feed } from "../../lib/eb/feed";
import demo from "../../lib/eb/demo/instagram.json";
export default defineEventHandler(() => demo as Feed);
```
app/pages/index.vue:
```vue
demo feed
{{ post.type }}
```
`useAsyncData` runs the request on the server while rendering and ships the result to the browser in the payload, so the grid is not fetched twice. Nitro types `$fetch("/api/instagram")` from the route, so `feed` arrives as a typed [schema v1](https://electricblaze.com/developer/instagram-feed/schema.html) object.
Leave the "demo feed" tag visible as long as `feed.demo` stays `true`. The command meant for real data answers like this today, with exit code 2:
`npx electricblaze connect instagram --json` on 2026-09-18:
```json
{
"ok": false,
"command": "connect",
"status": "not_available_yet",
"message": "\"connect\" needs the ElectricBlaze API and ships in 0.2. Everything else works in demo mode today.",
"next": "Keep the demo feed for now; read https://electricblaze.com for the API status."
}
```
## Server route with a private token
Writing the Instagram call yourself fits Nuxt well: the token stays in Nitro and the page keeps its `useAsyncData` call. The owner's side is heavier:
- a professional Instagram account, because the API with Instagram Login rejects personal ones ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18);
- a Meta app issuing a long-lived token, good for 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18), which a job you run must refresh;
- no fallback to Basic Display, which reached end of life on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
Declare the token in `runtimeConfig` outside `public`, then set `NUXT_IG_TOKEN` in the host's environment. Nuxt maps a `NUXT_` variable only when its key is declared in `nuxt.config.ts`.
nuxt.config.ts:
```ts
export default defineNuxtConfig({
runtimeConfig: {
igToken: "", // filled from NUXT_IG_TOKEN at runtime; server only
},
routeRules: {
"/api/instagram": { swr: 3600 },
"/": { isr: 3600 }, // Netlify and Vercel; use prerender: true for a static build
},
});
```
server/api/instagram.get.ts:
```ts
const FIELDS = "id,caption,media_type,media_product_type,media_url,thumbnail_url,permalink,timestamp";
export default defineEventHandler(async (event) => {
const { igToken } = useRuntimeConfig(event);
const res = await $fetch<{ data: Record[] }>("https://graph.instagram.com/me/media", {
query: { fields: FIELDS, limit: 9, access_token: igToken },
});
return res.data;
});
```
`useRuntimeConfig(event)` reads the value when the request arrives, so a refreshed token applies without a rebuild. Anything under `runtimeConfig.public` is sent to every browser; the token must never go there.
`$fetch` throws when Instagram returns an error status, and Nitro turns that into a 500 from `/api/instagram`. Show a fallback in the page through the `error` ref of `useAsyncData`.
Return schema v1 from the route and the demo template above keeps working: map `permalink` to `url`, `caption` to `text`, and `thumbnail_url` or `media_url` to `thumbnailUrl`.
### Caching with `routeRules`
- `swr: 3600` on `/api/instagram` keeps the route output in the server cache for an hour and renews it in the background, so Instagram is called roughly once an hour per server cache rather than once per visitor.
- `isr: 3600` on the page lets the CDN hold the HTML. The [Nuxt rendering docs](https://nuxt.com/docs/4.x/guide/concepts/rendering) name Netlify and Vercel as the platforms for it (read 2026-09-18).
- `prerender: true`, or `nuxt generate` for the whole site, renders at build time. The route runs once, and the posts stay as they were until the next build.
Whatever you cache, the `media_url` links inside expire on Instagram's side, so an old cache or build ends up with broken images. [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) explains the lifetime problem.
## Nuxt-specific problems
- **Import of `~/lib/eb/feed` fails.** In Nuxt 4 `~` means `app/`. Use `~~/lib/eb/feed` or a relative path.
- **`NUXT_IG_TOKEN` is set but `igToken` is empty.** The key is missing from `runtimeConfig` in `nuxt.config.ts`, and Nuxt ignores variables it has no key for.
- **Hydration warning next to the widget.** The container is rendered on the server. Wrap it in ``.
- **Posts never change on a generated site.** `nuxt generate` called the API route once. Deploy with a server and `swr`, or use the widget, which fetches in the browser.
- **Token expired after two months.** The long-lived token was never refreshed. [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html) lists the symptoms.
## FAQ
### Why does the CLI write to lib/eb/ and not to app/ in Nuxt 4?
CLI 0.1.2 uses `src/` only when that directory exists. Nuxt 4 keeps source in `app/`, so the files go to the project root. Import them with the `~~` alias.
### Does the widget need ClientOnly in Nuxt?
It is the safe choice. The loader edits the container, possibly before Vue hydrates it; creating the container on the client keeps that edit out of hydration.
### Does the widget keep working after NuxtLink navigation?
Yes. The loader watches the document for new containers, so the widget mounts on the next page without a full reload.
### How do I keep the Instagram token out of the browser in Nuxt?
Put it in private `runtimeConfig`, set it through `NUXT_IG_TOKEN`, and read it only under `server/` with `useRuntimeConfig(event)`.
### Can I use nuxt generate for an Instagram feed?
Yes. The widget works on a generated site because it runs in the browser. A server route runs once at build time, so its posts stay fixed until you generate again.
## Related pages
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [SvelteKit](https://electricblaze.com/developer/instagram-feed/sveltekit.html)
- [React Router](https://electricblaze.com/developer/instagram-feed/react-router.html)
- [Access token lifetime](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Fields for reels](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Instagram API without Basic Display](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/nuxt.html
---
# Instagram long-lived access token: exchange, refresh, schedule and store it
**Status, checked 2026-09-18:** a short-lived Instagram User token expires in 1 hour and a long-lived one lasts 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18); `refresh_access_token` returns a new 60-day token only while the current one is at least 24 hours old and unexpired. The ElectricBlaze widget puts no Instagram token in the site's code.
> Instagram Login hands your server a token that dies within the hour. Here is how it becomes a 60-day token, how `ig_refresh_token` keeps it alive, and what to do on the day it lapses.
Canonical: https://electricblaze.com/developer/instagram-feed/access-token.html
## At a glance
- **Use it when:** your server, build step or CI job calls `graph.instagram.com` with a token for a Business or Creator account, and the feed has to survive past day 60.
- **Do not use it when:** the site only displays posts and nobody wants to own a Meta app, a secret store and a scheduled job. The widget described at the end of this page needs none of the three.
- **What the human does:** the account owner logs in once through Instagram Login and grants `instagram_business_basic`. Nobody touches the token again unless a refresh is missed; then the owner logs in a second time.
- **Next step:** copy `lib/instagram-token.mjs`, run the exchange once on your server, and schedule `scripts/refresh-instagram-token.mjs` for the 1st of every month.
## The token lifecycle
A token passes through four stages. Every call that moves it forward runs on a server or in CI, never in a browser.
| Stage | How you get it | Valid for | Where it runs |
|---|---|---|---|
| Short-lived token | Instagram Login, after the owner approves the app; your OAuth callback obtains it on the server | 1 hour | Server route that handles the login redirect |
| Long-lived token | `GET /access_token` with `grant_type=ig_exchange_token` and the app secret | 60 days | Server only: the URL carries `client_secret` |
| Refreshed token | `GET /refresh_access_token` with `grant_type=ig_refresh_token` | 60 days, counted from the refresh | Server, cron or CI, once the token is 24 hours old |
| Expired token | No call revives it | Nothing | The owner repeats Instagram Login |
Sources: [Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18 (lifetimes, exchange); [Meta: refresh_access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token), checked 2026-09-18 (refresh conditions).
Both token endpoints live on `graph.instagram.com` without a version segment ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18). Data calls such as `/me/media` do take one: Meta's Instagram examples use `v25.0`, while the newest Graph API release is v26.0 from 2026-07-29 ([Meta: Graph API changelog](https://developers.facebook.com/docs/graph-api/changelog), checked 2026-09-18). Pin the version you tested instead of chasing the latest.
## Step 1: exchange the short-lived token
Exchange right after login: the short-lived token is gone 1 hour after it was issued, and the exchange URL carries the app secret, so the call belongs in a server route, a serverless function or a one-off script ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18).
lib/instagram-token.mjs:
```js
// Server only: this module reads the app secret. Node 18+ (global fetch).
const GRAPH = "https://graph.instagram.com";
async function tokenCall(path, params) {
const url = new URL(path, GRAPH);
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
const res = await fetch(url); // never log `url`: it contains the token
const body = await res.json();
if (!res.ok || !body.access_token) {
const e = body.error ?? {};
throw new Error(`${path} failed: HTTP ${res.status}, code ${e.code}, subcode ${e.error_subcode}: ${e.message}`);
}
// expires_in is in seconds; store the absolute date next to the token.
return { token: body.access_token, expiresAt: new Date(Date.now() + body.expires_in * 1000) };
}
// Short-lived (1 hour) -> long-lived (60 days). Run once, right after login.
export function exchangeToken(shortLivedToken) {
return tokenCall("/access_token", {
grant_type: "ig_exchange_token",
client_secret: process.env.INSTAGRAM_APP_SECRET,
access_token: shortLivedToken,
});
}
// Long-lived -> a new token valid for 60 days from now.
// Meta accepts it only if the token is at least 24 hours old and not expired.
export function refreshToken(longLivedToken) {
return tokenCall("/refresh_access_token", {
grant_type: "ig_refresh_token",
access_token: longLivedToken,
});
}
```
Keep `expiresAt` in the same record as the token, so a health check can warn two weeks before the deadline.
## Step 2: refresh before day 60
A refresh trades the current long-lived token for one that is valid for another 60 days, counted from the moment of the call. Meta accepts it under three conditions ([Meta: refresh_access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token), checked 2026-09-18):
- The token is at least 24 hours old. A job that fires minutes after the exchange gets an error.
- The token has not expired. Past day 60 there is nothing left to refresh.
- The owner granted `instagram_business_basic` at login. Authorize URLs that still ask for the retired value `business_basic`, deprecated on 2025-01-27, need updating ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18).
Response from `refresh_access_token` (example values):
```json
{
"access_token": "EXAMPLE_LONG_LIVED_TOKEN",
"token_type": "bearer",
"expires_in": 5183944
}
```
`expires_in` counts seconds: 5,183,944 seconds is just under 60 days. Save the returned `access_token` as the current value even if it looks identical to the old one, and overwrite the stored expiry.
scripts/refresh-instagram-token.mjs:
```js
// Prints ONLY the new token on stdout, so a scheduler can pipe it into a secret store.
// Messages for humans go to stderr and never contain the token.
import { refreshToken } from "../lib/instagram-token.mjs";
const current = process.env.IG_ACCESS_TOKEN;
if (!current) {
console.error("IG_ACCESS_TOKEN is not set");
process.exit(1);
}
const { token, expiresAt } = await refreshToken(current); // throws -> exit code 1
process.stdout.write(token);
console.error(`Refreshed. New token expires around ${expiresAt.toISOString()}.`);
```
## Step 3: put the refresh on a schedule
Refresh every 30 to 50 days. A run on the 1st of each month keeps the gap at 31 days or less, which leaves about four weeks to notice a failed run and fix it before the token lapses. Refreshing more often is allowed after the first 24 hours, but it only adds runs that can fail.
### GitHub Actions
This fits a static site that builds in Actions and reads the token from a repository secret at build time. The workflow refreshes the token and writes it back into the same secret.
.github/workflows/refresh-instagram-token.yml:
```yaml
name: Refresh Instagram token
on:
schedule:
- cron: "23 5 1 * *" # 05:23 UTC on the 1st of each month
workflow_dispatch: # manual run, useful for the first test
permissions:
contents: read
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- name: Refresh the token and store it as a repository secret
env:
IG_ACCESS_TOKEN: ${{ secrets.IG_ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.SECRETS_WRITE_PAT }}
run: |
node scripts/refresh-instagram-token.mjs > "$RUNNER_TEMP/ig-token"
gh secret set IG_ACCESS_TOKEN < "$RUNNER_TEMP/ig-token"
```
- The built-in `GITHUB_TOKEN` cannot write repository secrets. Create a fine-grained personal access token for this repository with the **Secrets** permission set to read and write, and save it as `SECRETS_WRITE_PAT`.
- If the script fails, the step stops before `gh secret set`, so a failed refresh never blanks the secret.
- In a public repository GitHub disables scheduled workflows after 60 days without repository activity ([GitHub: events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule), checked 2026-09-18). A quiet repository can lose its refresh job, so check the Actions tab or monitor the stored expiry from somewhere else.
### Vercel Cron
On Vercel an environment variable is fixed when the deployment is built, so the refreshed token needs a writable home: a KV entry or a database row that server code reads at request time. A cron entry calls a route handler once a month.
vercel.json:
```json
{
"crons": [{ "path": "/api/cron/instagram-token", "schedule": "23 5 1 * *" }]
}
```
app/api/cron/instagram-token/route.js:
```js
import { refreshToken } from "@/lib/instagram-token.mjs";
import { readToken, saveToken } from "@/lib/token-store"; // your KV, database or secret manager
export async function GET(request) {
const secret = process.env.CRON_SECRET;
if (!secret || request.headers.get("authorization") !== `Bearer ${secret}`) {
return new Response("Unauthorized", { status: 401 });
}
const { token, expiresAt } = await refreshToken(await readToken());
await saveToken(token, expiresAt);
return Response.json({ ok: true, expiresAt }); // no token in the response
}
```
Vercel sends `Authorization: Bearer` plus the value of `CRON_SECRET` when that variable is set, does not retry a failed invocation, and on the Hobby plan may run the job at any minute inside the scheduled hour ([Vercel: managing cron jobs](https://vercel.com/docs/cron-jobs/manage-cron-jobs), checked 2026-09-18). A monthly schedule is within the limits of every plan.
Any scheduler that runs Node works the same way, from a crontab line to a Cloudflare Workers cron trigger: one refresh a month, plus an alert when it fails.
## Where the token lives
- **Server side only.** An environment variable read by server code, a secret manager or a database row. With `instagram_business_basic` the token reads the profile and its media ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18), so handle it like a password.
- **Never behind a public prefix.** Variables named `NEXT_PUBLIC_*`, `VITE_*` or Astro's `PUBLIC_*` are inlined into the JavaScript every visitor downloads. Call it `IG_ACCESS_TOKEN` and read it only in server code or at build time.
- **Writable by the refresh job.** The store must accept the new value from the job. A variable baked into a deployment only works if the job also rewrites it and triggers a rebuild, as the GitHub workflow above does.
- **Out of logs.** The token travels as a query parameter, so never print request URLs from these calls. Log the expiry date instead, and keep `.env.local` out of git.
A token fixes access, not image links. `media_url` values expire on their own schedule; see [media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html) before caching pages built from them.
## When the token has already expired
Calls with a lapsed token fail with `OAuthException` code 190 and `error_subcode` 463; a changed password gives subcode 460 ([Meta: Graph API error handling](https://developers.facebook.com/docs/graph-api/guides/error-handling), checked 2026-09-18). The refresh endpoint rejects an expired token as well ([Meta: refresh_access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token), checked 2026-09-18), so recovery always goes through the account owner:
1. The owner goes through Instagram Login again and approves the app.
2. Your callback exchanges the new short-lived token within the hour.
3. You store the result and find out why the schedule missed: a disabled workflow, an unretried cron run, a store the job could not write to.
4. You add an alert for "fewer than 14 days left", computed from the stored expiry.
Other 190 subcodes and the errors that look similar are listed on [feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html).
## Skipping the token: the ElectricBlaze widget
When the goal is a grid of posts on a website, the token work can leave the project. With the ElectricBlaze widget the account owner connects Instagram at [a.electricblaze.com](https://a.electricblaze.com/), and the site embeds a snippet with no Meta app and no token behind it:
```html
```
The limits, stated plainly. The widget renders in the browser with layouts chosen in the dashboard, and its feed has no carousel `children` and no `media_product_type`. The Free plan covers 1000 views a month ([pricing](https://electricblaze.com/pricing.html)). A JSON API for server code is planned for 0.2 of the `electricblaze` CLI; until then, code that needs raw fields keeps its own token and follows the steps above.
## FAQ
### How long does an Instagram access token last in 2026?
A short-lived Instagram User token lasts 1 hour. The long-lived token from `ig_exchange_token` lasts 60 days, and every refresh starts a new 60-day period ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18).
### Can I refresh an expired Instagram token?
No. `refresh_access_token` only accepts a token that is at least 24 hours old and still valid. After expiry the account owner has to log in again.
### Does the refresh call need the app secret?
No. The refresh URL carries only `grant_type=ig_refresh_token` and the current token. The app secret appears once, in the exchange, which is why that call has to stay on a server.
### How often should I refresh the token?
Once a month. That keeps the gap well under 60 days and leaves time to repair a failed run. A token younger than 24 hours cannot be refreshed.
### Can I store the token in a NEXT_PUBLIC_ or VITE_ variable?
No. Those prefixes copy the value into the client bundle, where any visitor can read it. Read the token only in server code, a build step or a serverless function.
### Can I show an Instagram feed without managing a token at all?
Yes, with an embedded widget whose provider holds the connection. The ElectricBlaze widget is one option; the site gets a two-line snippet.
## Related pages
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
- [App Review](https://electricblaze.com/developer/instagram-feed/app-review.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Basic Display alternative](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Instagram feed for developers](https://electricblaze.com/developer/instagram-feed/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/access-token.html
---
# Instagram API media fields: what /me/media returns and when
**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.
> Each field you can name in `fields=` on the media edge of the Instagram API with Instagram Login: when it comes back, what it holds, and how to turn it into a portable post object.
Canonical: https://electricblaze.com/developer/instagram-feed/media-fields.html
## At a glance
- **Use it when:** you are writing the `fields=` list for `/me/media` or `//media` on `graph.instagram.com` and 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](https://electricblaze.com/developer/instagram-feed/comparison.html).
- **What the human does:** nothing for the field list itself. The account must already be a Business or Creator account that granted `instagram_business_basic` to your app ([access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)).
- **Next step:** run the request below with your token, check whether `caption` and `media_product_type` come back, then convert each item with `toPost()`.
## Name every field you render
Without `fields=`, the media edge answers with bare ids. The edge is `GET https://graph.instagram.com/v25.0//media`, and an item carries nothing but its `id` until you list more ([Meta: get started with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started), 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](https://developers.facebook.com/docs/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):
```sh
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](https://electricblaze.com/developer/instagram-feed/media-url-expired.html). |
| `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](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18; [Meta: IG Media children](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/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
>
> [Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18
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](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media/children), checked 2026-09-18), which saves one extra request per slide.
Slides have no working `permalink` ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), 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](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18). Skip that post, or show a text card that links to `permalink`. Never render an empty ``.
## 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):
```json
{
"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](https://developers.facebook.com/docs/graph-api/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](https://electricblaze.com/developer/instagram-feed/schema.html) 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.
| Instagram | 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:
```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 has `id`, `caption`, `media_type`, `media_url`, `permalink`, `timestamp` and `username`. There is no `media_product_type`, so a reel is only a `VIDEO`, and no `children`, so carousel slides are not expanded. The demo feed also lacks `thumbnail_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-item `media[]`. 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`.
## FAQ
### Why does /me/media return only ids?
The request has no `fields=` parameter. The media edge sends bare ids by default; list every field you draw, for example `fields=id,media_type,media_url,thumbnail_url,permalink,timestamp`.
### How do I get only reels from the Instagram API?
Keep items whose `media_product_type` equals `REELS`. The IG Media reference marks that field for Facebook Login only (checked 2026-09-18), so confirm your Instagram Login token returns it. `media_type` says `VIDEO` for reels and other videos alike.
### Why is thumbnail_url missing on photos?
It exists only on `VIDEO` items. Use `media_url` for an `IMAGE`, and the first slide for a `CAROUSEL_ALBUM`.
### How do I get every image in a carousel?
Request `children{media_type,media_url,thumbnail_url}` in the same call. Unexpanded, `children` returns ids, and each slide would need a request of its own.
### Which Graph API version should the URL use?
Meta's Instagram examples use v25.0, available until 2028-07-29, and v26.0 is the newest release (Graph API changelog, checked 2026-09-18). Pin one explicitly.
## Related pages
- [Feed schema v1](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Basic Display alternatives](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/media-fields.html
---
# Instagram Basic Display API is gone: the alternatives in 2026
**Status, checked 2026-09-18:** the Basic Display API has failed every request since 2024-12-04. The Instagram API with Instagram Login replaces it for Business and Creator accounts; personal accounts get no feed API.
> What replaced the Basic Display API after 2024-12-04, which option fits a personal or a professional account, and the exact scope, endpoint and field changes for existing code.
Canonical: https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html
## At a glance
- **Use it when:** a feed, plugin or script stopped showing Instagram posts after December 2024, or inherited code still requests `user_profile,user_media`.
- **Do not use it when:** you already call the Instagram API with Instagram Login and only need field details; see [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).
- **What the human does:** the account owner decides the account type. A personal account has to be switched to Business or Creator before any feed API can list its posts.
- **Next step:** pick a row in the options table, then follow the migration table if you keep your own code.
## What happened and what broke
Meta announced the end of the Basic Display API on 2024-09-04 and switched it off on 2024-12-04; since then every request to it fails, and Meta names the Instagram API with Instagram Login as the replacement ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
| Date | Change |
|---|---|
| 2024-09-04 | Shutdown of the Basic Display API announced. |
| 2024-12-04 | Basic Display API off; all requests fail. |
| 2025-01-27 | Instagram Login scope values `business_basic`, `business_content_publish`, `business_manage_comments` and `business_manage_messages` deprecated. |
| 2025-11-03 | `instagram_oembed` stops returning `thumbnail_url`, `thumbnail_width`, `thumbnail_height` and `author_name`. |
| 2026-05-15 | `instagram_oembed` callable without an access token, 1,000 requests per hour. |
Sources: [Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18; [Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18; [Meta developer blog, oEmbed updates](https://developers.facebook.com/blog/post/2025/04/08/oembed-updates/), checked 2026-09-18; [Meta: Instagram Platform changelog](https://developers.facebook.com/docs/instagram-platform/changelog), checked 2026-09-18.
Three things stopped working on the shutdown date:
- **Code on the old endpoints.** Plugins, widgets and scripts built on Basic Display kept working only if their authors moved them to another API; the rest get errors.
- **Old tokens.** Tokens granted with `user_profile` and `user_media` belonged to the retired API. Each account has to authorize again under `instagram_business_basic`.
- **Personal accounts.** Basic Display could read them. Its replacement serves professional accounts only ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18).
## The options
Four routes are left. Three are legitimate; reject the fourth.
| Option | Lists a feed | Accounts | What it needs | Limits |
|---|---|---|---|---|
| Instagram API with Instagram Login (your code) | Yes, with cursor paging | Business and Creator only | A Meta app, the `instagram_business_basic` scope, a token refreshed within 60 days | Accounts you do not own or manage need Advanced Access: App Review plus Business Verification |
| oEmbed (`instagram_oembed`) | No: one post per call, by URL | Public accounts, except inactive, age-restricted or embed-disabled ones | No token since 2026-05-15 | 1,000 requests per hour; no thumbnail or author name since 2025-11-03; no Stories |
| Hosted feed or widget | Yes | Set by each service | The owner connects the account on the service | Plan limits and image handling differ by vendor |
| Scraping instagram.com | Until it breaks | Any, in theory | Proxies or headless browsers | Breaks on markup changes and violates Instagram's terms; do not use |
Sources: [Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18; [Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18; [Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18; [Meta: Instagram Platform changelog](https://developers.facebook.com/docs/instagram-platform/changelog), checked 2026-09-18; [Meta developer blog, oEmbed updates](https://developers.facebook.com/blog/post/2025/04/08/oembed-updates/), checked 2026-09-18.
### Instagram API with Instagram Login
This is the successor Meta points to, and the host is still `graph.instagram.com`, so most request code survives. What changes is the account type, the scope and the app setup. A sibling variant, the Instagram API with Facebook Login, uses the `instagram_basic` permission instead ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18); choose one and do not mix their scopes.
For accounts you own or manage, Standard Access is enough and no App Review is needed. A product that connects other people's accounts needs Advanced Access, which requires App Review and Business Verification ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18). Details are on [App Review](https://electricblaze.com/developer/instagram-feed/app-review.html).
In Development mode the Instagram account usually also needs the Instagram Tester role; without it, calls fail with "Insufficient developer role", according to one integrator's documentation ([Chatwoot docs on Instagram Business Login](https://developers.chatwoot.com/self-hosted/configuration/features/integrations/instagram-via-instagram-business-login), checked 2026-09-18).
### oEmbed
oEmbed turns a single post URL into embed HTML. Since 2025-11-03 it no longer returns thumbnail fields or `author_name` ([Meta developer blog, oEmbed updates](https://developers.facebook.com/blog/post/2025/04/08/oembed-updates/), checked 2026-09-18), so it cannot feed images into a grid you design. Since 2026-05-15 it can be called without an access token, up to 1,000 requests per hour ([Meta: Instagram Platform changelog](https://developers.facebook.com/docs/instagram-platform/changelog), checked 2026-09-18).
One Meta page describing the oEmbed feature still lists App Review as a requirement. The changelog entry is newer, so test an unauthenticated call before planning around either (changelog checked 2026-09-18).
Terminal:
```sh
curl -sG "https://graph.facebook.com/v25.0/instagram_oembed" \
--data-urlencode "url=https://www.instagram.com/p/EXAMPLE/"
```
It fits a page that shows a few chosen posts. It is not a feed: every post URL must be known in advance, and new posts never appear on their own.
### A hosted feed or widget
A service holds the Instagram connection and gives the site an embed or a JSON feed. The ElectricBlaze widget is one: the owner connects the account in the dashboard and the site pastes two lines of HTML. Which Instagram account types it can connect is not established yet. Its data fields are listed on [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html#electricblaze), and other services are compared, with check dates, in the [feed API comparison](https://electricblaze.com/developer/instagram-feed/comparison.html).
### Scraping
Scrapers of instagram.com break whenever the markup or the login wall changes, and automated collection without permission violates Instagram's Terms of Use. Do not ship one to a client site.
## Move Basic Display code to Instagram Login
If the account is Business or Creator, most existing code stays. The table pairs what Basic Display code called with its current counterpart.
| Piece | Basic Display (retired) | Instagram Login (current) |
|---|---|---|
| Accounts | Personal and professional | Business or Creator only |
| Scopes | `user_profile,user_media` | `instagram_business_basic` |
| List media | `GET graph.instagram.com/me/media` | `GET graph.instagram.com/v25.0//media`, or `/me/media` |
| Media fields | `id`, `caption`, `media_type`, `media_url`, `thumbnail_url`, `permalink`, `timestamp`, `username` | Same names. `caption` is documented for Facebook Login only; test it |
| Short-lived token | From the authorization code | Expires in 1 hour |
| Long-lived token | `ig_exchange_token` | `ig_exchange_token`, server only (it carries the app secret); valid 60 days |
| Refresh | `ig_refresh_token` | `ig_refresh_token`; the token must be at least 24 hours old, unexpired, with `instagram_business_basic` granted |
Sources: [Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18; [Meta: get started with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started), checked 2026-09-18; [Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18; [Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18; [Meta: refresh_access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/refresh_access_token), checked 2026-09-18.
The Basic Display column reflects code written against the retired API, not a Meta source we checked on 2026-09-18. The right column comes from the sources under the table.
Scope names changed again after the shutdown: `business_basic` and the other `business_*` values were deprecated on 2025-01-27 in favor of the `instagram_`-prefixed names ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18). Code written in late 2024 may still carry the short spelling.
Take the authorization URL and redirect settings from the Instagram Login setup of your Meta app rather than editing the old Basic Display URL. A minimal code change then looks like this:
lib/instagram.ts:
```diff
-const SCOPES = "user_profile,user_media";
+const SCOPES = "instagram_business_basic";
-const MEDIA = "https://graph.instagram.com/me/media";
+const MEDIA = "https://graph.instagram.com/v25.0/me/media";
// Field names did not change. caption is documented for Facebook Login only: test it.
const FIELDS = "id,caption,media_type,media_url,thumbnail_url,permalink,timestamp";
```
What `/me/media` returns under Instagram Login: the field names a Basic Display parser already reads (example values):
```json
{
"data": [
{
"id": "17900000000000001",
"caption": "Example caption.",
"media_type": "IMAGE",
"media_url": "https://scontent.cdninstagram.com/v/example/photo-1.jpg?oe=6AB05820",
"permalink": "https://www.instagram.com/p/EXAMPLE1/",
"timestamp": "2026-09-12T07:41:00+0000"
},
{
"id": "17900000000000002",
"caption": "Example caption for a video.",
"media_type": "VIDEO",
"media_url": "https://scontent.cdninstagram.com/v/example/video-1.mp4?oe=6AB05820",
"thumbnail_url": "https://scontent.cdninstagram.com/v/example/video-1-cover.jpg?oe=6AB05820",
"permalink": "https://www.instagram.com/reel/EXAMPLE2/",
"timestamp": "2026-09-10T16:05:00+0000"
}
],
"paging": {
"cursors": { "before": "EXAMPLE_BEFORE", "after": "EXAMPLE_AFTER" }
}
}
```
After the first authorization, a refresh has to run inside each 60-day window ([access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)), and image links still expire ([media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)).
## If the account is personal
The Instagram API with Instagram Login does not serve personal accounts ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18), and no app setting changes that. The consequence: to list a personal account's posts through any API, its owner has to switch it to a professional account, Business or Creator, in the Instagram app.
That switch is the owner's call, not the developer's. Routes that need no switch:
- Embed chosen public posts with oEmbed, one call per post. It does not work for private, inactive, age-restricted or embed-disabled accounts, or for Stories ([Meta: Instagram Platform changelog](https://developers.facebook.com/docs/instagram-platform/changelog), checked 2026-09-18).
- Link to the profile instead of mirroring it. A button to instagram.com has no token to maintain.
- Ask a hosted service whether it accepts personal accounts, and get the answer in writing before you build on it.
Avoid tools that offer a personal-account feed in exchange for the Instagram password or a session cookie. They are scrapers under another name, and the account owner carries the risk.
## FAQ
### When did the Instagram Basic Display API shut down?
On 2024-12-04, three months after the announcement of 2024-09-04 (Meta developer blog, checked 2026-09-18). Every request to it has failed since.
### What replaces the user_profile and user_media scopes?
`instagram_business_basic` in the Instagram API with Instagram Login. It reads the profile and media of Business and Creator accounts.
### Can a personal Instagram account still appear on a website?
Not as an API feed. The owner can switch to Business or Creator, or you can embed selected public posts one at a time with oEmbed.
### Do I need App Review to show my own feed?
No. Standard Access covers accounts you own or manage without App Review (Meta App Review page, checked 2026-09-18). Accounts you do not own or manage need Advanced Access, App Review and Business Verification.
### Does my old media parsing code still work?
Mostly. `media_url`, `permalink` and `timestamp` keep their names; the scope, the token and the account type are what change.
## Related pages
- [Instagram feed overview](https://electricblaze.com/developer/instagram-feed/)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [App Review](https://electricblaze.com/developer/instagram-feed/app-review.html)
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html
---
# Instagram App Review: when a feed needs Advanced Access
**Status, checked 2026-09-18:** every app starts at Standard Access, which covers accounts you own or manage plus people with a role on the app, with no App Review; serving accounts you do not own or manage takes Advanced Access, App Review and Business Verification ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18). The ElectricBlaze widget connects accounts through ElectricBlaze's own Meta app, so the site owner creates none.
> A site that shows its own posts never meets App Review. Here is where the line sits, what the other side of it costs, and how to test before launch.
Canonical: https://electricblaze.com/developer/instagram-feed/app-review.html
## At a glance
- **Use it when:** you are choosing how to get Instagram posts onto one or many websites and need to know whether a Meta review blocks the plan.
- **Do not use it when:** the app publishes posts, moderates comments or sends messages. Those features use other permissions, and this page covers reading a feed only.
- **What the human does:** with Standard Access the account owner only logs in through your app. With Advanced Access someone at your company completes Business Verification and submits App Review in the Meta App Dashboard.
- **Next step:** find your case in the first table. If it says Standard Access, go on to [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
## Your case in one table
What decides the access level is whose Instagram account the token belongs to, and how that person relates to the app.
| Scenario | Whose accounts | Access level | App Review | Business Verification |
|---|---|---|---|---|
| Your own website | Your own Business or Creator account | Standard Access | Not needed | Listed only for Advanced Access |
| Client sites you build and run | Accounts you manage, or whose owners hold a role on your app | Standard Access | Not needed | Listed only for Advanced Access |
| One Meta app per client | The client's own account, in an app the client owns | Standard Access in each app | Not needed | Listed only for Advanced Access |
| Public SaaS, plugin or theme | Accounts of people you have never met | Advanced Access | Required | Required |
| Widget from a feed provider | Connected through the provider's app | Handled by the provider | Not filed by you | Not filed by you |
Sources: [Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18; [Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18.
The last row is where ElectricBlaze and similar embed services sit; it is described at the end of this page.
## Standard Access: the default level
Every app starts at Standard Access. The app can then call the API for Instagram accounts you own or manage and for people who hold a role on the app, and no App Review is involved ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18).
For a feed, the permission to request is `instagram_business_basic`, which reads the profile and its media. `instagram_basic` is its counterpart in the Instagram API with Facebook Login, a different product ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18). The older scope value `business_basic` was deprecated on 2025-01-27 ([Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18).
The account must be professional: Business or Creator. Personal accounts are not supported ([Meta: Instagram Platform overview](https://developers.facebook.com/docs/instagram-platform/overview), checked 2026-09-18), and the Basic Display API was shut down on 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18).
A Standard Access token for your own account, `GET /v25.0/me/media` (example values):
```json
{
"data": [
{
"id": "17900000000000001",
"media_type": "IMAGE",
"media_url": "https://instagram-cdn.example/v/photo-1.jpg?oe=6A1B2C3D",
"permalink": "https://www.instagram.com/p/EXAMPLE1/",
"timestamp": "2026-09-15T14:02:11+0000"
}
],
"paging": {
"cursors": { "before": "EXAMPLE_BEFORE", "after": "EXAMPLE_AFTER" },
"next": "https://graph.instagram.com/v25.0/17841400000000000/media?after=EXAMPLE_AFTER"
}
}
```
If this comes back for your own account, the access level is not what stands between you and the feed. Errors that remain are covered on [feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html).
## Development mode and the Instagram Tester role
Chatwoot's integration guide reports that while an app is in Development mode, the Instagram account usually needs the Instagram Tester role; the owner accepts it in Instagram under Settings, Apps and websites, Tester invites, and without it calls fail with "Insufficient developer role" ([Chatwoot docs on Instagram Business Login](https://developers.chatwoot.com/self-hosted/configuration/features/integrations/instagram-via-instagram-business-login), checked 2026-09-18).
That comes from a secondary source, not from Meta, so read it as the first thing to rule out rather than as a rule. For a site that shows its own feed, the working order is: give the account the role, accept the invite, log in, call `/me/media`.
A role is also how Standard Access reaches an account you do not own. People with a role on the app are one of the two groups it covers, so a client who accepts a role can be served without review.
What else changes when an app moves from Development to Live mode is outside the facts verified for this page. Check the App Dashboard before launch and test with the production account.
## Advanced Access: App Review plus Business Verification
Advanced Access is for apps that serve Instagram accounts you neither own nor manage. Meta requires two things for it: App Review and Business Verification ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18). Typical products on this side of the line:
- A SaaS dashboard where customers connect their own Instagram accounts.
- A WordPress plugin, Shopify app or theme that ships one Meta app to every install.
- A site builder that offers an Instagram block to all of its users.
Until approval, the same app keeps working under Standard Access for your own accounts and for people with a role. That is enough to build the feature, record a demo and test it end to end.
Leave time for both steps before a public launch, because the product cannot serve strangers' accounts until they are done. Each connected account still has its own token with a 60-day life ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18), so a SaaS product also needs a refresh job per account.
## Agencies: three ways to handle client feeds
1. **One Meta app per client, owned by the client.** Standard Access, no review. Each client needs a Meta developer account and a refresh job; forty clients means forty tokens running on separate 60-day clocks.
2. **One agency app, clients with a role.** Standard Access covers people with a role on the app, so no review is needed as long as every account owner accepts one. Each account still has a token of its own to refresh.
3. **An embedded widget.** The provider's app holds the connection; you run no Meta app and store no token.
If it is unclear whether a client account counts as one you manage, a role on the app settles the question.
## Where ElectricBlaze fits
The ElectricBlaze Instagram widget connects the account through ElectricBlaze's own Meta app. The site owner signs in at [a.electricblaze.com](https://a.electricblaze.com/), connects Instagram and pastes the snippet. They create no Meta app, so there is no access level to choose and no review to file.
```html
```
What it does not do, as of 2026-09-18. Its feed carries no `media_product_type` and no carousel `children`, so reels appear as plain videos and carousels as their cover image. There is no supported JSON API for server code yet; one is planned for 0.2.
If a product needs those fields, or publishing and comments, build your own app and pick the access level from the table above.
## FAQ
### Do I need App Review to show my own Instagram feed on my website?
No. Standard Access, the default level, covers Instagram accounts you own or manage and involves no App Review. The account must be a Business or Creator account.
### When is Advanced Access required?
When the app serves Instagram accounts you do not own or manage, such as customers of a SaaS product or users of a plugin. Advanced Access requires App Review and Business Verification ([Meta: Instagram Platform App Review](https://developers.facebook.com/docs/instagram-platform/app-review), checked 2026-09-18).
### Can an agency show client feeds without App Review?
Yes, if the agency manages those accounts or each client holds a role on the agency app; both cases fall under Standard Access. A public product that any account can connect to needs Advanced Access.
### Which permission does an Instagram feed need?
`instagram_business_basic`, which reads the profile and media through the Instagram API with Instagram Login. `instagram_basic` belongs to the Facebook Login variant.
### What does "Insufficient developer role" mean?
A third-party setup guide reports that it appears in Development mode when the Instagram account lacks the Instagram Tester role. The owner accepts the invite in Instagram under Settings, Apps and websites, Tester invites.
### Do I pass App Review to use the ElectricBlaze widget?
No. The account connects through ElectricBlaze's own Meta app; the site owner creates no app and files nothing with Meta.
## Related pages
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
- [Basic Display alternative](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Instagram feed for developers](https://electricblaze.com/developer/instagram-feed/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/app-review.html
---
# Instagram feed not working: API errors, empty data and missing fields
**Status, checked 2026-09-18:** requests to the Basic Display API have failed since 2024-12-04 ([Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18); Instagram Login tokens stop working after 60 days unless refreshed ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18); throttled calls return error `80002`. For the ElectricBlaze widget, three checks on the page itself are listed at the end.
> Start from what you see, match it to a cause Meta documents, apply the fix. Each row names its source and the date we checked it.
Canonical: https://electricblaze.com/developer/instagram-feed/feed-not-working.html
## At a glance
- **Use it when:** a feed built on `graph.instagram.com` returns an error, an empty `data` array, 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](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html).
- **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](https://developers.facebook.com/docs/graph-api/changelog), checked 2026-09-18). Pin the version you tested and upgrade on purpose.
```sh
# -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:
```js
// 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](https://electricblaze.com/developer/instagram-feed/access-token.html)) |
| 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](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)) |
| 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](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)) |
| 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](https://developers.facebook.com/docs/graph-api/guides/error-handling), checked 2026-09-18; [Meta: get started with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started), checked 2026-09-18; [Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18; [verbb/social-feeds issue 21 (2026-01)](https://github.com/verbb/social-feeds/issues/21), checked 2026-09-18 (secondary); [Meta: rate limiting](https://developers.facebook.com/docs/graph-api/overview/rate-limiting), checked 2026-09-18; [Chatwoot docs on Instagram Business Login](https://developers.chatwoot.com/self-hosted/configuration/features/integrations/instagram-via-instagram-business-login), checked 2026-09-18 (secondary); [Meta developer blog, 2024-09-04](https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/), checked 2026-09-18; [Meta: Instagram API with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login), checked 2026-09-18; [Meta: Instagram Platform overview](https://developers.facebook.com/docs/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):
```json
{
"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](https://developers.facebook.com/docs/graph-api/guides/error-handling), checked 2026-09-18). All three end with the owner logging in again.
- Do not branch on `type` alone. `OAuthException` also shows up on errors that have nothing to do with tokens.
- Log `fbtrace_id` with 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:
1. **No fields requested.** Without `fields=`, the media edge returns each item as an id and nothing more ([Meta: get started with Instagram Login](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/get-started), checked 2026-09-18). The renderer has no image to draw.
2. **Another account, or no posts.** A token belongs to exactly one account. Ask `/me?fields=username` which one before debugging the rest.
3. **Pagination stopped early.** A page can hold fewer items than `limit`, so a short page is not the end of the feed; stop only when `paging.next` is absent ([Meta: Graph API paginated results](https://developers.facebook.com/docs/graph-api/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](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18). `thumbnail_url` exists only on VIDEO items ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), 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](https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/ig-media/children), checked 2026-09-18).
> Available for Instagram API with Facebook Login only
>
> [Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18, on `caption` and `media_product_type`
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](https://electricblaze.com/developer/instagram-feed/media-fields.html) 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](https://developers.facebook.com/docs/graph-api/overview/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:
1. **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 and `class="feed electricblaze-id-w42"` does not.
2. **The Content Security Policy lets the widget through.** `script-src` must allow `https://s.electricblaze.com`. `connect-src` must allow `https://api.electricblaze.com`, `https://proxy.electricblaze.com`, which serves the data of connected accounts, and `https://s.electricblaze.com` for demo data. The browser console names any other blocked host and directive.
3. **The state class tells you where it stopped.** The loader sets `electricblaze-state-loading`, `-mounting`, `-mounted` or `-error` on the container. A container left in `electricblaze-state-error` did not mount: check the widget id and the `/v1/package` request in the network panel.
```http
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.
## FAQ
### Why does /me/media return only ids?
The request has no `fields=` parameter. Without it the media edge returns every item as a bare id. Add `fields=id,media_type,media_url,permalink,timestamp`.
### What does OAuthException 190 with error_subcode 463 mean?
The access token expired. Tokens from Instagram Login last 60 days and cannot be refreshed after that, so the account owner has to log in again.
### Why is caption missing from the Instagram API response?
The IG Media reference marks `caption` as available with Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18). Test with your token and render posts without a caption when the field is absent.
### Why do Instagram images stop loading some time after the page was built?
`media_url` is a signed link whose `oe` parameter holds the expiry; after it the server answers 403 "URL signature expired" ([verbb/social-feeds issue 21 (2026-01)](https://github.com/verbb/social-feeds/issues/21), checked 2026-09-18, secondary). Meta publishes no lifetime, so fetch fresh URLs before serving cached pages.
### Does the Instagram Basic Display API still work?
No. Meta shut it down on 2024-12-04, and every request to it fails. The replacement is the Instagram API with Instagram Login, for Business and Creator accounts.
### What is Instagram API error 80002?
The Instagram Platform throttling error. The budget is 4800 × impressions per 24 hours, and the `X-Business-Use-Case-Usage` header shows how close you are. Cache responses on the server.
## Related pages
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Basic Display alternative](https://electricblaze.com/developer/instagram-feed/basic-display-api-alternative.html)
- [App Review](https://electricblaze.com/developer/instagram-feed/app-review.html)
- [Instagram feed for developers](https://electricblaze.com/developer/instagram-feed/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/feed-not-working.html
---
# Instagram images return 403 "URL signature expired": cause and fixes
**Status, checked 2026-09-18:** the lifetime of a `media_url` link is not published by Meta; each link's `oe` parameter shows its own expiry. Whether ElectricBlaze stores copies of images for connected accounts is not established yet.
> Why `media_url` links from the Instagram API stop loading on static and cached sites, how to read the exact expiry from a link, and three ways to keep images on the page.
Canonical: https://electricblaze.com/developer/instagram-feed/media-url-expired.html
## At a glance
- **Use it when:** Instagram images on your site worked at deploy time and now fail with `403`, or opening a `media_url` directly shows `URL signature expired`.
- **Do not use it when:** the API call itself fails or returns no posts. That is a token or permission problem; see [feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html).
- **What the human does:** nothing for the first fix. The copy-on-a-schedule fix needs a repository or bucket the site is served from, and the Instagram token saved as a CI secret.
- **Next step:** decode one broken image URL with the script below. If `expiresAt` is in the past, pick a fix by where your HTML is rendered.
## Why the images break
The API does not hand out permanent image addresses. Each `media_url` is a signed link to Instagram's CDN, and the signature has an end date.
A developer report describes the mechanism: the `oe` query parameter holds the expiry as a hexadecimal Unix timestamp, and after that moment the CDN answers `403` with the body `URL signature expired` ([verbb/social-feeds issue 21 (2026-01)](https://github.com/verbb/social-feeds/issues/21), checked 2026-09-18). That source is a public issue tracker, not Meta, so treat it as secondary.
How long a link lives is not documented. A question about it on the Meta developer forum has no official answer ([Meta developer community thread](https://developers.facebook.com/community/threads/889163498159993/), checked 2026-09-18), so a number of hours quoted in a blog post is someone's observation, not a contract.
The code that fetched the posts is rarely at fault. What breaks is the place where the links were stored:
- **Static export or SSG.** The build writes the links of that moment into HTML that is then served for weeks.
- **ISR or a CDN cache.** A `revalidate` window or `s-maxage` longer than the remaining life of the links keeps serving dead URLs until the next regeneration.
- **A JSON cache.** Posts saved to KV, Redis or a file at fetch time go stale the same way.
- **An image optimizer.** `next/image` and similar proxies fetch the original again on a cache miss and receive the same `403`.
A dev server usually hides the problem because it calls the API on each request, so every link it renders is fresh. Handle `thumbnail_url` and the URLs inside `children` the same way as `media_url` unless you have checked that they carry no `oe`.
## Read the expiry from oe
The `oe` value is seconds since 1970, written in hexadecimal. `parseInt(oe, 16)` gives Unix seconds; multiply by 1000 for a JavaScript `Date`. In the browser console the same check on a broken image is one line: `new Date(parseInt(new URL(img.src).searchParams.get("oe"), 16) * 1000)`.
decode-oe.mjs:
```js
// Prints when an Instagram CDN link stops working.
// Usage: node decode-oe.mjs ""
const link = new URL(process.argv[2]);
const oe = link.searchParams.get("oe");
if (!oe) {
console.log(JSON.stringify({ oe: null, note: "no oe parameter: expiry unknown" }));
} else {
const unixSeconds = parseInt(oe, 16);
const expiresAt = new Date(unixSeconds * 1000);
console.log(JSON.stringify({
oe,
unixSeconds,
expiresAt: expiresAt.toISOString(),
expired: expiresAt.getTime() <= Date.now(),
}, null, 2));
}
```
Output for an example link with `oe=6AB0F580`, run on 2026-09-18 (illustrative value, not a measured lifetime):
```json
{
"oe": "6AB0F580",
"unixSeconds": 1789982080,
"expiresAt": "2026-09-21T09:14:40.000Z",
"expired": false
}
```
Run it on a link fresh from the API and on a broken one. The fresh link shows how much time a new response gives you. The broken one confirms that expiry, and not a token problem, emptied the page.
Do not hardcode what you observe. The lifetime is Meta's to change, and nothing guarantees that every link in one response expires at the same moment.
## Fix 1: fetch again before the links expire
When a server renders the page, cache the posts for less time than their links live. Instead of guessing a number, derive it from the response: take the earliest `oe` and subtract a safety margin.
lib/instagram-cache.ts:
```ts
// Seconds until the first Instagram CDN link in a /me/media response expires, minus a margin.
type Item = { media_url?: string; thumbnail_url?: string; children?: { data: Item[] } };
function links(items: Item[]): string[] {
return items
.flatMap((m) => [m.media_url, m.thumbnail_url, ...links(m.children?.data ?? [])])
.filter((u): u is string => Boolean(u));
}
export function secondsUntilFirstExpiry(items: Item[], marginSec = 600): number | null {
const expiries = links(items)
.map((u) => new URL(u).searchParams.get("oe"))
.filter((oe): oe is string => oe !== null)
.map((oe) => parseInt(oe, 16));
if (expiries.length === 0) return null; // no readable expiry: choose a short TTL yourself
const now = Math.floor(Date.now() / 1000);
return Math.max(0, Math.min(...expiries) - now - marginSec);
}
```
Use the result wherever the cache lifetime can be set per response: `Cache-Control: s-maxage=` from a route handler or serverless function, or the TTL of a KV entry. Each regeneration calls the API, receives new signatures and restarts the clock.
Where the window must be fixed in advance, as with a static `revalidate` value in Next.js, log the computed number for a few days and set the window well below the smallest value you saw.
This fix needs a server at request time. GitHub Pages and other static hosts have none, and calling `graph.instagram.com` from the browser would expose the token. On those hosts use fix 2 or 3.
## Fix 2: copy the images on a schedule
Download each image once, while its link is still valid, and serve that copy. A copy has no signature, so it keeps loading until you delete it. A scheduled CI job is enough for most sites:
scripts/sync-instagram.mjs:
```js
// Downloads the latest posts and one cover image per post into public/instagram/.
// Runs in CI. IG_ACCESS_TOKEN comes from repository secrets and never reaches the browser.
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
const OUT = "public/instagram";
const token = process.env.IG_ACCESS_TOKEN;
if (!token) throw new Error("IG_ACCESS_TOKEN is not set");
const api = new URL("https://graph.instagram.com/v25.0/me/media");
api.searchParams.set("fields", "id,media_type,media_url,thumbnail_url,permalink,timestamp,children{media_type,media_url,thumbnail_url}");
api.searchParams.set("limit", "12");
api.searchParams.set("access_token", token);
const res = await fetch(api);
if (!res.ok) throw new Error(`Instagram API ${res.status}: ${await res.text()}`);
const { data } = await res.json();
// Photo: media_url. Video: thumbnail_url. Carousel: the same rule on the first slide.
function coverOf(m) {
const item = m.media_type === "CAROUSEL_ALBUM" ? m.children?.data?.[0] : m;
return item?.media_type === "VIDEO" ? item.thumbnail_url : item?.media_url;
}
await mkdir(OUT, { recursive: true });
const posts = [];
for (const m of data) {
const src = coverOf(m);
if (!src) continue; // media_url is omitted for copyright-flagged media
const img = await fetch(src);
if (!img.ok) continue; // a fresh link should load; if not, the post waits for the next run
const ext = img.headers.get("content-type")?.includes("webp") ? "webp" : "jpg";
const file = `${m.id}.${ext}`;
await writeFile(`${OUT}/${file}`, Buffer.from(await img.arrayBuffer()));
posts.push({ id: m.id, mediaType: m.media_type, url: m.permalink, image: `/instagram/${file}`, publishedAt: m.timestamp });
}
// Delete copies of posts that are no longer in the response.
const keep = new Set(posts.map((p) => p.image.split("/").pop()));
for (const f of await readdir(OUT)) {
if (f !== "feed.json" && !keep.has(f)) await rm(`${OUT}/${f}`);
}
await writeFile(`${OUT}/feed.json`, JSON.stringify({ fetchedAt: new Date().toISOString(), posts }, null, 2) + "\n");
```
.github/workflows/instagram-sync.yml:
```yaml
name: Sync Instagram
on:
schedule:
- cron: "23 5 * * *" # sets how soon new posts appear; copies stay valid between runs
workflow_dispatch:
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
- run: node scripts/sync-instagram.mjs
env:
IG_ACCESS_TOKEN: ${{ secrets.IG_ACCESS_TOKEN }}
- name: Commit changed files
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add public/instagram
git diff --cached --quiet || { git commit -m "Sync Instagram posts"; git push; }
```
`public/instagram/feed.json` written by the script (example values):
```json
{
"fetchedAt": "2026-09-18T05:23:41.512Z",
"posts": [
{
"id": "17900000000000001",
"mediaType": "IMAGE",
"url": "https://www.instagram.com/p/EXAMPLE1/",
"image": "/instagram/17900000000000001.jpg",
"publishedAt": "2026-09-12T07:41:00+0000"
},
{
"id": "17900000000000002",
"mediaType": "VIDEO",
"url": "https://www.instagram.com/reel/EXAMPLE2/",
"image": "/instagram/17900000000000002.jpg",
"publishedAt": "2026-09-10T16:05:00+0000"
}
]
}
```
Three details decide whether this holds up in production:
- **The token still expires.** A long-lived token is valid for 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18), so the job fails after that unless someone refreshes it. See [access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html).
- **Removed posts leave the site.** The script deletes files that are no longer in the latest response, so a post the owner deletes on Instagram disappears here on the next run.
- **Videos are not copied.** The script keeps the cover frame and links video posts to `permalink`. For playback on the page, upload the video files to object storage, not to the repository.
Committing images works for a small feed on GitHub Pages or Netlify. For more posts, upload to S3, Cloudflare R2 or Supabase Storage in the same job and write the public object URLs into `feed.json` instead of local paths.
## Fix 3: a hosted feed that serves copies
Some feed services download media and serve it from their own servers; others pass Instagram's signed links through. That can change over time, so check it before you rely on a vendor for a static site. The images column of the [feed API comparison](https://electricblaze.com/developer/instagram-feed/comparison.html#data) records, with a check date, which services keep copies.
A quick test works for any service: fetch its feed, pick an image URL, and look for an `oe=` parameter or an Instagram CDN host such as `cdninstagram.com` or `fbcdn.net`. If the link carries `oe`, decode it with the script above.
## What ElectricBlaze can promise today
What we observed, kept apart from what we do not know:
- **The widget asks for its data on every page load.** Each call to the package endpoint returns a newly signed data address (observed 2026-09-17), so the post list is never baked into your HTML and a static site does not freeze the feed at build time.
- **Where the images of a connected account are served from is not established yet.** They may be Instagram CDN links or copies; we have not verified either and do not claim one. The demo feed's images sit on `s.electricblaze.com`, which says nothing about real accounts.
- **The CLI 0.1.2 ships demo data only.** How the 0.2 JSON API will serve images of real posts is not established yet.
If image longevity on a static site is a hard requirement today, fix 2 is the option you control end to end.
## FAQ
### How long does an Instagram media_url stay valid?
Meta does not say; a forum question about it has no official answer (checked 2026-09-18). Decode the `oe` parameter of your own links and cache for less than the time it shows.
### Can I renew an expired media_url?
Not by editing the link. Request the post again, for example `GET /v25.0/?fields=media_url,thumbnail_url`, and use the new link it returns.
### Does thumbnail_url expire as well?
Assume it does. Check it for an `oe` parameter exactly as you check `media_url`.
### Why do images load locally but fail in production?
Locally the API is usually called on each request. In production a build, an ISR window or a CDN cache keeps serving links that were valid when the page was generated.
### Does the ElectricBlaze widget avoid expired images?
It requests a freshly signed data address on each page load, so the post list is never frozen in your HTML. Whether images of connected accounts are Instagram links or stored copies is not established yet.
## Related pages
- [Static HTML sites](https://electricblaze.com/developer/instagram-feed/static-site.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Compare feed APIs](https://electricblaze.com/developer/instagram-feed/comparison.html)
- [Feed not working](https://electricblaze.com/developer/instagram-feed/feed-not-working.html)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/media-url-expired.html
---
# The electricblaze CLI: commands, output and what it touches
**Status, checked 2026-09-18:** version 0.1.2, released 2026-09-18. `add`, `preview`, `doctor` and `skill` work with demo data. `login`, `connect` and `list` need the JSON API and exit 2 until 0.2.
> A non-interactive command line tool that puts an Instagram feed component with demo data into a project. Built to be run by coding agents: JSON output, stable exit codes, no prompts.
Canonical: https://electricblaze.com/developer/instagram-feed/cli.html
## At a glance
- **Use it when:** you want a feed component, types and realistic demo data in a project in one step, and you need output a script or an agent can parse.
- **Do not use it when:** you need real posts from the CLI today: that is 0.2. Use the [widget embed](https://electricblaze.com/developer/instagram-feed/nextjs.html#widget) for real posts now.
- **What the human does:** nothing for the demo. The CLI has no account, no login and no telemetry in 0.1.
- **Next step:** run `npx electricblaze@0.1.2 add instagram-feed --json` from the project root and read `files` and `next`.
## Before you run it
An agent should tell the user what it is about to run. These facts are what the user needs to say yes:
| Question | Answer |
|---|---|
| Package | [electricblaze on npm](https://www.npmjs.com/package/electricblaze), MIT license |
| Source | [github.com/ElectricBlaze/electricblaze](https://github.com/ElectricBlaze/electricblaze) |
| Runtime | Node.js 18 or newer, no dependencies; no install scripts |
| Publishing | Only from GitHub Actions with npm provenance; no npm token exists outside the workflow |
| Writes | Only inside the project directory or `--dir`; every file is listed in the output |
| Overwrites | Never a file you edited, unless you pass `--force`; line endings do not count as an edit |
| Network | None in 0.1. The generated loader calls the ElectricBlaze API only when `ELECTRICBLAZE_API_KEY` is set (0.2) |
| Security contact | [SECURITY.md](https://github.com/ElectricBlaze/electricblaze/blob/main/SECURITY.md) |
Pin the version in scripts and agent sessions, then verify the signature after an install:
```sh
npx electricblaze@0.1.2 --version
npm install -D electricblaze@0.1.2 && npm audit signatures
```
## Commands
| Command | Does | Works today |
|---|---|---|
| `add instagram-feed` | Writes the component, loader, types and demo feed for the detected framework | Yes |
| `preview [instagram]` | Prints the feed as JSON; `--formats=reel --limit=6` filter it | Yes, demo data |
| `doctor` | Checks Node, framework, installed kit, feed validity and account state | Yes |
| `skill` | Installs the agent skill into `.claude/skills/` and `.agents/skills/` | Yes |
| `login` | Device-flow sign in, like `gh auth login` | No, exits 2 until 0.2 |
| `connect instagram` | Prints a URL for the account owner and waits for the connection | No, exits 2 until 0.2 |
| `list` | Lists connected accounts | No, exits 2 until 0.2 |
Flags: `--json`, `--yes`, `--help`, `--version`, `--force`, `--dir=`, `--framework=next|html|data`, `--agent=claude,codex`, `--limit=`, `--formats=reel,post`. A flag the command does not accept is an error, not a silent no-op.
## Exit codes and JSON output
Every command accepts `--json` and never prompts. Three exit codes cover every outcome:
- `0`: done. Read `files`, `notes` and `next`.
- `1`: something is broken, for example an invalid feed file. The failing check carries a `next` command.
- `2`: the command needs something that does not exist yet, or the input was wrong. `next` says what to run instead.
`npx electricblaze connect instagram --json` today (exit 2):
```json
{
"ok": false,
"command": "connect",
"status": "not_available_yet",
"message": "\"connect\" needs the ElectricBlaze API and ships in 0.2. Everything else works in demo mode today.",
"next": "Keep the demo feed for now; read https://electricblaze.com for the API status."
}
```
`npx electricblaze add tiktok-feed --json` (exit 2):
```json
{
"ok": false,
"error": "unknown component \"tiktok-feed\"",
"available": ["instagram-feed"],
"next": "Run: npx electricblaze add instagram-feed"
}
```
## What add writes, per framework
The framework is detected from `package.json`. `--framework=next|html|data` overrides it.
| Detected | Kit | Files |
|---|---|---|
| `next` | `next` | `components/eb/InstagramFeed.tsx`, `InstagramFeed.module.css`, `lib/eb/instagram.ts`, `lib/eb/feed.d.ts`, `lib/eb/demo/instagram.json`, `.env.example` (under `src/` with `src/app`). With only a `pages/` directory the output notes that the component needs the App Router; the loader works in `getStaticProps` |
| `astro`, `@sveltejs/kit`, `nuxt`, `@remix-run/react`, `react-router`, `gatsby`, `vite` | `data` | `lib/eb/feed.d.ts`, `lib/eb/demo/instagram.json` (under `src/` when it exists) |
| No `package.json`, or anything else | `html` | `electricblaze/instagram-feed.js`, `.css`, `instagram.json`, `instagram.demo.js`, `feed.d.ts` |
The `data` kit is deliberate. Those frameworks would not serve a script dropped into the project root, and each has its own way to load JSON. See [Astro](https://electricblaze.com/developer/instagram-feed/astro.html), [SvelteKit](https://electricblaze.com/developer/instagram-feed/sveltekit.html) and [Nuxt](https://electricblaze.com/developer/instagram-feed/nuxt.html) for the component to write.
When the project has `AGENTS.md` or `CLAUDE.md`, `add` appends a block between `` and ``. It tells the next coding agent where the feed lives and where real posts come from. Running `add` again updates the block in place.
## doctor, the next-step machine
`doctor` finds whichever kit is installed, even one that does not match the framework, validates the feed file against the schema and reports the account state.
`npx electricblaze doctor --json` in a Next.js project after add (trimmed):
```json
{
"ok": true,
"framework": "next",
"kit": "next",
"checks": [
{ "name": "node", "status": "ok", "detail": "Node 24.13.0" },
{ "name": "framework", "status": "ok", "detail": "Next.js (app router)" },
{ "name": "component", "status": "ok", "detail": "instagram-feed (next)" },
{ "name": "feed", "status": "ok", "detail": "12 posts, schema v1, demo data" },
{ "name": "account", "status": "warn", "detail": "not connected: demo feed",
"next": "Real posts today: connect the account in the ElectricBlaze widget and paste its snippet (https://electricblaze.com). JSON by API key: npx electricblaze connect instagram, ships in 0.2." }
]
}
```
## Other ways to install the same component
- **shadcn:** `npx shadcn@latest add ElectricBlaze/electricblaze/instagram-feed` installs the Next.js files through the shadcn registry in the repository.
- **Claude Code plugin:** `/plugin marketplace add ElectricBlaze/electricblaze`, then `/plugin install electricblaze@electricblaze`.
- **Agent skill only:** `npx electricblaze skill`. See [the skill page](https://electricblaze.com/developer/instagram-feed/skill.html).
Release notes for every version are on the [changelog](https://electricblaze.com/developer/changelog.html).
## FAQ
### Does the CLI send telemetry?
No. Version 0.1 makes no network calls. The generated loader calls the ElectricBlaze API only when `ELECTRICBLAZE_API_KEY` is set, and that API ships in 0.2.
### Can an agent run it without a human?
Yes. Nothing prompts, every command has `--json`, and exit code 2 always comes with a `next` command. It should still tell the user which files it wrote.
### What happens if I run add twice?
Unchanged files are reported as `unchanged`, edited files as `skipped`. Nothing you changed is overwritten without `--force`.
### Why does add tiktok-feed fail?
Only Instagram exists today. The command exits 2 and lists the available components rather than writing a component that could not work.
### How do I verify the package?
Install a pinned version and run `npm audit signatures`, or open the Provenance section of the version on npmjs.com.
## Related pages
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Agent skill](https://electricblaze.com/developer/instagram-feed/skill.html)
- [Changelog](https://electricblaze.com/developer/changelog.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Plain HTML](https://electricblaze.com/developer/instagram-feed/static-site.html)
- [Developers home](https://electricblaze.com/developer/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/cli.html
---
# The electricblaze-feeds agent skill for Claude Code, Codex and other agents
**Status, checked 2026-09-18:** the skill is part of the `electricblaze` repository and npm package 0.1.2. It covers Instagram only and says so: TikTok and YouTube are marked planned, and real posts are routed to the widget until the 0.2 API.
> A `SKILL.md` with the workflow, the feed schema and dated Instagram API facts. With it installed, a request like "add our Instagram to the footer" ends with a working component instead of a hand-written token fetch.
Canonical: https://electricblaze.com/developer/instagram-feed/skill.html
## At a glance
- **Use it when:** you or your team build sites with a coding agent and want Instagram feed requests handled the same way every time, with the account step handed to a human.
- **Do not use it when:** the project already has its own Instagram integration that works. The skill would suggest replacing a fetch you are happy with.
- **What the human does:** approves the install. Later, the site owner connects the Instagram account when real posts are needed.
- **Next step:** run `npx electricblaze skill` in the project, or `npx skills add ElectricBlaze/electricblaze` for any agent the skills CLI supports.
## Install it
| You use | Command | Installs to |
|---|---|---|
| Claude Code and Codex, per project | `npx electricblaze skill` | `.claude/skills/` and `.agents/skills/`; `--agent=claude` or `--agent=codex` picks one |
| Any agent the skills CLI knows | `npx skills add ElectricBlaze/electricblaze` | The directory of the agent you choose; `-a claude-code -y` skips the questions |
| Claude Code, as a plugin | `/plugin marketplace add ElectricBlaze/electricblaze`, then `/plugin install electricblaze@electricblaze` | Your Claude Code plugins |
| By hand | Copy `skills/electricblaze-feeds/` from [the repository](https://github.com/ElectricBlaze/electricblaze) | Anywhere your agent reads skills |
We ran both command-line routes on 2026-09-18. `npx skills add ElectricBlaze/electricblaze --list` finds one skill, `electricblaze-feeds`. The electricblaze route prints what it wrote:
`npx electricblaze skill --json` in an empty project (paths trimmed):
```json
{
"ok": true,
"skill": "electricblaze-feeds",
"agents": ["claude", "codex"],
"files": [
{ "agent": "claude", "path": ".claude/skills/electricblaze-feeds/SKILL.md", "status": "written" },
{ "agent": "claude", "path": ".claude/skills/electricblaze-feeds/references/schema.md", "status": "written" },
{ "agent": "claude", "path": ".claude/skills/electricblaze-feeds/references/platforms.md", "status": "written" },
{ "agent": "codex", "path": ".agents/skills/electricblaze-feeds/SKILL.md", "status": "written" },
{ "agent": "codex", "path": ".agents/skills/electricblaze-feeds/references/schema.md", "status": "written" },
{ "agent": "codex", "path": ".agents/skills/electricblaze-feeds/references/platforms.md", "status": "written" }
],
"next": "Start a new agent session in this project; the skill triggers on requests like \"add an Instagram feed to the footer\"."
}
```
A skill you edited is never overwritten. Start a new agent session afterwards: agents read skill descriptions when a session begins.
## When it triggers
An agent loads a skill when the request matches its description. This is the description, word for word:
> Add an Instagram feed (posts, reels, carousels) to a website or app with ElectricBlaze; TikTok and YouTube feeds are planned, not available yet. Use when asked to show social media posts on a page, an Instagram grid or reels in a footer, or when Instagram Graph API / Basic Display API / access token questions come up.
>
> [SKILL.md](https://github.com/ElectricBlaze/electricblaze/blob/main/skills/electricblaze-feeds/SKILL.md), front matter, version 0.1.2
Prompts that trigger it in practice: "show our latest Instagram posts in the footer", "add a reels strip to the home page", "my Instagram feed stopped working after Basic Display was shut down".
## What it tells the agent to do
1. Run `npx electricblaze add instagram-feed --json` and read `framework`, `files`, `notes` and `next`.
2. Wire the result in: the Server Component in Next.js, the four-line snippet in plain HTML, or a native component over the JSON for Astro, SvelteKit, Nuxt and the other `data` frameworks.
3. Restyle freely, but keep the "demo feed" label until real data flows.
4. Filter on request: `formats={["reel"]}` in Next.js, `data-formats="reel"` in HTML, `limit` for fewer posts.
5. Check with `npx electricblaze doctor --json`; every failing check carries a `next` command.
6. Hand the account step to a human: the widget today, `connect` in 0.2.
The skill also carries a list of things never to do. It forbids scraping instagram.com, putting a token in a client-side fetch to graph.instagram.com, setting `ELECTRICBLAZE_API_KEY` before 0.2, editing `feed.d.ts`, and faking real posts by editing the demo JSON.
## What is inside
| File | Contents |
|---|---|
| `SKILL.md` | Front matter with name and description, the six-step workflow, the never-do list, a quick view of the data shape |
| `references/schema.md` | Schema v1 field by field; the same contract as [the schema page](https://electricblaze.com/developer/instagram-feed/schema.html) |
| `references/platforms.md` | Instagram API facts with a check date and sources: Basic Display shutdown, professional accounts only, 60-day tokens, reel and carousel fields |
The format is the open [Agent Skills](https://agentskills.io) standard: a folder with a `SKILL.md` that has YAML front matter. Claude Code reads it from `.claude/skills/`, Codex from `.agents/skills/`.
## Without the skill: the AGENTS.md block
If a project has `AGENTS.md` or `CLAUDE.md`, `npx electricblaze add` appends a short block between `` and ``. It gives any agent that opens the project the essentials, even without the skill:
AGENTS.md (block written by add):
```md
## Social feeds (ElectricBlaze)
The Instagram feed on this site comes from ElectricBlaze (TikTok and YouTube
are planned, not available yet). The feed shape is schema v1 (see the copied
`feed.d.ts`) ...
- `npx electricblaze preview instagram --json` prints the current feed
(demo data until an account is connected).
- `npx electricblaze doctor --json` checks the setup and explains what to do next.
- Real posts need a human. Today the site owner connects the account in the
ElectricBlaze widget at https://electricblaze.com and pastes its embed
snippet where the feed belongs. The JSON API for this component
(`npx electricblaze connect instagram`) ships in 0.2.
```
## Why a skill and not only docs
Without guidance a coding agent writes the Instagram code itself. The code looks right in review and fails weeks later. The usual failures are a long-lived token that expires after 60 days ([Meta: access_token reference](https://developers.facebook.com/docs/instagram-platform/reference/access_token), checked 2026-09-18) and image links that stop loading on a cached page ([media URL expired](https://electricblaze.com/developer/instagram-feed/media-url-expired.html)).
A skill moves the decision to the start of the task. The agent learns what exists today, what the human has to do, and which command gives it a working grid in one step.
## FAQ
### Does the skill work outside Claude Code and Codex?
The files follow the Agent Skills format. `npx skills add ElectricBlaze/electricblaze` installs them for any agent that CLI supports; others can read `SKILL.md` as plain Markdown.
### Does it make network calls?
The skill is text. The commands it suggests are the `electricblaze` CLI, which makes no network calls in 0.1.
### Will it replace my existing Instagram code?
Only if you ask for a feed and let the agent follow it. Remove the skill if your project already has an integration you want to keep.
### How do I update it?
Run `npx electricblaze@latest skill` again. Files you edited are reported as skipped; `--force` replaces them.
## Related pages
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Access tokens](https://electricblaze.com/developer/instagram-feed/access-token.html)
- [Changelog](https://electricblaze.com/developer/changelog.html)
- [Developers home](https://electricblaze.com/developer/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/skill.html
---
# ElectricBlaze feed schema v1: one JSON shape for social posts
**Status, checked 2026-09-18:** schema v1 is what `npx electricblaze add` writes today (as demo data) and what the 0.2 JSON API will return. The widget embed uses its own internal data, shown at the end of this page.
> The contract between ElectricBlaze and your component. Render `type`, filter by `format`, and never parse a platform payload in your UI.
Canonical: https://electricblaze.com/developer/instagram-feed/schema.html
## At a glance
- **Use it when:** you write a component that renders social posts and want one shape for posts, reels and carousels, with the rendering decision in a single field.
- **Do not use it when:** you consume the raw Instagram API yourself. Its envelope is different; see [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).
- **What the human does:** nothing. The schema, the types and the sample are public files.
- **Next step:** download [schema.json](https://electricblaze.com/developer/instagram-feed/schema.json) and [sample-feed.json](https://electricblaze.com/developer/instagram-feed/sample-feed.json), or run `npx electricblaze@0.1.2 preview instagram --json`.
## The shape in thirty seconds
A feed is an origin (the account) plus a list of posts. Every post carries what a renderer needs as required fields. Anything specific to one platform stays in `raw`.
lib/eb/feed.d.ts (excerpt):
```ts
interface Feed {
schemaVersion: 1;
source: "instagram" | "youtube" | "tiktok" | (string & {});
origin: { kind: "profile" | "hashtag" | "playlist" | "board" | "channel";
id: string; name: string; displayName: string; url: string; avatarUrl?: string };
posts: Post[];
fetchedAt: string; // ISO 8601
demo: boolean; // true until an account is connected
nextCursor?: string;
}
interface Post {
id: string; source: string;
type: "image" | "video" | "carousel" | "text" | "link"; // what to render
format: string; // "post" | "reel" on Instagram
status: "published" | "live" | "upcoming";
url: string; // permalink on the platform
text: string; // caption, may be empty
media: { url: string; mime: string; width?: number; height?: number; alt?: string; durationSec?: number }[];
thumbnailUrl?: string; // required for image, video and carousel
aspectRatio?: number; // width / height: 1, 0.8, 0.5625, 1.7778
durationSec?: number;
publishedAt: string;
stats?: { likes?: number; comments?: number; views?: number; shares?: number };
raw?: unknown; // original platform payload, untouched
}
```
The full file with comments is [feed.d.ts on GitHub](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/feed.d.ts). The CLI copies it into your project as `lib/eb/feed.d.ts`.
## type decides layout, format decides badges
Two fields describe a post, and they answer different questions. `type` tells the component which element to draw. `format` repeats what the platform calls the post, so you can filter and label it.
| type | format | Instagram calls it | Render | Posts in the sample |
|---|---|---|---|---|
| `image` | `post` | Photo post | `` | 6 |
| `video` | `reel` | Reel | Poster from `thumbnailUrl`, "Reel" badge, 9:16 | 2 |
| `video` | `post` | Video in the feed | Poster from `thumbnailUrl`, play badge | 2 |
| `carousel` | `post` | Album (`CAROUSEL_ALBUM`) | First item, count badge from `media.length` | 2 |
Layout code should switch on `type` only. A new platform format, such as a YouTube `short`, then renders correctly without a code change. `thumbnailUrl` is always present for the three visual types, so a grid never needs to inspect `media`.
## One post, as JSON
A reel from `sample-feed.json`. Its media file is vertical, so `aspectRatio` is 0.5625 (9:16).
posts[1] of sample-feed.json:
```json
{
"id": "demo-ig-02",
"source": "instagram",
"type": "video",
"format": "reel",
"status": "published",
"url": "https://electricblaze.com/demo/instagram#post-02",
"text": "How we cup a new lot in under 60 seconds.",
"media": [
{
"url": "https://cdn.jsdelivr.net/gh/ElectricBlaze/electricblaze@89e2819/demo/media/reel-01.mp4",
"mime": "video/mp4",
"width": 540,
"height": 960,
"durationSec": 10
}
],
"thumbnailUrl": "https://cdn.jsdelivr.net/gh/ElectricBlaze/electricblaze@89e2819/demo/media/reel-01.jpg",
"aspectRatio": 0.5625,
"durationSec": 10,
"publishedAt": "2026-09-10T16:05:00Z",
"stats": { "likes": 1287, "comments": 64, "views": 18450 }
}
```
The demo posts are synthetic: a coffee roaster with twelve posts, photos under 1:1, 4:5, 9:16 and 16:9, two reels, two videos and two carousels. The media files are licensed from Pexels and Blender Foundation projects and served from jsDelivr, pinned to a commit.
## Files you can use
| File | What it is | Use it for |
|---|---|---|
| [schema.json](https://electricblaze.com/developer/instagram-feed/schema.json) | JSON Schema, draft 2020-12, `$id` on this site | Validation in CI, form generators, other languages |
| [sample-feed.json](https://electricblaze.com/developer/instagram-feed/sample-feed.json) | The 12-post demo feed, byte for byte the file the CLI ships | Fixtures, Storybook, design work |
| [feed.d.ts](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/feed.d.ts) | TypeScript types with comments | Import as `import type { Feed } from "./lib/eb/feed"` |
| [validate.js](https://github.com/ElectricBlaze/electricblaze/blob/main/schema/validate.js) | Dependency-free validator used by `doctor` | Runtime checks without a schema library |
Validate a file with Ajv, which supports draft 2020-12:
```sh
curl -sO https://electricblaze.com/developer/instagram-feed/schema.json
npx -p ajv-cli@5 -p ajv-formats ajv validate --spec=draft2020 -c ajv-formats -s schema.json -d lib/eb/demo/instagram.json
# lib/eb/demo/instagram.json valid
```
We ran this on 2026-09-18: the sample passes, and a post with `thumbnailUrl` removed fails with `must have required property 'thumbnailUrl'`.
Or let the CLI do it: `npx electricblaze doctor --json` validates whichever feed file it finds in the project and names the field that fails.
## How Instagram fields map onto it
This is how an Instagram post becomes a schema v1 post. Field details and their caveats are on [media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html).
| Instagram field | Schema v1 | Note |
|---|---|---|
| `media_type` IMAGE / VIDEO / CAROUSEL_ALBUM | `type` image / video / carousel | Always present |
| `media_product_type` REELS | `format: "reel"` | Documented for Facebook Login only ([Meta: IG Media reference](https://developers.facebook.com/docs/instagram-platform/reference/instagram-media), checked 2026-09-18) |
| `thumbnail_url`, else `media_url` | `thumbnailUrl` | `thumbnail_url` exists only on videos |
| `media_url` | `media[0].url` | Left out for copyright-flagged media |
| `children{media_type,media_url,thumbnail_url}` | `media[]` | Only when expanded in `fields=` |
| `permalink` | `url` | |
| `caption` | `text` | Empty string when absent |
| `timestamp` | `publishedAt` | Normalized to ISO 8601 with Z |
| `like_count`, `comments_count` | `stats.likes`, `stats.comments` | Optional |
## Two shapes you may meet today
The ElectricBlaze widget predates schema v1. Its script loads the Graph API response as it comes, and your code never sees it. We list it so that an agent inspecting network traffic does not mistake it for the public format.
| | Widget data (internal) | Schema v1 (public contract) |
|---|---|---|
| Envelope | `{"data": [...], "paging": {...}}` | `{"schemaVersion": 1, "origin": {...}, "posts": [...]}` |
| Post fields | `id, caption, media_type, media_url, permalink, timestamp, username` | See above |
| Reels | Not marked: a reel is a `VIDEO` | `format: "reel"` when the source provides it |
| Carousel items | Not expanded | `media[]` |
| Address | Signed URL, minted per page load, expires within hours (observed 2026-09-17) | Your file today; the 0.2 API later |
Code against schema v1 only. Breaking changes will raise `schemaVersion`; fields a renderer does not know should be ignored, not rejected.
## FAQ
### Why both type and format?
`type` is closed and drives layout: five values that every renderer handles. `format` is open and repeats the platform term, so a new format never breaks a grid.
### Is thumbnailUrl always there?
For `image`, `video` and `carousel` posts, yes; the schema makes it required for those types. Text and link posts may omit it.
### Can I extend the schema?
Do not edit `feed.d.ts`; it is the contract. Wrap the post in your own type or read extra data from `raw`.
### Does the widget return this format?
No. The widget renders its own data inside the page. Schema v1 is what the CLI writes and what the 0.2 JSON API will serve.
### Where is the JSON Schema hosted?
At `https://electricblaze.com/developer/instagram-feed/schema.json`, which is also its `$id`.
## Related pages
- [Media fields](https://electricblaze.com/developer/instagram-feed/media-fields.html)
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Agent skill](https://electricblaze.com/developer/instagram-feed/skill.html)
- [Next.js](https://electricblaze.com/developer/instagram-feed/nextjs.html)
- [Astro](https://electricblaze.com/developer/instagram-feed/astro.html)
- [Instagram feed hub](https://electricblaze.com/developer/instagram-feed/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/instagram-feed/schema.html
---
# Changelog of the electricblaze CLI
**Status, checked 2026-09-18:** the latest release is 0.1.2, published to npm from GitHub Actions with provenance on 2026-09-18. The JSON API, `login` and `connect` are planned for 0.2.
> Generated from `CHANGELOG.md` in the repository at every site build. Newest release first.
Canonical: https://electricblaze.com/developer/changelog.html
## Follow releases
Subscribe to the [Atom feed](https://electricblaze.com/developer/changelog.xml) or watch releases of [the repository](https://github.com/ElectricBlaze/electricblaze/releases). Each GitHub release carries the same text as the section below, and each version can be installed exactly with `npx electricblaze@`.
What is planned next, with no date promised: the JSON API behind the generated component and the `login`, `connect` and `list` commands (0.2). TikTok and YouTube follow Instagram; they are not available in any released version.
## 0.1.2, 2026-09-18
Install this version: `npx electricblaze@0.1.2`. [Release on GitHub](https://github.com/ElectricBlaze/electricblaze/releases/tag/v0.1.2).
### Added
- `--framework=data`, detected automatically for Astro, SvelteKit, Nuxt, Remix, React Router, Gatsby and Vite: writes only `lib/eb/demo/instagram.json` and `lib/eb/feed.d.ts` (under `src/` when the project has it) instead of a plain HTML kit those frameworks would not serve.
- Flag validation: an unknown flag, a flag that does not apply to the command, a flag without its value (`--dir`) or a bad `--limit` exits 2 with the list of accepted flags. A typo is no longer a silent no-op.
- `preview instagram-feed` is accepted as an alias of `preview instagram`.
- `CHANGELOG.md`, `SECURITY.md`; the publish workflow creates a GitHub release from the changelog section of the tag.
### Changed
- The skill, README, `doctor` and the block written into `AGENTS.md` and `CLAUDE.md` describe the two routes to real posts honestly: the ElectricBlaze widget embed works today, the JSON API for the generated component ships in 0.2. TikTok and YouTube are described as planned, not available.
- `doctor` finds whichever kit is installed (Next.js, data-only or plain HTML) even when it does not match the detected framework, and reports `kit` in `--json`.
- `doctor` says plainly that setting `ELECTRICBLAZE_API_KEY` before 0.2 makes the loader throw; the loader's header comment says the same.
- Line endings are ignored when deciding whether a file was edited, so a checkout with `autocrlf` no longer turns every file into "kept".
- The import path suggested after `add` uses the `@/*` alias from `tsconfig.json` or `jsconfig.json` when the project has one.
- Carousel badge says "items" instead of "photos" when the carousel contains a video.
- `bin` sets `process.exitCode` instead of calling `process.exit()`, so piped JSON is never cut off.
- `references/platforms.md` carries the check date and the documentation sources; the reel and carousel facts name the exact fields.
- The publish workflow no longer installs an unpinned `npm@latest`; it checks that the bundled npm supports trusted publishing instead.
## 0.1.1, 2026-09-15
Install this version: `npx electricblaze@0.1.1`. [Release on GitHub](https://github.com/ElectricBlaze/electricblaze/releases/tag/v0.1.1).
- Agent skill `electricblaze-feeds` (workflow, schema, platform facts) and the `skill` command that installs it into `.claude/skills/` and `.agents/skills/`.
- Claude Code plugin and marketplace manifests (`.claude-plugin/`).
- `registry.json`: the repository is a shadcn registry (`npx shadcn@latest add ElectricBlaze/electricblaze/instagram-feed`).
## 0.1.0, 2026-09-15
Install this version: `npx electricblaze@0.1.0`. [Release on GitHub](https://github.com/ElectricBlaze/electricblaze/releases/tag/v0.1.0).
- `add instagram-feed` for Next.js App Router and plain HTML, `preview`, `doctor`; all non-interactive with `--json` and exit codes 0/1/2.
- Feed schema v1 (`schema/feed.d.ts`) with a dependency-free validator.
- Synthetic Instagram demo feed with self-hosted media.
## 0.0.3, 2026-09-15
Install this version: `npx electricblaze@0.0.3`. [Release on GitHub](https://github.com/ElectricBlaze/electricblaze/releases/tag/v0.0.3).
- The CLI reads its version from `package.json`.
## 0.0.1, 2026-09-15
Install this version: `npx electricblaze@0.0.1`. [Release on GitHub](https://github.com/ElectricBlaze/electricblaze/releases/tag/v0.0.1).
- Name reserved on npm; CI and trusted publishing set up.
## Related pages
- [The CLI](https://electricblaze.com/developer/instagram-feed/cli.html)
- [Agent skill](https://electricblaze.com/developer/instagram-feed/skill.html)
- [Feed schema](https://electricblaze.com/developer/instagram-feed/schema.html)
- [Developers home](https://electricblaze.com/developer/)
---
ElectricBlaze, updated 2026-09-18. Source of this page: https://electricblaze.com/developer/changelog.html