# Instagram, Facebook & Threads Post Metrics Tracker (`sz022/social-post-metrics-tracker`) Actor

Track Instagram, Facebook and Threads post metrics over time. Put your post URLs on a schedule and every run returns likes, comments, shares, views and followers, plus the change since the last run and since the first. Export to CSV, Excel or JSON, or use the API.

- **URL**: https://apify.com/sz022/social-post-metrics-tracker.md
- **Developed by:** [CHEN-SHU-CHEN](https://apify.com/sz022) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 post trackeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Track **Instagram, Facebook and Threads post metrics over time**. Give it your campaign's post URLs once and put it on a **Schedule**: every run returns each post's **likes, comments, shares, views and followers**, next to the **change since the last run** and **since the first**.

A single export tells you how a post is doing today. This tells you how much it has grown since launch and since yesterday, without keeping old exports and subtracting them by hand. Instagram, Facebook and Threads can be mixed in one list, and every row has the same columns.

It tracks posts you already know about. If you need to *find* posts by hashtag, keyword or account, use a platform-specific scraper; this one takes URLs and nothing else.

### How to track Instagram, Facebook and Threads post metrics

1. Paste your post URLs into **Post URLs**. Instagram posts and reels, Threads posts and Facebook posts, reels and videos can be mixed.
2. Enter a **Tracker name**, one per campaign, such as `spring-launch`. It names the history this run reads and adds to.
3. Click **Start**. The first run is the baseline: every row has `snapshotCount: 1` and null change columns.
4. Open **Schedules** in Apify Console and create a schedule. Daily is enough for most campaigns; hourly is useful in the first day or two after a post goes up.
5. Add this Actor to the schedule with the **same input**, above all the same Tracker name.
6. Read the results in the run's dataset, export them to CSV, Excel or JSON, or fetch them through the Apify API. To change the list later, edit the input saved on the schedule; existing posts carry on and new ones start their own history.

### How the tracking history works

Each run files a snapshot of every post it reads in a key-value store named after your tracker, `post-tracker-{trackerName}`. Named storages on Apify are kept until you delete them, so the next run, a day or a month later, finds the history there and measures against it.

Use a different Tracker name per campaign. Two campaigns under one name share one history, and a post in both would have its growth counted from whichever run saw it first.

- **Adding a URL later** is fine. It starts its own history on the run that first sees it, and its change columns are null until its second reading.
- **Removing a URL** keeps its history. Add it back and the change is measured from its last reading, however long ago. `RUN_SUMMARY` lists tracked posts that were left out of the run, so a URL dropped by mistake does not go unnoticed.
- **The same post twice** in one list, say once with a share link's `?igsh=` and once without, is read and charged once, and gets a row for each line.

#### Unknown is `null`, never `0`, and so is the change

A post whose author hid its like count reports `likeCount: null`, and the change involving it is `null` too. A post that showed 500 likes today after hiding them yesterday did not gain 500 likes. Each metric stands on its own, so a hidden like count does not blank the change in views.

A count that goes down is reported as a negative change. People withdraw likes and platforms remove spam comments; that is real movement, not an error.

#### Deleted posts keep their last-known numbers

When a post disappears, its row comes back with `status: "not_found"` and the last values ever read for it, flagged with `valuesAreLastKnown: true`, `lastKnownAt` and `notFoundSince`. The change columns are null, because nothing was read to compare. The same applies to any row that could not be read this run: a login wall, a parse failure, or a URL left unprocessed when the run stopped early. To sum only fresh readings, filter on `valuesAreLastKnown`.

Every input URL still yields a row. 100 URLs in, 100 rows out.

### How much does it cost to track post metrics?

Pricing is pay per event. You pay for each post read on each run, and nothing for the platform usage on top:

| Event | Charged for | Price |
|---|---|---|
| `post-tracked` | Each Instagram or Threads post read, per run | $0.003 |
| `post-tracked-facebook` | Each Facebook post read, per run | $0.006 |
| `post-unavailable` | Each post that could not be read, per run | $0.0005 |

Facebook has its own price because it is the one platform that needs residential proxies, and its video pages are five times the size of a photo page. A flat rate would have Instagram and Threads subsidising it.

#### What is not charged?

Duplicates within one list, URLs left unprocessed when a run stops early, and unsupported URLs. If the platform restarts a run partway through, the posts it already finished are not read again, and their rows are not repeated.

A Facebook post that failed to load on 3 runs in a row is tried again once a day, not on every run; on the runs in between it comes back `not_processed` with its last-known values, uncharged. Within one run, after 5 Facebook posts in a row fail to load, the rest of that run's Facebook posts are skipped the same way, uncharged, whatever `abortAfterConsecutiveFailures` says; Instagram and Threads carry on.

A run also stops, leaving the rest of its posts `not_processed` and uncharged, if it is plainly costing more than it earns: for example, a proxy of your own that stalls every request, or a long list of Facebook posts that all turn out deleted. A list takes up to 5,000 URLs.

#### How much does a daily schedule cost?

A daily schedule over 50 posts is 50 events a day. A deleted post is reported, and charged as `post-unavailable`, on every run until you remove it from the list; it is fetched again only once every 7 days, in case it comes back, and on the runs in between its row comes from the tracker's record. If you set a maximum charge per run, the run checks it before reading each post and stops when the next post could not be paid for: posts read so far keep their snapshots, and the rest come back as `not_processed` rows with their last-known values, uncharged. If charging itself keeps failing, the run stops the same way rather than carry on unbilled.

### Input example

```json
{
  "postUrls": [
    "https://www.instagram.com/p/DeclSZmF_7I/",
    "https://www.threads.com/@nasa/post/Decc755lYhC"
  ],
  "trackerName": "my-campaign",
  "fetchFollowers": true,
  "writeHistoryDataset": false
}
```

| Field | Description |
|---|---|
| `postUrls` | Post URLs. Platforms can be mixed freely. |
| `trackerName` | Which history to read and add to. One per campaign, the same on every run. Letters, digits and hyphens, up to 40 characters. |
| `writeHistoryDataset` | Also append every reading to the dataset `post-tracker-{trackerName}-history`, one row per post per run. Off by default. |
| `maxSnapshotsPerPost` | How many readings of each post to keep. Default 1000. |
| `fetchFollowers` | Look up each author's follower count, so follower growth is tracked too. Costs one extra request per account. |
| `abortAfterConsecutiveFailures` | Stops the run when a platform stops responding. Deleted posts do not count toward this. |
| `proxyConfiguration` | For Instagram and Threads. Datacenter proxies are enough. Residential and Google SERP groups are not used here, since the price per post is set for datacenter traffic: a run that asks for them falls back to the datacenter proxy and says so in `RUN_SUMMARY`. Your own proxy URLs are used as given. |
| `facebookProxyConfiguration` | For Facebook only, which needs residential proxies. |

Accepted URL shapes: Instagram `/p/` and `/reel/`; Threads on `threads.com` or `threads.net`; Facebook `{pageId}_{postId}`, `permalink.php`, `/posts/pfbid…`, `/reel/`, `/videos/`, `/photo/?fbid=`.

### Output example

One row per input URL. This is a post on its second run:

```json
{
  "status": "success",
  "platform": "Instagram",
  "postUrl": "https://www.instagram.com/p/DeclSZmF_7I/",
  "username": "nasa",
  "mediaType": "carousel",
  "likeCount": 1918845,
  "likeCountDelta": 33,
  "likeCountSinceFirst": 33,
  "replyCount": 6013,
  "replyCountDelta": 0,
  "viewCount": null,
  "viewCountDelta": null,
  "observedAt": "2026-10-18T09:12:40.000Z",
  "previousAt": "2026-10-18T09:12:04.000Z",
  "hoursSincePrevious": 0.01,
  "snapshotCount": 2,
  "valuesAreLastKnown": false
}
```

`viewCount` is null because image and carousel posts have no plays, and so its change is null too.

| Field | Notes |
|---|---|
| `likeCount`, `replyCount`, `shareCount`, `viewCount`, `followerCount` | Current likes, comments, shares, views and followers. On a row that could not be read, the last-known values. |
| `likeCountDelta` … `followerCountDelta` | Change since the post's previous reading. Null on its first run, and whenever either reading is unknown. |
| `likeCountSinceFirst` … `followerCountSinceFirst` | Change since the post's first reading. Same null rules. |
| `observedAt` | When this run read the post. Null if it could not. |
| `previousAt`, `hoursSincePrevious` | When the previous reading was, and how long ago. A run that could not read the post leaves a longer gap than the schedule's interval. |
| `firstSeenAt`, `hoursSinceFirst` | When the first reading was, and how long ago. |
| `snapshotCount` | Readings of this post so far, this one included. |
| `valuesAreLastKnown`, `lastKnownAt` | `true` when the counts are from an earlier run, and when that was. |
| `notFoundSince` | When a deleted post was first found missing. |

The post's own fields are the same as in Instagram, Facebook, Threads & YouTube Post Metrics: `status`, `platform`, `postUrl`, `username`, `authorUrl`, `mediaType`, `createdAt`, `text`, `hashtags`, `mentions`. `repostCount` and `quoteCount` are kept for Threads but have no change columns of their own; `shareCount`, their sum, does.

The run's key-value store also holds `FAILED_URLS`, with the reason each failed URL could not be read, and `RUN_SUMMARY`, with counts, the names of the history store and dataset, and any tracked posts missing from this run's list.

#### History dataset for charts

With `writeHistoryDataset` on, every successful reading is appended to `post-tracker-{trackerName}-history` as `trackerName`, `postUrl`, `platform`, `username`, `observedAt` and the seven counts. A line chart of a post over time is a filter on `postUrl` with `observedAt` on the x-axis. Rows for posts that could not be read are not added: a gap in the line is honest, a flat stretch of copied values is not.

It starts from the run you turn it on, and it only grows. Delete it from **Storage** when the campaign is over.

### Other Actors for social media post metrics

| Actor | Use it when |
|---|---|
| [Instagram, Facebook, Threads & YouTube Post Metrics](https://apify.com/sz022/social-post-metrics) | You need the numbers once, for a report, without keeping a history. |
| [Instagram Engagement Rate Calculator & Audit](https://apify.com/sz022/instagram-threads-influencer-audit) | You are choosing creators and want each account's engagement rate, posting frequency and standout posts. |

### FAQ

#### What happens if two runs of one tracker start at once?

Both run, and both are charged for the posts they read: each is a reading you asked for. No reading is lost. Each run also keeps its own readings in a record of its own, which no other run writes, and adds to it after every post. If the two write a post's history at the same moment and one overwrites the other's reading, the next run puts that reading back, however long the schedule's gap is. A run's record is deleted only after two later checks, at least 72 hours apart, both find every reading in it filed in the history. Checks happen when a run starts, so a record lives for roughly 3 days plus up to two schedule intervals: about 4 days on a daily schedule, up to about 2–3 weeks on a weekly one. Each record holds only that run's readings, so the storage this takes stays small. Until the next run, a reading lost that way is missing only from the stored history, not from either run's output.

#### What if the platform restarts a run?

Apify sometimes moves a running Actor to another server. The tracker saves how far it got after every post, and starts no new post once the move is announced, so the restarted run carries on from where it stopped instead of from the top. Before going on, it checks the run's dataset: a row that went out just before the restart is not pushed again and its post is not read again, only its charge is completed. If that dataset cannot be read after three tries, the run stops and fails instead of guessing, so no post is reported or charged twice. Two narrow gaps remain. If the run died after charging a post but before saving that it had, the charge is sent again with the same key, which Apify ignores only within three minutes of the first. And if the history dataset is on, the one reading in flight at that moment can be missing from it; the stored history and the output row still have it.

#### What if a link carries a login token?

Links copied from an app or an admin tool sometimes carry an `access_token`, an `api_key`, a session id, a signature or a `user:password@` before the host. The post is read with the link as given, but that part of it is removed from everything the tracker keeps or shows: the rows, the log, the stored history and its index, and `FAILED_URLS`. Error messages that quote the link are cleaned the same way.

#### Is the change measured per day?

No, per reading. If a run fails to read a post, its next reading's change covers both intervals. `hoursSincePrevious` tells you how long that was.

#### Are share links and plain links the same post?

URLs that differ only in share parameters (`igsh`, `utm_*`, `fbclid` and the like), letter case in the host, `www.` or a trailing slash count as the same post. Two genuinely different forms of one Facebook post, a `/posts/pfbid…` link and the `permalink.php` link, are not recognised as the same and keep separate histories. Use one form per post.

#### How much history is kept?

Up to `maxSnapshotsPerPost` readings per post, 1000 by default, which is almost three years of daily runs. The first reading is always kept, so the change since the first stays exact; beyond the cap the oldest of the rest are dropped. `snapshotCount` keeps counting every reading.

#### Can it track reach, impressions or private accounts?

No. Reach and impressions are visible only to a page's own administrators, and no public scraper can produce them. Private accounts, comment text and posts behind a login are out of reach too.

#### Why does an Instagram follower count barely move?

On Instagram single-image posts the follower count comes from displayed text, rounded above ~10K, so small follower changes on large accounts do not show up there. Threads follower counts are exact. Facebook's are exact when the page's data carries the number, and otherwise fall back to the displayed text, which is rounded too ("2.8M followers").

A follower change between two readings can therefore partly reflect a switch between a rounded and an exact reading rather than real growth: from 2,800,000 (rounded) to 2,812,345 (exact) is not 12,345 new followers. Treat `followerCountDelta` on large Instagram and Facebook accounts as approximate.

#### Why is memory limited to 1 GB?

The Actor reads pages over plain HTTP and peaks at about 110 MB, so 1 GB is plenty. More memory would not make it faster, only more expensive, so runs are held between 512 MB and 1 GB.

#### What if Meta changes its pages?

Extraction depends on undocumented internal JSON, which Meta can change without notice. Failures are reported per URL, with the last-known values, rather than crashing the run. If you see a run of `parse_failed` rows, open an issue on this Actor.

# Actor input Schema

## `postUrls` (type: `array`):

Public Instagram post and reel URLs, Threads post URLs and Facebook post, reel and video URLs, mixed freely in one list. Add or remove URLs between runs: a new URL starts its own history, and a removed one keeps its history in case it comes back. See the README for accepted URL shapes. Up to 5,000 per run.

## `trackerName` (type: `string`):

Names the history this run reads and adds to, kept in a key-value store called post-tracker-{name}. Use one name per campaign so their histories never mix, and keep it the same on every run of the Schedule. Letters, digits and hyphens, up to 40 characters.

## `writeHistoryDataset` (type: `boolean`):

Append every successful reading to a dataset called post-tracker-{name}-history, one row per post per run, so a chart of a post over time can be drawn from a single table. Off by default: the history itself is kept either way, and this dataset only grows.

## `maxSnapshotsPerPost` (type: `integer`):

How many readings of each post to keep. The first reading is always kept, so change since the first stays exact; the oldest of the rest are dropped beyond this. 1000 is almost three years of daily runs.

## `fetchFollowers` (type: `boolean`):

Look up the author's follower count, so follower growth is tracked too. This costs one extra request per account, so turn it off if you only need post metrics.

## `abortAfterConsecutiveFailures` (type: `integer`):

Ends the run early when a platform stops responding, so a broken run cannot burn through your whole list. Deleted posts do not count. Set to 0 to never stop early.

## `debugSaveHtml` (type: `boolean`):

Store the raw response of failed URLs in the run's key-value store. For diagnosing why a proxy or platform change broke extraction.

## `proxyConfiguration` (type: `object`):

For Instagram and Threads, which Apify's datacenter proxy reads fine. Residential and Google SERP proxy groups are not used here: the price per post is set for datacenter traffic, so a run that asks for them falls back to the datacenter proxy and says so in RUN_SUMMARY. Proxy URLs of your own are used as given.

## `facebookProxyConfiguration` (type: `object`):

Facebook serves a login wall to datacenter IPs, so it needs residential proxies. This only applies to Facebook URLs, so Instagram and Threads in the same run stay on the cheap proxy.

## Actor input object example

```json
{
  "postUrls": [
    "https://www.instagram.com/p/DeclSZmF_7I/",
    "https://www.threads.com/@nasa/post/Decc755lYhC"
  ],
  "trackerName": "my-campaign",
  "writeHistoryDataset": false,
  "maxSnapshotsPerPost": 1000,
  "fetchFollowers": true,
  "abortAfterConsecutiveFailures": 5,
  "debugSaveHtml": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "facebookProxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `metrics` (type: `string`):

Likes, comments, shares, views and followers per post, each with the change since the last run and since the first.

## `failedUrls` (type: `string`):

The subset of input URLs that returned no fresh metrics this run, with the reason for each. Also present in the dataset itself.

## `runSummary` (type: `string`):

Counts for this run, the names of the tracker's history store and history dataset, and any tracked posts that were left out of this run's list.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "postUrls": [
        "https://www.instagram.com/p/DeclSZmF_7I/",
        "https://www.threads.com/@nasa/post/Decc755lYhC"
    ],
    "trackerName": "my-campaign"
};

// Run the Actor and wait for it to finish
const run = await client.actor("sz022/social-post-metrics-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "postUrls": [
        "https://www.instagram.com/p/DeclSZmF_7I/",
        "https://www.threads.com/@nasa/post/Decc755lYhC",
    ],
    "trackerName": "my-campaign",
}

# Run the Actor and wait for it to finish
run = client.actor("sz022/social-post-metrics-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "postUrls": [
    "https://www.instagram.com/p/DeclSZmF_7I/",
    "https://www.threads.com/@nasa/post/Decc755lYhC"
  ],
  "trackerName": "my-campaign"
}' |
apify call sz022/social-post-metrics-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sz022/social-post-metrics-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/CFPrp4ED2hsrtS5iY/builds/DgIQXeeeWQxc4Toij/openapi.json
