jQuery Integration
This guide shows how to fetch and display Instagram posts with jQuery and the Feedframer REST API. The examples call GET /api/v1/me, which returns your profile and your posts in one response.
Installation
Include jQuery in your HTML:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
About your API key
The browser sends your API key with each request, so visitors can see it in the page source. The key is read-only and only reads posts that are already public on Instagram, so this is usually fine. If a key leaks and you want to stop it working, press Regenerate on the account's card on the Dashboard. To keep the key off the page entirely, call Feedframer from your own server. The JavaScript examples page has a small proxy under "Keeping the key on a server".
Basic example
This page loads your latest posts into a grid. Without page[size], the API returns your plan's limit: 6 posts on Free, up to 100 on Premium.
It builds every element with jQuery and sets captions with .text(), so a caption can't inject HTML into your page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Instagram Feed</title>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<style>
.instagram-feed {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
.post {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.post img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
.post-caption {
padding: 15px;
font-size: 14px;
white-space: pre-line;
}
</style>
</head>
<body>
<div id="instagram-feed" class="instagram-feed"></div>
<script>
const apiKey = 'YOUR_API_KEY';
// VIDEO posts: mediaUrl is usually an .mp4, so show the thumbnail.
// Premium posts carry resized copies in `sizes`.
function imageFor(post) {
if (post.sizes) {
return post.sizes['768'];
}
return post.mediaType === 'VIDEO' ? post.thumbnailUrl : post.mediaUrl;
}
function renderPosts(posts) {
const $feed = $('#instagram-feed');
posts.forEach(function (post) {
const src = imageFor(post);
if (!src) {
return;
}
const $post = $('<div>', { 'class': 'post' });
$('<a>', { href: post.permalink, target: '_blank', rel: 'noopener noreferrer' })
.append($('<img>', {
src: src,
alt: post.altText || 'Instagram post',
loading: 'lazy'
}))
.appendTo($post);
if (post.caption) {
$('<p>', { 'class': 'post-caption' }).text(post.caption).appendTo($post);
}
$feed.append($post);
});
}
$(function () {
$.ajax({
url: 'https://feedframer.com/api/v1/me',
method: 'GET',
dataType: 'json',
data: { api_key: apiKey }
})
.done(function (response) {
renderPosts(response.posts);
})
.fail(function () {
$('#instagram-feed').text('Could not load Instagram posts.');
});
});
</script>
</body>
</html>
With filtering
Pass parameters in data and jQuery encodes the square brackets for you. This loads only image posts:
$(function () {
$.ajax({
url: 'https://feedframer.com/api/v1/me',
dataType: 'json',
data: {
api_key: 'YOUR_API_KEY',
'filter[type]': 'IMAGE'
}
}).done(function (response) {
renderPosts(response.posts);
});
});
filter[type] accepts IMAGE, VIDEO, CAROUSEL_ALBUM and REELS. REELS returns the same posts as VIDEO, because Instagram reports reels as videos. On the Free plan, filters apply within your 6 most recent posts, so a filter can return fewer than 6 posts, or none.
To add carousel children or comments, pass an array. jQuery sends it as include[]=children&include[]=comments, which is the format the API expects:
$.ajax({
url: 'https://feedframer.com/api/v1/me',
dataType: 'json',
data: {
api_key: 'YOUR_API_KEY',
include: ['children', 'comments']
}
}).done(function (response) {
response.posts.forEach(function (post) {
console.log(post.id, post.children.length, post.comments.length);
});
});
See Filtering and sorting for date filters and sort.
With pagination
Each response has a pagination object with nextCursor and hasMore. To load the next page, send nextCursor back as page[cursor]. Hide the button when hasMore is false.
Free keys get one page: the 6 most recent posts, with hasMore: false. The button hides after the first load. The page size of 12 below is for Premium; Free keys get 6 whatever you ask for.
<div id="instagram-feed" class="instagram-feed"></div>
<button id="load-more" type="button" hidden>Load more</button>
let nextCursor = null;
function loadPosts() {
const data = {
api_key: 'YOUR_API_KEY',
'page[size]': 12 // Premium; Free keys always get 6
};
if (nextCursor) {
data['page[cursor]'] = nextCursor;
}
$('#load-more').prop('disabled', true);
$.ajax({
url: 'https://feedframer.com/api/v1/me',
dataType: 'json',
data: data
})
.done(function (response) {
renderPosts(response.posts);
nextCursor = response.pagination.nextCursor;
$('#load-more').prop('hidden', !response.pagination.hasMore);
})
.always(function () {
$('#load-more').prop('disabled', false);
});
}
$(function () {
loadPosts();
$('#load-more').on('click', loadPosts);
});
See Pagination for more on cursors.
Error handling
Every error response has the same JSON:API body:
{
"errors": [
{
"status": "401",
"code": "INVALID_API_KEY",
"title": "Authentication Failed",
"detail": "The provided API key is invalid."
}
]
}
jQuery parses it into xhr.responseJSON. Read code from the first error:
$.ajax({
url: 'https://feedframer.com/api/v1/me',
dataType: 'json',
data: { api_key: 'YOUR_API_KEY' }
})
.done(function (response) {
if (response.posts.length > 0) {
renderPosts(response.posts);
} else {
$('#instagram-feed').text('No posts yet.');
}
})
.fail(function (xhr) {
const error = (xhr.responseJSON && xhr.responseJSON.errors && xhr.responseJSON.errors[0]) || {};
let message = 'Could not load Instagram posts.';
if (xhr.status === 0) {
message = 'Could not reach Feedframer. Check your connection.';
} else if (error.code === 'MISSING_API_KEY' || error.code === 'INVALID_API_KEY') {
// The key is wrong, was regenerated, or its account was disconnected.
message = 'The Instagram feed is not configured correctly.';
} else if (error.code === 'VALIDATION_ERROR') {
message = 'Invalid parameter: ' + error.detail;
}
$('#instagram-feed').text(message);
console.error('Feedframer error', xhr.status, error.code, error.detail);
});
The Errors page lists every code.
Lightbox
This opens a simple lightbox when someone clicks a post. Video posts play in a <video> element with the thumbnail as the poster. Images use the largest resized copy on Premium, or mediaUrl on Free.
.lightbox {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
color: #fff;
}
.lightbox img,
.lightbox video {
max-width: 90vw;
max-height: 75vh;
}
.lightbox a {
color: #fff;
}
function renderGallery(posts) {
const $feed = $('#instagram-feed');
posts.forEach(function (post) {
const src = imageFor(post);
if (!src) {
return;
}
$('<img>', { src: src, alt: post.altText || 'Instagram post', loading: 'lazy' })
.css('cursor', 'pointer')
.on('click', function () {
openLightbox(post);
})
.appendTo($('<div>', { 'class': 'post' }).appendTo($feed));
});
}
function openLightbox(post) {
const $box = $('<div>', { id: 'lightbox', 'class': 'lightbox' });
if (post.mediaType === 'VIDEO') {
$('<video>', { src: post.mediaUrl, poster: post.thumbnailUrl, controls: true })
.appendTo($box);
} else {
$('<img>', { src: post.sizes ? post.sizes['1200'] : post.mediaUrl, alt: post.altText || '' })
.appendTo($box);
}
if (post.caption) {
$('<p>').text(post.caption).appendTo($box);
}
$('<a>', { href: post.permalink, target: '_blank', rel: 'noopener noreferrer' })
.text('View on Instagram')
.appendTo($box);
// Close on a click outside the media and link.
$box.on('click', function (event) {
if (event.target === this) {
$box.remove();
}
});
$('body').append($box);
}
Call renderGallery(response.posts) in place of renderPosts in any of the examples above.
AJAX settings
$.ajaxSetup sets defaults for every request on the page. A timeout stops the page waiting forever if the network stalls:
$.ajaxSetup({
timeout: 10000 // milliseconds
});
$.ajax({
url: 'https://feedframer.com/api/v1/me',
dataType: 'json',
data: { api_key: 'YOUR_API_KEY' },
beforeSend: function () {
$('#instagram-feed').text('Loading...');
}
})
.done(function (response) {
$('#instagram-feed').empty();
renderPosts(response.posts);
})
.fail(function (xhr, status) {
$('#instagram-feed').text(status === 'timeout' ? 'The request timed out.' : 'Could not load Instagram posts.');
});
Don't set cache: false. It adds a _=<timestamp> parameter to every request, which gives each request a unique URL and stops it being served from Feedframer's edge cache.
Best practices
-
Render captions as text. Use
.text()or$('<p>').text(caption). If you build HTML strings instead, escape the caption first:function escapeHtml(text) { return $('<div>').text(text).html(); } -
Use
thumbnailUrlfor video posts in an<img>.mediaUrlfor a video is usually an.mp4. -
Handle errors. Show visitors a short message and log the
codefor yourself. -
Let the browser lazy-load images with
loading="lazy". -
Check
hasMorebefore loading another page. Free keys only ever get one page.
Next steps
- The REST API reference lists every parameter
- Pagination explains cursors
- Errors lists every error code
- JavaScript examples show the same patterns without jQuery