Import Instagram Followers into Google Sheets Automatically

Import Instagram followers into Google Sheets with a short Apps Script that logs one dated row per account per day, plus TikTok, YouTube and X counts.

By Scrape Everything team · · 12 min read

Last updated: November 3, 2026 · By the Scrape Everything team

TL;DR: To import Instagram followers into Google Sheets today, skip IMPORTXML and use a short Google Apps Script. It calls POST /v1/instagram/profile with UrlFetchApp, reads follower_count from the JSON and appends one dated row per account. A daily trigger turns that into a follower history, and the same script handles TikTok, YouTube and X.

This post is about follower counts for public accounts, logged over time: yours, your clients', your competitors'. It doesn't export the list of usernames that follow an account, and it doesn't pull Insights (reach, impressions, demographics) from your own Business account. Both are covered briefly further down, so you know where to go instead.

If you came here because =IMPORTXML("https://www.instagram.com/natgeo/", ...) returns #N/A, here's why: Google's fetcher usually gets Instagram's login page, so the XPath has nothing to match. What follows is the replacement: a two-tab sheet, a script of about 50 lines, a daily trigger, and an n8n version for teams that don't want to touch Apps Script. For the API side across nine platforms, see our follower count API overview.

How do I import Instagram followers into Google Sheets?

Call an API from Apps Script and write the number into a row. Apps Script is the JavaScript runtime built into Google Sheets (Extensions > Apps Script), and its UrlFetchApp service can send an authenticated POST to any HTTPS endpoint.

Scrape Everything is a REST API that returns public social media data as clean JSON. Every endpoint is a POST to https://api.scrapeeverything.com/v1/<platform>/<action> with a JSON body and an Authorization: Bearer ssk_live_... header. The Instagram profile endpoint takes one parameter, username, which accepts a bare name, an @handle or a full profile URL, so you can paste whatever format your client sent.

Test it from a terminal before opening the script editor:

bash
curl -X POST https://api.scrapeeverything.com/v1/instagram/profile \
  -H "Authorization: Bearer $SCRAPEEVERYTHING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username": "example_user"}'

Trimmed response (placeholder values):

json
{
  "success": true,
  "data": {
    "id": "1234567890",
    "username": "example_user",
    "full_name": "Example User",
    "is_private": false,
    "is_verified": false,
    "follower_count": 48213,
    "following_count": 512,
    "post_count": 731
  },
  "meta": {
    "request_id": "req_XXXXXXXXXXXXXXXXXXXX",
    "scraper": "instagram.profile",
    "cached": false,
    "credits_charged": 1,
    "credits_remaining": 99
  }
}

Same thing in Node 18+, against the TikTok endpoint, if you'd rather check field names in code:

javascript
// node check.mjs
const res = await fetch('https://api.scrapeeverything.com/v1/tiktok/profile', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SCRAPEEVERYTHING_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ username: 'example_user' }),
});
const { success, data, code } = await res.json();
console.log(success ? [data.follower_count, data.like_count, data.video_count] : code);

One lookup costs 1 credit. No Instagram login, no Meta app review. The Instagram profile scraper page lists every field.

Why did IMPORTXML stop working for Instagram followers?

IMPORTXML fetches the raw HTML of a URL from Google's servers and runs an XPath query on it. Google's documentation describes it as importing data from structured formats like XML, HTML, CSV and TSV. It doesn't run JavaScript and it doesn't log in.

Instagram profile pages are a JavaScript app. The follower count has no stable spot in the initial HTML, and anonymous requests from data-center IPs (which is what Google's fetcher is) often get a login page instead of the profile. So the XPath from an old tutorial matches nothing. Pulling the og:description meta tag with REGEXEXTRACT fails the same way, and even when it worked it gave you "48K Followers", a rounded string you can't chart.

My view: stop fighting the HTML. Any formula that scrapes a social page from inside Sheets breaks again the next time the markup changes, and you find out on the morning the client report is due.

Which way should I get Instagram follower counts into Google Sheets?

There are four routes people actually use. They differ mostly in which accounts they can see and how much setup they need.

MethodWorks on any public accountNeeds your own Instagram/Meta setupDated historyBreaks when Instagram changes HTMLCost
IMPORTXML or og:description formulaNo, returns #N/A or the login pageNoNo, overwritesYesFree, but doesn't work
Instagram Graph API Business Discovery, via Apps ScriptOnly professional (Business/Creator) accounts, not age-gated onesYes: your own professional account, a Meta app, a user access tokenOnly if you script itNoFree, plus setup and token upkeep
Sheets add-on (API Connector, Porter Metrics and similar)Varies by add-onOften yesUsually, on a scheduleNoSubscription, see vendor
Apps Script + Scrape Everything (this post)Yes, plus TikTok, YouTube, X and more in the same sheetNoYes, one row per dayNo, we maintain the scraper1 credit per lookup, packs from $39

Business Discovery is Meta's official answer and returns followers_count and media_count for other professional accounts. It's the right tool if every account you track is a business or creator account and you already run a Meta app. It can't see personal accounts. Our Graph API vs scraper API comparison goes through the trade-offs in detail.

If the account is your own and you want Insights (reach, impressions, audience demographics), use the Graph API or an add-on built on it. That data isn't public, so we don't return it.

How do I store the API key safely in Apps Script?

Put it in Script Properties, not in a cell and not in the code. Anyone with edit access to the sheet can read cells, and code gets copied into other sheets.

  1. Open your sheet and go to Extensions > Apps Script.
  2. Click the gear icon (Project Settings) in the left sidebar.
  3. Under Script Properties, click Add script property.
  4. Name it SCRAPEEVERYTHING_API_KEY and paste your ssk_live_... key as the value.
  5. Save.

Editors of the script project can still see Script Properties, so give script access only to people you'd trust with the key, and rotate it from your dashboard when someone leaves. The authentication docs cover key format and revocation.

What does the full follower tracker script look like?

Create two tabs. Handles, with a header row:

platformhandle
instagramnatgeo
tiktok@duolingo
youtube@mkbhd
xNASA

And Snapshots, with the header date | platform | handle | followers | following | posts | error.

Paste this into Code.gs:

javascript
const API = 'https://api.scrapeeverything.com/v1/';
// platform -> [followers, following, posts] field names in data
const FIELDS = {
  instagram: ['follower_count', 'following_count', 'post_count'],
  tiktok: ['follower_count', 'following_count', 'video_count'],
  youtube: ['subscriber_count', null, 'video_count'],
  x: ['follower_count', 'following_count', 'tweet_count'],
};
const BATCH = 5; // default limit: 5 concurrent requests
const HOURS = [9]; // add 10 for a second pass on long lists

function snapshotFollowers() {
  const start = Date.now();
  const key = PropertiesService.getScriptProperties().getProperty('SCRAPEEVERYTHING_API_KEY');
  const ss = SpreadsheetApp.getActive();
  const tz = ss.getSpreadsheetTimeZone();
  const today = Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd');
  const out = ss.getSheetByName('Snapshots');
  const asDay = (v) => (v instanceof Date ? Utilities.formatDate(v, tz, 'yyyy-MM-dd') : String(v));
  // handles that already have a good row today are skipped, so reruns cost 0 credits
  const done = new Set(out.getDataRange().getValues()
    .filter((r) => asDay(r[0]) === today && r[6] === '')
    .map((r) => r[1] + '|' + r[2]));
  const todo = ss.getSheetByName('Handles').getDataRange().getValues().slice(1)
    .map((r) => [String(r[0]).trim().toLowerCase(), String(r[1]).trim()])
    .filter(([p, h]) => FIELDS[p] && h && !done.has(p + '|' + h));

  for (let i = 0; i < todo.length; i += BATCH) {
    if (Date.now() - start > 5 * 60 * 1000) break; // stay under the 6-minute limit
    const chunk = todo.slice(i, i + BATCH);
    const responses = UrlFetchApp.fetchAll(chunk.map(([p, h]) => ({
      url: API + p + '/profile',
      method: 'post',
      contentType: 'application/json',
      headers: { Authorization: 'Bearer ' + key },
      payload: JSON.stringify({ username: h }),
      muteHttpExceptions: true,
    })));
    const rows = responses.map((res, j) => {
      const [p, h] = chunk[j];
      let body;
      try { body = JSON.parse(res.getContentText()); } catch (e) { body = { code: 'HTTP_' + res.getResponseCode() }; }
      if (!body.success) return [today, p, h, '', '', '', body.code || 'ERROR'];
      return [today, p, h, ...FIELDS[p].map((f) => (f ? body.data[f] : '')), ''];
    });
    out.getRange(out.getLastRow() + 1, 1, rows.length, 7).setValues(rows);
    Utilities.sleep(5000); // keeps a run under 60 requests per minute
  }
}

function installTrigger() {
  // remove old triggers first, so running this twice doesn't double up
  ScriptApp.getProjectTriggers()
    .filter((t) => t.getHandlerFunction() === 'snapshotFollowers')
    .forEach((t) => ScriptApp.deleteTrigger(t));
  HOURS.forEach((h) => ScriptApp.newTrigger('snapshotFollowers').timeBased().everyDays(1).atHour(h).create());
}

The done set makes the script idempotent. Press Run twice, or let a trigger fire twice, and handles that already have a clean row for today are skipped at no cost. Rows with an error code don't count as done, so the next run retries them.

muteHttpExceptions: true turns a 404 or 429 into a normal response instead of an exception that kills the whole batch. The error column then shows the API's stable code: NOT_FOUND for a handle that doesn't exist (or was renamed), RATE_LIMITED if you went too fast, INSUFFICIENT_CREDITS if the balance ran out. A NOT_FOUND lookup is charged, because it's a definitive answer; blocks, timeouts and server errors are refunded automatically. So delete dead handles instead of letting them retry daily, and add a conditional-format rule (red fill when column G isn't empty) so renamed accounts jump out. The errors reference lists every code.

YouTube has no public "following" number, so that column stays empty. And subscriber_count is parsed from YouTube's rounded public text (for example "521M"), which shows up in charts. More on that below.

Why a trigger and not a custom function like =FOLLOWERS("natgeo")? Custom functions recalculate whenever Sheets decides to, so every recalculation would spend credits, and they're capped at 30 seconds per call. To show the latest count next to a client's name, read it from Snapshots with a plain formula instead:

Code
=INDEX(SORT(FILTER(Snapshots!A:D, Snapshots!B:B="instagram", Snapshots!C:C="natgeo", Snapshots!G:G=""), 1, FALSE), 1, 4)

Get 100 free credits (no payment details needed), paste the key into Script Properties, keep three or four handles in the tab and run snapshotFollowers once from the editor.

How do I run the tracker daily without hitting Apps Script limits?

Run installTrigger once and approve the permission prompt. You can confirm it worked under Triggers in the editor's left sidebar. From then on snapshotFollowers runs every day.

Three Google limits shape the design. They come from the Apps Script quotas page, so check it for your account type:

  1. A single execution stops after 6 minutes. The script checks the clock and exits after 5, leaving remaining handles for the next run.
  2. atHour(9) doesn't mean 09:00 sharp. Google fires the trigger somewhere within that hour, in the script's time zone.
  3. Triggers have a daily total runtime cap and UrlFetchApp a daily call cap, both lower on free Gmail accounts than on Google Workspace (at the time of writing, 90 minutes and 20,000 calls a day for consumer accounts).

On our side, every account gets 60 requests per minute and 5 concurrent until purchases total $39 (a Builder pack or larger does it at once), then 300 per minute and 20 concurrent. That's why the script sends batches of 5 with fetchAll and sleeps 5 seconds: at most 12 batches a minute, never more than 60 requests. Once you're on the higher limit, set BATCH = 20 and shorten the sleep. Details on the rate limits page.

The sleep alone caps one run at roughly 300 handles, and real response times make it fewer. For a bigger Handles tab, set HOURS = [9, 10] and rerun installTrigger. Thanks to the done set, the second pass only picks up what the first missed and costs nothing when there's nothing left.

Once a day is plenty. Follower counts move slowly, and hourly runs multiply the bill by 24 without telling you much more.

If another sheet or script looks up the same handles on the same day, add "cache_days": 1 to its request body. A cached result costs 0 credits and comes back with meta.cached: true. Leave it off the scheduled run itself, or you might log yesterday's number as today's. The caching docs explain the window.

How many credits does a daily follower tracker use?

Each profile lookup is 1 credit, one per handle per day. Monthly credits = accounts x platforms x 30.

AccountsPlatforms eachLookups per dayCredits per 30 daysAt Builder rate ($1.56 per 1k)At Growth rate ($1.19 per 1k)
10330900$1.40$1.07
2541003,000$4.68$3.57
5031504,500$7.02$5.36
100440012,000$18.72$14.28
25041,00030,000$46.80$35.70

A worked example: a freelancer tracks 12 clients on Instagram, TikTok and YouTube. That's 36 lookups a day, 1,080 credits per 30 days. The 100 free credits cover the first two days of testing. After that, a Builder pack ($39 for 25,000 credits) runs the sheet for about 23 months, around $1.68 a month, and the same purchase moves the account to 300 requests per minute and 20 concurrent.

Packs are one-time purchases and credits never expire. Growth ($119 / 100,000) and Scale ($449 / 500,000) bring the per-1k price down to $1.19 and $0.90. Every pack is on the pricing page, and the credit calculator takes your own numbers.

Is there a no-code way to track followers in Google Sheets with n8n?

Yes. If your team already runs n8n, the same tracker is five nodes and no script. This is the "n8n google sheets daily stats" pattern:

  1. Schedule Trigger: every day at 09:00.
  2. Google Sheets (Get Row(s)): read the Handles tab. Each row becomes one item.
  3. Scrape Everything node: pick the platform's profile action and map username to the handle column. For a mixed tab, put a Switch node on platform first and give each branch its own node. On n8n Cloud, or any instance that can't install community nodes, use an HTTP Request node with the same POST and Bearer header.
  4. Edit Fields (Set): map today's date, platform, handle and the count field (follower_count, or subscriber_count for YouTube).
  5. Google Sheets (Append Row): write to the Snapshots tab.

On a self-hosted instance, install n8n-nodes-scrapeeverything from Settings > Community Nodes and add a Scrape Everything API credential. The n8n integration page has the details.

Pick Apps Script if the sheet is the product and one person owns it. Pick n8n if the counts also need to reach Slack, a database or a client dashboard.

How do I chart follower growth in Google Sheets?

Add a Growth tab and pivot the snapshots with QUERY. This gives one row per date and one column per Instagram handle:

Code
=QUERY(Snapshots!A:G, "select A, max(D) where B = 'instagram' and D is not null group by A pivot C", 1)

The D is not null filter drops error rows, since the script leaves the followers cell empty when a lookup fails. Select the result, then Insert > Chart and pick a line chart. Repeat with 'tiktok', 'youtube' or 'x'. Mixing platforms in one chart rarely reads well because the scales differ by orders of magnitude.

Seven-day growth for one account:

Code
=SUMIFS(Snapshots!D:D, Snapshots!B:B, "instagram", Snapshots!C:C, "natgeo", Snapshots!A:A, TODAY())
 - SUMIFS(Snapshots!D:D, Snapshots!B:B, "instagram", Snapshots!C:C, "natgeo", Snapshots!A:A, TODAY()-7)

This works because Sheets converts the yyyy-MM-dd strings the script writes into real dates. If you format column A as plain text, the comparison with TODAY() returns zero. Divide by the second SUMIFS for a percentage. A missing day (renamed account, empty balance) shows up as a fake jump, so glance at the error column before the weekly report goes out.

About YouTube: the public subscriber count is abbreviated, and the official YouTube Data API also rounds subscriberCount to three significant figures. A large channel's line looks like a staircase, flat for weeks and then a step up. That's YouTube's rounding. The YouTube scraper hub lists the endpoint.

FAQ

Can I import the list of someone's Instagram followers into Google Sheets?

Not with this setup. The API returns the follower count and public profile fields, not the usernames of the people who follow an account. Follower lists sit behind a login, and they're where Instagram's terms and personal-data rules apply hardest. Our post on whether scraping public social media data is legal covers the general picture.

Does this work for private Instagram accounts?

Partly. A private account returns its public profile fields with is_private: true. Counts come back when Instagram shows them to logged-out visitors and are null when it doesn't. Nothing that needs a login is available. Scrape Everything reads public data only and isn't affiliated with Instagram, TikTok, YouTube or X.

How do I get TikTok follower counts in Google Sheets?

Add a row with tiktok in the platform column and the handle next to it. The script calls POST /v1/tiktok/profile and writes follower_count, following_count and video_count at 1 credit per lookup. The TikTok profile endpoint also returns like_count, total likes received, if you want an extra column.

Is the YouTube subscriber count in Google Sheets exact?

No. YouTube shows rounded counts publicly, so subscriber_count is parsed from text like "521M". To keep the raw string too, replace null in FIELDS.youtube with 'subscriber_count_text'; it lands in the otherwise empty "following" column.

Can I track Facebook or LinkedIn followers the same way?

Facebook Pages, yes: add facebook: ['follower_count', null, null] to FIELDS and the script calls facebook.profile (Facebook scraper hub). Pinterest (pinterest.profile, follower_count) and Telegram channels (telegram.profile, subscriber_count) work the same way, and so does the X profile endpoint already in the script. Twitch uses twitch.channel instead of profile, so you'd also change the URL the script builds. LinkedIn, Threads, Reddit and Snapchat aren't covered.

Can I use a custom function like =INSTAGRAMFOLLOWERS(A2)?

You can write one, but it's a bad fit. Sheets recalculates custom functions on its own schedule, each recalculation would cost a credit, and each call has a 30-second limit. Let the daily trigger fetch the data and use the INDEX(SORT(FILTER(...))) formula above to display the latest value.

More from the blog

Start with 100 free credits.

Continue with Google, create an API key and send your first request in under a minute.

Get 100 free credits