REST API

The Feedframer REST API returns an Instagram account's profile and posts as JSON. This page is the reference for every endpoint, parameter and response field.

Base URL

https://feedframer.com

All REST endpoints live under /api/v1.

Authentication

Every request needs your API key in the api_key query parameter. Headers are not read.

?api_key=YOUR_API_KEY

Each connected Instagram account has its own key. You'll find it on the Dashboard, on the account's card, labelled "Feedframer API Token". A key only ever reads its own account. See Authentication for regenerating and revoking keys.

Endpoints

Method Path Returns
GET /api/v1/me The account's profile plus its posts. Documented on this page.
GET /api/v1/accounts The account's profile as a JSON:API resource. See Accounts API.
GET /api/v1/rss An RSS 2.0 feed of the latest posts. See RSS Feed.
GET or POST /graphql GraphQL. See GraphQL API.

There is no /api/v1/posts endpoint. Posts come from /api/v1/me. Any other path under /api/ returns a 404 with the code NOT_FOUND.

GET /api/v1/me

Returns the profile of the account that owns the key, with its posts nested in a posts array.

Parameters

All parameters go in the query string. Only api_key is required.

Parameter Values Default Notes
api_key Your API key Required.
page[size] Integer, 1 to 100 Your plan's limit: 6 on Free, 100 on Premium Posts per page. On Free, values from 7 to 100 return 6 posts. Values below 1, above 100, or not a whole number return a 422.
page[cursor] pagination.nextCursor or pagination.prevCursor from a previous response Premium only. Free keys can send it, but it is ignored. cursor is accepted as an alias. See Pagination.
filter[type] IMAGE, VIDEO, CAROUSEL_ALBUM, REELS Case-insensitive. REELS returns the same posts as VIDEO.
filter[publishedAfter] ISO 8601 date or date-time Posts published at or after this time. A plain date means 00:00 UTC on that day.
filter[publishedBefore] ISO 8601 date or date-time Posts published at or before this time. A plain date includes the whole of that day, up to 23:59:59 UTC.
sort -published_at, published_at, -publishedAt, publishedAt, -id, id -published_at A leading - means descending. See Filtering & Sorting.
include[] children, comments Adds carousel items or comments to each post. Must use array syntax. Repeat it for both: include[]=children&include[]=comments.
format jpg, webp jpg Image format of the URLs in sizes. Lowercase only. Premium only, because only premium responses have sizes. Free keys can send it, but it has no effect.

There is no parameter to choose an account. The key decides which account you get.

Square brackets in query strings trip up some tools. With curl, pass -g (or URL-encode the brackets as %5B and %5D). A + in a time zone offset must be sent as %2B, or it arrives as a space and fails validation.

Free and Premium

Free Premium
Posts you can read The 6 most recent Every post Feedframer has stored for the account
Default and maximum page[size] 6 100
Further pages None Follow pagination.nextCursor
sizes field and format Not included Included

On Free, filters and sorting apply within those 6 most recent posts. For example, filter[type]=VIDEO returns only the videos among the latest 6, which may be none. sort=published_at returns the same 6 posts, oldest first. The pagination object on a free key always has nextCursor: null, prevCursor: null and hasMore: false.

The limits are set per plan on the pricing page.

Example request

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&filter[type]=IMAGE'

This returns the image posts, newest first, up to your plan's page size.

Example response

{
  "username": "your_username",
  "name": "Your Name",
  "biography": "Your bio text",
  "website": "https://example.com",
  "profilePictureUrl": "https://cdn.feedframer.com/your_username/123_profile.jpg",
  "followersCount": 1234,
  "followsCount": 567,
  "mediaCount": 89,
  "status": "ready",
  "lastFetchAt": "2026-01-15T19:50:34+00:00",
  "posts": [
    {
      "id": "18123456789012345",
      "caption": "Sunset over the harbour #photography",
      "altText": null,
      "mediaType": "IMAGE",
      "mediaUrl": "https://cdn.feedframer.com/your_username/18123456789012345_media.jpg",
      "thumbnailUrl": null,
      "permalink": "https://www.instagram.com/p/ABC123def45/",
      "timestamp": "2026-01-15T18:30:00+00:00",
      "likeCount": 1542,
      "commentsCount": 87,
      "sizes": {
        "320": "https://cdn.feedframer.com/cdn-cgi/image/width=320,format=jpg/your_username/18123456789012345_media.jpg",
        "768": "https://cdn.feedframer.com/cdn-cgi/image/width=768,format=jpg/your_username/18123456789012345_media.jpg",
        "960": "https://cdn.feedframer.com/cdn-cgi/image/width=960,format=jpg/your_username/18123456789012345_media.jpg",
        "1200": "https://cdn.feedframer.com/cdn-cgi/image/width=1200,format=jpg/your_username/18123456789012345_media.jpg"
      }
    }
  ],
  "pagination": {
    "nextCursor": "eyJwdWJsaXNoZWRfYXQiOiIyMDI2LTAxLTE1IDE4OjMwOjAwIiwiaWQiOjQyLCJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9",
    "prevCursor": null,
    "hasMore": true,
    "perPage": 100
  }
}

This is a premium response. A free response has no sizes field on posts.

Response headers

Header Value
Content-Type application/json
X-Last-Fetched-At When Feedframer last fetched this account's posts from Instagram, as ISO 8601 (for example 2026-09-30T19:50:34+00:00), or Never if it hasn't yet. Also in the body as lastFetchAt, which browser JavaScript can read.
X-Feedframer-Account-Status State of the Instagram connection: ready, importing, refreshing_token or invalid_token. On every response. See Knowing when a feed has stopped updating.
X-Feedframer-Feed-Stale Only present when the connection is broken. Plain-English explanation of when the feed froze and what fixes it.
Cache-Control max-age=3600, public in production. Responses are cached at the edge for an hour. See Caching & Performance.
Access-Control-Allow-Origin *, so you can call the API from browser JavaScript.

Account fields

These are at the top level of the response.

Field Type Notes
username string Instagram username, without the @.
name string or null Display name.
biography string or null Bio text. May contain line breaks.
website string or null Website from the Instagram profile.
profilePictureUrl string or null Profile picture. Usually a copy on cdn.feedframer.com.
followersCount integer or null Followers.
followsCount integer or null Accounts this account follows.
mediaCount integer or null Total posts on Instagram. This counts all posts on Instagram, not just the ones Feedframer returns.
status string State of the Instagram connection. See Connection status below.
lastFetchAt string or null When Feedframer last fetched this account's posts from Instagram, as ISO 8601, or null if it never has. Same value as the X-Last-Fetched-At header.
posts array Posts on this page. See below. Can be empty.
pagination object See Pagination fields below.

Profile fields refresh at most once a day.

Connection status

status tells you whether Feedframer is still able to reach Instagram for this account. A broken connection does not change the HTTP status code. You still get a 200 with the posts we last stored, so an existing feed keeps rendering instead of going blank — but nothing new will arrive until the account is reconnected. Check this field if you want to know.

status Meaning What to do
ready Healthy. Posts refresh on your plan's interval. Nothing.
importing First import is still running. posts may be empty or partial. Retry shortly.
refreshing_token A routine token refresh is in flight. Posts are current. Nothing.
invalid_token Instagram is refusing our access token, so the feed has stopped updating. posts are the last ones we stored and may be months old. Reconnect the account at feedframer.com/dashboard. Reconnecting does not change your API key.

An Instagram access token lasts 60 days, and it can also be cut short by a password change, by removing Feedframer in Instagram's app settings, or by a security checkpoint on the account. Feedframer refreshes tokens automatically and emails you if a connection breaks, but if you are rendering a feed on a live site it is worth surfacing invalid_token somewhere you will see it:

const feed = await fetch('https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY')
  .then((r) => r.json());

if (feed.status === 'invalid_token') {
  // Still render feed.posts — they are real, just not fresh.
  console.warn(`Feedframer: @${feed.username} needs reconnecting. Last updated ${feed.lastFetchAt}.`);
}

Responses are cached at the edge for an hour, so a status change can take up to an hour to appear. /api/v1/accounts returns the same status for every account on your plan.

Post fields

Each item in posts has these fields.

Field Type Notes
id string Instagram's id for the post.
caption string or null Caption text, including hashtags and mentions.
altText string or null Almost always null. Instagram rarely provides alt text.
mediaType string IMAGE, VIDEO or CAROUSEL_ALBUM. Never REELS: Instagram reports reels as VIDEO.
mediaUrl string The post's media, usually a copy stored on cdn.feedframer.com. For CAROUSEL_ALBUM it is the first item. For VIDEO it is usually an .mp4 file, but can be a .jpg when Instagram withheld the video file. If Feedframer hasn't stored the media, this is Instagram's own URL (scontent.cdninstagram.com), which expires after a while.
thumbnailUrl string or null A .jpg poster image for VIDEO posts. null for other types.
permalink string The post on instagram.com.
timestamp string When the post was published on Instagram, ISO 8601 in UTC, for example 2026-01-15T18:30:00+00:00.
likeCount integer or null Likes. null when Instagram doesn't provide it.
commentsCount integer or null Comments. null when Instagram doesn't provide it.
sizes object Premium only. Resized image URLs keyed "320", "768", "960" and "1200" (pixel widths). For VIDEO posts they are resized copies of the thumbnail. The field is left out, not set to null, on free keys and on any post whose media Feedframer hasn't stored.
children array Only with include[]=children. See Carousel items below.
comments array Only with include[]=comments. See Comments below.

Don't put mediaUrl in an <img> tag for VIDEO posts. It is usually a video file. Use thumbnailUrl instead:

const imageSrc = post.mediaType === 'VIDEO' ? post.thumbnailUrl : post.mediaUrl;

Pagination fields

Field Type Notes
nextCursor string or null Pass as page[cursor] to get the next page. null on the last page, and always null on Free.
prevCursor string or null Pass as page[cursor] to go back a page. null on the first page, and always null on Free.
hasMore boolean true when there is a next page, meaning nextCursor is not null. Always false on Free.
perPage integer The page size used for this request: your page[size], capped at your plan's limit.

Cursors are opaque strings. Pass them back unchanged. See Pagination for loops in several languages.

Including extra data

include takes an array, so write it as include[]=. Plain include=children or include=children,comments returns a 422. To get both, repeat the parameter:

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&include[]=children&include[]=comments'

A CAROUSEL_ALBUM post's mediaUrl is only its first item. Add include[]=children to get every item:

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&filter[type]=CAROUSEL_ALBUM&include[]=children'

Each post then has a children array. Posts that aren't carousels get "children": [].

{
  "id": "18123456789012345",
  "caption": "Three views of the harbour",
  "altText": null,
  "mediaType": "CAROUSEL_ALBUM",
  "mediaUrl": "https://cdn.feedframer.com/your_username/18123456789012345_media.jpg",
  "thumbnailUrl": null,
  "permalink": "https://www.instagram.com/p/ABC123def45/",
  "timestamp": "2026-01-15T18:30:00+00:00",
  "likeCount": 1542,
  "commentsCount": 87,
  "children": [
    {
      "id": "18111111111111111",
      "caption": null,
      "altText": null,
      "mediaType": "IMAGE",
      "mediaUrl": "https://cdn.feedframer.com/your_username/18111111111111111_media.jpg",
      "thumbnailUrl": null,
      "permalink": "https://www.instagram.com/p/XYZ789ghi01/",
      "timestamp": "2026-01-15T18:30:00+00:00",
      "likeCount": null,
      "commentsCount": null
    },
    {
      "id": "18222222222222222",
      "caption": null,
      "altText": null,
      "mediaType": "VIDEO",
      "mediaUrl": "https://cdn.feedframer.com/your_username/18222222222222222_media.mp4",
      "thumbnailUrl": "https://cdn.feedframer.com/your_username/18222222222222222_thumbnail.jpg",
      "permalink": "https://www.instagram.com/p/JKL456mno78/",
      "timestamp": "2026-01-15T18:30:00+00:00",
      "likeCount": null,
      "commentsCount": null
    }
  ]
}

This example is from a free key, so there are no sizes.

Children have the same fields as posts, so the same rendering code works for both. The differences:

  • caption, altText, likeCount and commentsCount are always null. Use the parent post's values.
  • Each child has its own permalink, which is not the parent's.
  • mediaType is IMAGE or VIDEO. VIDEO children have a thumbnailUrl; IMAGE children have null.
  • mediaUrl and thumbnailUrl always point to Feedframer's CDN. Children whose media Feedframer hasn't stored are left out of the array.
  • On premium keys every child has a sizes field. It uses the thumbnail for VIDEO children, and is null for a VIDEO child with no stored thumbnail. On free keys children have no sizes field.

Comments

Add include[]=comments to get each post's comments as Instagram returned them at the last fetch:

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&include[]=comments'

Each post then has a comments array. It is empty when there are none.

{
  "comments": [
    {
      "id": "17900000000000000",
      "text": "Love this view",
      "timestamp": "2026-01-15T19:02:11+0000",
      "like_count": 3
    }
  ]
}
Field Type Notes
id string Instagram's comment id.
text string The comment. Instagram occasionally leaves this field out.
timestamp string ISO 8601, in Instagram's format with no colon in the offset (+0000).
like_count integer Likes on the comment. Snake case, because Feedframer passes Instagram's data through unchanged.

Comments don't include the commenter's username.

Responsive images and format (Premium)

Premium responses include a sizes object on each post whose media Feedframer has stored. It holds URLs for the same image resized to 320, 768, 960 and 1200 pixels wide, served through Cloudflare. For VIDEO posts, they are resized copies of the thumbnail.

Add format=webp to get WebP URLs instead of JPEG:

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&format=webp'
"sizes": {
  "320": "https://cdn.feedframer.com/cdn-cgi/image/width=320,format=webp/your_username/18123456789012345_media.jpg",
  "768": "https://cdn.feedframer.com/cdn-cgi/image/width=768,format=webp/your_username/18123456789012345_media.jpg",
  "960": "https://cdn.feedframer.com/cdn-cgi/image/width=960,format=webp/your_username/18123456789012345_media.jpg",
  "1200": "https://cdn.feedframer.com/cdn-cgi/image/width=1200,format=webp/your_username/18123456789012345_media.jpg"
}

The keys are strings. In JavaScript, use post.sizes['320']; post.sizes.320 is a syntax error. Because sizes is missing on free keys and on some posts, fall back to thumbnailUrl or mediaUrl:

function imageAttributes(post) {
  const fallback = post.mediaType === 'VIDEO' ? post.thumbnailUrl : post.mediaUrl;

  if (!post.sizes) {
    return { src: fallback };
  }

  return {
    src: post.sizes['768'],
    srcset: ['320', '768', '960', '1200']
      .map((width) => `${post.sizes[width]} ${width}w`)
      .join(', '),
  };
}

Filtering

# Image posts only
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&filter[type]=IMAGE'

# Posts published during March 2026 (both dates included in full)
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&filter[publishedAfter]=2026-03-01&filter[publishedBefore]=2026-03-31'

Filters can be combined. See Filtering & Sorting for every rule.

Sorting

# Oldest first
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&sort=published_at'
Value Order
-published_at or -publishedAt Newest first (the default)
published_at or publishedAt Oldest first
-id Most recently stored by Feedframer first
id First stored by Feedframer first

id sorting uses the order Feedframer stored the posts, not the Instagram post id.

Pagination

Premium keys page through posts with cursors:

# First page, 50 posts (Premium; a free key gets its 6 posts)
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&page[size]=50'

# Next page: pass pagination.nextCursor from the previous response
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&page[size]=50&page[cursor]=NEXT_CURSOR'

Keep the same filters and sort on every page. Free keys get one page. See Pagination.

Errors

Every error has the Content-Type application/vnd.api+json and a JSON:API body, whatever Accept header you send. Switch on code, not on the message text.

HTTP status code When
401 MISSING_API_KEY No api_key parameter.
401 INVALID_API_KEY The key is unknown, was regenerated, or belongs to a disconnected account.
404 NOT_FOUND The path doesn't exist, for example /api/v1/posts.
405 METHOD_NOT_ALLOWED Anything other than GET on a REST endpoint.
422 VALIDATION_ERROR A parameter is invalid. There is one entry per problem.
500 INTERNAL_SERVER_ERROR Something went wrong on Feedframer's side. Retry, then contact [email protected].

There are no rate limits.

A request with several bad parameters gets one error for each:

curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&page[size]=500&sort=newest'
{
  "errors": [
    {
      "status": "422",
      "code": "VALIDATION_ERROR",
      "title": "Invalid Parameter",
      "detail": "The page[size] field must not be greater than 100.",
      "source": { "parameter": "page[size]" }
    },
    {
      "status": "422",
      "code": "VALIDATION_ERROR",
      "title": "Invalid Parameter",
      "detail": "The sort value must be one of: -published_at, published_at, -publishedAt, publishedAt, -id, id.",
      "source": { "parameter": "sort" }
    }
  ]
}

source.parameter names the parameter the way you sent it. These are the validation messages /api/v1/me can return:

source.parameter detail
page[size] The page[size] field must be an integer.
page[size] The page[size] field must be at least 1.
page[size] The page[size] field must not be greater than 100.
filter[type] The selected filter[type] is invalid.
filter[publishedAfter] The filter[publishedAfter] field must be a valid date.
filter[publishedBefore] The filter[publishedBefore] field must be a valid date.
sort The sort value must be one of: -published_at, published_at, -publishedAt, publishedAt, -id, id.
include The include field must be an array.
include[] The selected include[] is invalid.
format The selected format is invalid.

The 401 bodies look like this:

{
  "errors": [
    {
      "status": "401",
      "code": "INVALID_API_KEY",
      "title": "Authentication Failed",
      "detail": "The provided API key is invalid."
    }
  ]
}

See Error Handling for handling them in code.

Code examples

You can also try requests in the API Playground when you're logged in.