Skip to content

02 Get your data in /Integrations

Instagram comments

Send comments on your Instagram professional account's posts to Dimension Labs with the universal API.

  1. IC Instagram commentsTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in the comments and replies on your own Instagram posts: each post as one conversation, with what the public wrote as customer messages and your caption and replies as your side. You run it on your side: a small script pulls from Meta’s Instagram API and posts each message to the universal API.

  • An Instagram professional account (Business or Creator). Meta’s Instagram API only serves professional accounts.

  • A Meta app with one of Meta’s three Instagram API setups, and an access token for your account:

    SetupHostTokenPermissions
    Instagram API with Facebook Login (account linked to a Facebook Page)graph.facebook.comFacebook Page access tokeninstagram_basic, instagram_manage_comments, pages_read_engagement
    Instagram API with Instagram Logingraph.instagram.comInstagram User access tokeninstagram_business_basic, instagram_business_manage_comments
    Instagram API with Unified Login (Meta’s recommendation for new integrations)graph.facebook.comBusiness Integration System User access tokeninstagram_basic, instagram_manage_comments

    If your role on the linked Page was granted through Business Manager, Meta also asks for ads_management or ads_read.

  • Standard Access is enough for an app that serves only accounts you own or manage. Advanced Access, through App Review, is for serving other people’s accounts.

  • A Dimension Labs API key for a new Universal API source. See Your API key.

Meta’s steps are in the Instagram Platform overview and Get started with Facebook Login.

  1. Create a Meta app and add the Instagram setup you picked. Sign in as a person who manages the account and grant the permissions above.
  2. With Facebook Login, find your Instagram account id from the linked Page: GET https://graph.facebook.com/v26.0/PAGE_ID?fields=instagram_business_account. The instagram_business_account.id is your IG User ID. A long-lived Page access token (see the Facebook Page comments guide) does not expire on a schedule.
  3. With Instagram Login, the long-lived Instagram User token lasts 60 days. Refresh it before then with GET https://graph.instagram.com/refresh_access_token?grant_type=ig_refresh_token&access_token=... (the token must be at least 24 hours old). Set IG_HOST=graph.instagram.com for the code below.
  4. Meta puts the API version in the path. This guide uses v26.0, the version Meta’s reference showed when it was checked. Pin a version and move it forward on purpose.
Terminal window
curl -G "https://graph.facebook.com/v26.0/IG_USER_ID/media" \
--data-urlencode "fields=id,caption,timestamp,media_type,permalink" \
--data-urlencode "since=1759276800" --data-urlencode "access_token=$IG_ACCESS_TOKEN"
curl -G "https://graph.facebook.com/v26.0/IG_MEDIA_ID/comments" \
--data-urlencode "fields=id,text,timestamp,from,user,parent_id,replies{id,text,timestamp,from,user,parent_id}" \
--data-urlencode "access_token=$IG_ACCESS_TOKEN"
  • /{ig-user-id}/media lists your posts and supports time-based pagination with since and until. Meta returns at most the 10,000 most recently created media, and no Stories (they have their own endpoint).
  • /{ig-media-id}/comments returns top-level comments only, newest first, at most 50 per query. Replies come only when you request the replies field. Meta says comments can’t be filtered by timestamp, so the code reads every comment on recent posts and skips the ones it has already sent.
  • Pagination: follow paging.next until it is missing, for comments and for each comment’s replies.
  • Your replies: Meta returns the user field on a comment only when your account wrote it. That is how the code tells your replies from the public’s.
  • Rate limits (Meta’s): Instagram calls count against Meta’s Business Use Case limits; a throttled call returns error code 80002. Meta strongly recommends webhooks for new comments to avoid rate limiting.

Storing the data. Meta’s Platform Terms apply to what you pull. They let you use a service provider such as Dimension Labs only under a written agreement that it processes the data for you and for no other purpose. They also ask you to delete Platform Data when you no longer need it for a legitimate business purpose, or when Meta or the person asks. You can’t delete individual records in Dimension Labs yourself: email support@dimensionlabs.io with the source and the comment ids for a targeted deletion.

Each post is one conversation. Your caption is the first, outgoing message; each comment and reply from the public is incoming; your account’s replies are outgoing, in the same session.

Dimension LabsInstagramNotes
textmedia caption, comment textSkip comments with no text.
userIdcomment from.idThe Instagram-scoped id of the commenter. Your account’s caption and replies use instagram-account-IG_USER_ID. Fall back to the sessionId.
incomingcomment userfalse when user is present (your account wrote it), true otherwise. The caption is always false.
sessionIdmedia idPrefix it, for example instagram-17895695668004550.
conversationIdmedia idSame value as sessionId.
dimensionlabs_timestamptimestampLike 2017-08-31T19:16:02+0000. Convert with toMillis. A comment within a second of its post is moved a second later.
platformJsonIG User ID, media id, media_type, comment id, parent_idparent_id ties a reply to the comment it answers.

Personal data. Don’t send from.username or username. Comments can hold names and handles: redact them before you send. See PII redaction.

// instagram-comments.mjs. Environment: IG_USER_ID, IG_ACCESS_TOKEN, DL_API_KEY.
// Optional: IG_HOST (default graph.facebook.com; graph.instagram.com for Instagram Login),
// IG_GRAPH_VERSION (default v26.0), IG_LOOKBACK_DAYS (default 30).
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000, DAY = 24 * HOUR;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const IG = process.env.IG_USER_ID;
const API = `https://${process.env.IG_HOST || 'graph.facebook.com'}/${process.env.IG_GRAPH_VERSION || 'v26.0'}`;
const THROTTLED = new Set([4, 17, 32, 613, 80002]); // Meta's rate limit error codes (80002 is Instagram's)
const FIELDS = 'id,text,timestamp,from,user,parent_id';
// Retries rate limits (429 or a throttle error code) and server errors (5xx), honoring Retry-After when it is sent.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const u = new URL(url);
if (!u.searchParams.has('access_token')) u.searchParams.set('access_token', process.env.IG_ACCESS_TOKEN);
const res = await fetch(u);
const body = await res.json().catch(() => ({}));
if (res.ok) return body;
const throttled = res.status === 429 || THROTTLED.has(body.error?.code);
if ((!throttled && res.status < 500) || attempt === 5) throw new Error(`Instagram returned ${res.status}: ${JSON.stringify(body)}`);
await sleep((Number(res.headers.get('retry-after')) || 30 * 2 ** attempt) * 1000);
}
}
// Follows paging.next until the last page.
async function* pages(url) {
while (url) {
const page = await get(url);
yield page.data ?? [];
url = page.paging?.next;
}
}
// `user` is returned only when your own account wrote the comment.
const mine = (c) => Boolean(c.user);
// Sends every caption, comment and reply on your media created at or after `since` (Unix ms). Returns the new `since`.
export async function syncInstagramComments({ since, historical = false }) {
let newest = since;
const from = historical ? since : since - Number(process.env.IG_LOOKBACK_DAYS || 30) * DAY; // comments arrive on older media too
const send = (msg) => sendToDimensionLabs(msg, { historical: historical || msg.dimensionlabs_timestamp < Date.now() - HOUR });
const mq = new URLSearchParams({ fields: 'id,caption,timestamp,media_type,permalink', since: String(Math.floor(from / 1000)) });
for await (const media of pages(`${API}/${IG}/media?${mq}`)) {
for (const m of media) {
const sessionId = `instagram-${m.id}`;
const mediaTs = toMillis(m.timestamp);
const base = { sessionId, conversationId: sessionId, platformJson: { igUserId: IG, mediaId: m.id, mediaType: m.media_type } };
if (m.caption?.trim() && mediaTs >= since) {
await send({ ...base, text: m.caption.trim(), userId: `instagram-account-${IG}`, incoming: false, dimensionlabs_timestamp: mediaTs,
platformJson: { ...base.platformJson, kind: 'caption' } });
newest = Math.max(newest, mediaTs);
}
const cq = new URLSearchParams({ fields: `${FIELDS},replies{${FIELDS}}` });
for await (const comments of pages(`${API}/${m.id}/comments?${cq}`)) {
for (const top of comments) {
// Replies come with the comment; if there are more than one page of them, follow the rest.
const replies = [...(top.replies?.data ?? [])];
for (let next = top.replies?.paging?.next; next;) { const p = await get(next); replies.push(...(p.data ?? [])); next = p.paging?.next; }
for (const c of [top, ...replies]) {
const created = toMillis(c.timestamp);
if (created < since || !c.text?.trim()) continue; // already sent, or no text
newest = Math.max(newest, created);
await send({ ...base, text: c.text.trim(), incoming: !mine(c),
userId: mine(c) ? `instagram-account-${IG}` : c.from?.id ? `instagram-${c.from.id}` : sessionId,
dimensionlabs_timestamp: Math.max(created, mediaTs + 1000),
platformJson: { ...base.platformJson, kind: c.parent_id ? 'reply' : 'comment', commentId: c.id, parentId: c.parent_id } });
}
}
}
}
}
return newest;
}

On every run the code reads the posts from the last 30 days (IG_LOOKBACK_DAYS) and all their comments, because people keep commenting on older posts. It sends only what was written since the last run. A busy account with a long lookback uses many calls: shorten the lookback, or move new comments to Meta’s webhooks.

Save the code as instagram-comments.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncInstagramComments as sync } from './instagram-comments.mjs';.

  • History first: node run.mjs 2025-01-01 sends every caption, comment and reply on posts since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new comments to /track, and they are enriched automatically every day. Anything more than an hour old when the job sees it goes to /trackhistorical, and needs a backfill.
  • Re-runs are safe: duplicates are removed automatically.

When Meta throttles the app, the code waits and tries again. If you have comments in a file instead, upload it and check it first with the file checker, then run a backfill.

Checked against Meta’s documentation on 2026-10-26.