Instagram feed JSON API reference
GET /api/feeds/{username} returns your Instagram posts as JSON. FeedFusion fetches them from the Instagram Graph API endpoints (Meta's Instagram API with Instagram Login), keeps a stored copy for you, and authenticates requests with a public-key query parameter, so it works in any framework.
A single GET endpoint serves the cached feed for a connected Instagram account. It authenticates with a public-key query parameter, and browser requests must come from one of the feed's allowed sites. Below is the full surface.
Endpoint
GET https://www.feed-fusion.com/api/feeds/{username}?publicKey={publicKey}The username path segment is your Instagram handle (the same one shown on the feed page in your dashboard). The publicKey query parameter is a 32-character hex string from the same dashboard page. Both must match a single connected feed; mismatched pairs return 403.
Query parameters
| Prop | Type | Default | Description |
|---|---|---|---|
publicKey | string | required | 32-character hex from your dashboard. Tied to the username path segment. |
limit | number (1 to 100) | 24 | Posts to return in this response. FeedFusion stores the 24 most recent posts per account, so meta.total and any one response top out at 24. Out-of-range values clamp to the bounds. |
offset | number (0 or greater) | 0 | Starting index of the slice. Use with limit for pagination. |
Example response
{
"username": "your-instagram-handle",
"meta": {
"tier": "free",
"showBadge": true,
"overLimit": false,
"total": 24,
"limit": 12,
"offset": 0,
"cacheTtl": 3600
},
"posts": [
{
"id": "18418647295121497",
"timestamp": "2026-04-02T09:49:14+0000",
"permalink": "https://www.instagram.com/p/DWn_VzyDB65/",
"mediaType": "IMAGE",
"mediaUrl": "https://scontent-iad3-1.cdninstagram.com/...",
"thumbnailUrl": null,
"optimizedMediaUrl": "https://...public.blob.vercel-storage.com/...",
"optimizedMediaSmallUrl": "https://...public.blob.vercel-storage.com/...-640.webp",
"caption": "..."
}
]
}Response fields
Top-level:
| Prop | Type | Default | Description |
|---|---|---|---|
username | string | required | Echoes the username from the URL. Useful for logging. |
meta | object | required | Pagination + tier + cache info (see below). |
posts | Post[] | required | Array of post objects (see below). May be empty if the account has no posts yet. |
meta object:
| Prop | Type | Default | Description |
|---|---|---|---|
tier | "free" | "pro" | "agency" | required | The subscription tier of the feed's owner. Use it to drive tier-aware client behaviour if needed. |
showBadge | boolean | required | true when the "Powered by FeedFusion" badge should show: always on Free, and on paid plans once the month's feed loads go over the plan's limit. The embeddable widget handles this for you. |
overLimit | boolean | required | true once this month's feed loads have passed the plan's limit. The feed keeps working; nothing is blocked. |
widgetSettings | object | null | required | The layout saved in your dashboard's Customize widget: { limit, columns, columnsMobile, gap, aspectRatio, borderRadius } (gap and borderRadius in px). null when nothing is saved. The embeddable widget applies it for you; use it in your own grid if you like. |
viewToken | string | — | Used by the embeddable widget to report views. You don't need it in your own integration; it may be absent. |
total | number | required | Total posts available in the cached feed, before applying limit/offset. Use to detect when pagination ends. |
limit | number | required | Posts actually returned in this response. Matches the limit query param after clamping. |
offset | number | required | Starting index of this slice. Matches the offset query param. |
cacheTtl | number | required | Suggested cache time for your side, in seconds (3600 at time of writing). FeedFusion has no edge cache, so this is only a hint for your own caching. |
post object:
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Instagram post ID. Stable; use as React key. |
timestamp | string | required | ISO 8601 timestamp from Instagram (e.g. "2026-04-02T09:49:14+0000"). |
permalink | string | required | Public Instagram URL for the post. Link to this on click. |
mediaType | "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM" | required | Type of post. |
mediaUrl | string | required | Direct URL to the media file. For VIDEO posts, this is the video file; for IMAGE posts, the image. |
thumbnailUrl | string | null | required | Cover image for VIDEO and CAROUSEL_ALBUM. null for plain IMAGE posts (use mediaUrl directly). |
optimizedMediaUrl | string | null | required | A copy of the image to display, hosted by FeedFusion: the photo for IMAGE posts, the cover for VIDEO and CAROUSEL_ALBUM. Use it first: Instagram's own media URLs are signed and expire. null when the copy isn't ready; fall back to thumbnailUrl ?? mediaUrl. |
optimizedMediaSmallUrl | string | null | required | The same image, at most 640 px wide (WebP), hosted by FeedFusion. Use it for grids and thumbnails; null until the small copy is ready (fall back to optimizedMediaUrl). |
caption | string | null | required | Post caption. Can be null for posts without text. |
Response headers
| Prop | Type | Default | Description |
|---|---|---|---|
X-Ratelimit-Limit | number | required | Per-minute rate limit cap for this publicKey (600 at time of writing). |
X-Ratelimit-Remaining | number | required | Requests remaining in the current sliding-window minute. |
X-Ratelimit-Reset | number | required | Unix timestamp (seconds) when the rate limit window resets. |
Retry-After | number | — | Seconds to wait before retrying. Only present on 429 responses. |
Access-Control-Allow-Origin | string | required | Always *. CORS-open for browser-side widgets. |
Access-Control-Expose-Headers | string | required | Exposes the four X-Ratelimit-* and Retry-After headers to browser JavaScript. |
Status codes
| Prop | Type | Default | Description |
|---|---|---|---|
200 | OK | required | Returns the feed JSON. Rate-limit headers populated. |
400 | Bad Request | required | Missing username or publicKey query parameter. |
403 | Forbidden | required | Username and publicKey don't match a connected feed (returned without consuming rate-limit quota), or a browser called from a site that isn't in the feed's allowed sites (body {"error":"domain_not_allowed"}). Add the site in your dashboard. |
429 | Too Many Requests | required | The per-minute rate limit (600/min per publicKey) was exceeded. Retry-After tells you when to retry. Monthly plan limits never return 429: past the limit the feed keeps working and meta.overLimit is true. |
500 | Internal Server Error | required | Database error or other upstream failure. Captured in our error monitoring; retry with backoff. |
Rate limits
The endpoint allows up to 600 requests per minute per publicKey, on a sliding window. That's more than enough for any normal site traffic; we built it to absorb traffic spikes without throttling the legitimate visitors.
Monthly feed-load limits apply on top of the per-minute rate limit. Each successful response counts as one feed load. Going over never blocks the feed: meta.overLimit turns true and the badge shows until the month resets.
| Prop | Type | Default | Description |
|---|---|---|---|
Free | tier | required | 10,000 feed loads per month, 10 manual refreshes per month. |
Pro | tier | required | 100,000 feed loads per month, 100 manual refreshes per month. |
Agency | tier | required | Unlimited feed loads, unlimited manual refreshes. |
Caching and CORS
Responses aren't cached at the edge: each request reads the latest stored feed, so changes saved in your dashboard show on the next page load. meta.cacheTtl (3600 seconds) is a hint for your own caching. Server-side calls (Node, mobile apps, scripts) work from anywhere. Browser calls are accepted from the sites you list for the feed in your dashboard (localhost is always allowed, for development); other sites get 403 domain_not_allowed.
Questions? hello@feed-fusion.com
Continue reading