FeedFusionDocs
Get started free

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

 
http
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

PropTypeDefaultDescription
publicKeystringrequired32-character hex from your dashboard. Tied to the username path segment.
limitnumber (1 to 100)24Posts 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.
offsetnumber (0 or greater)0Starting index of the slice. Use with limit for pagination.

Example response

 
json
{
  "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:

PropTypeDefaultDescription
usernamestringrequiredEchoes the username from the URL. Useful for logging.
metaobjectrequiredPagination + tier + cache info (see below).
postsPost[]requiredArray of post objects (see below). May be empty if the account has no posts yet.

meta object:

PropTypeDefaultDescription
tier"free" | "pro" | "agency"requiredThe subscription tier of the feed's owner. Use it to drive tier-aware client behaviour if needed.
showBadgebooleanrequiredtrue 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.
overLimitbooleanrequiredtrue once this month's feed loads have passed the plan's limit. The feed keeps working; nothing is blocked.
widgetSettingsobject | nullrequiredThe 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.
viewTokenstring—Used by the embeddable widget to report views. You don't need it in your own integration; it may be absent.
totalnumberrequiredTotal posts available in the cached feed, before applying limit/offset. Use to detect when pagination ends.
limitnumberrequiredPosts actually returned in this response. Matches the limit query param after clamping.
offsetnumberrequiredStarting index of this slice. Matches the offset query param.
cacheTtlnumberrequiredSuggested 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:

PropTypeDefaultDescription
idstringrequiredInstagram post ID. Stable; use as React key.
timestampstringrequiredISO 8601 timestamp from Instagram (e.g. "2026-04-02T09:49:14+0000").
permalinkstringrequiredPublic Instagram URL for the post. Link to this on click.
mediaType"IMAGE" | "VIDEO" | "CAROUSEL_ALBUM"requiredType of post.
mediaUrlstringrequiredDirect URL to the media file. For VIDEO posts, this is the video file; for IMAGE posts, the image.
thumbnailUrlstring | nullrequiredCover image for VIDEO and CAROUSEL_ALBUM. null for plain IMAGE posts (use mediaUrl directly).
optimizedMediaUrlstring | nullrequiredA 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.
optimizedMediaSmallUrlstring | nullrequiredThe 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).
captionstring | nullrequiredPost caption. Can be null for posts without text.

Response headers

PropTypeDefaultDescription
X-Ratelimit-LimitnumberrequiredPer-minute rate limit cap for this publicKey (600 at time of writing).
X-Ratelimit-RemainingnumberrequiredRequests remaining in the current sliding-window minute.
X-Ratelimit-ResetnumberrequiredUnix timestamp (seconds) when the rate limit window resets.
Retry-Afternumber—Seconds to wait before retrying. Only present on 429 responses.
Access-Control-Allow-OriginstringrequiredAlways *. CORS-open for browser-side widgets.
Access-Control-Expose-HeadersstringrequiredExposes the four X-Ratelimit-* and Retry-After headers to browser JavaScript.

Status codes

PropTypeDefaultDescription
200OKrequiredReturns the feed JSON. Rate-limit headers populated.
400Bad RequestrequiredMissing username or publicKey query parameter.
403ForbiddenrequiredUsername 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.
429Too Many RequestsrequiredThe 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.
500Internal Server ErrorrequiredDatabase 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.

PropTypeDefaultDescription
Freetierrequired10,000 feed loads per month, 10 manual refreshes per month.
Protierrequired100,000 feed loads per month, 100 manual refreshes per month.
AgencytierrequiredUnlimited 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