# Social Media Analytics API: Post & Account Metrics

import { TechArticle } from "../components/TechArticle";

<TechArticle slug="analytics" fm={frontmatter} />

Read the performance of everything you publish: per post, per platform, per account, and as a workspace roll-up. The API also computes recommended posting times from your own history.

All analytics endpoints require the `analytics:read` scope on your API key. Every endpoint reads stored snapshots from the OmniSocials database. No endpoint calls the social platforms live, so responses are fast and never consume platform rate limits.

| Endpoint | What it returns |
|----------|-----------------|
| `GET /analytics/posts/:id` | Latest per-platform metrics for one post |
| `GET /analytics/posts?ids=` | Bulk version: up to 100 posts in one call |
| `GET /analytics/overview` | Workspace totals, engagement rate, platform breakdown |
| `GET /analytics/accounts` | Account-level snapshots (followers, profile views, and more) |
| `GET /analytics/best-times` | Recommended posting slots per platform |

## How metrics are collected

OmniSocials collects metrics on a schedule, starting right after a post publishes:

| Post age | Refresh interval |
|----------|------------------|
| 0-7 days | Every 2 hours |
| 8-30 days | Every 24 hours |
| 31-365 days | Every 7 days |
| Stories | Every hour for the story's 24-hour lifetime |

Two consequences:

1. A post published minutes ago can legitimately report zeros. The platforms themselves need time to count views, so check back after the first refresh window.
2. Metrics for a 6-month-old post can be up to a week old. `collected_at` on every response tells you exactly when the numbers were captured.

Account-level snapshots (followers, profile views) are collected once per day.

## Get analytics for one post

Use the numeric OmniSocials post id (the `id` field from `GET /posts`), not a platform post id such as a tweet id or YouTube video id. A platform id returns `400 invalid_post_id`.

```bash
curl https://api.omnisocials.com/v1/analytics/posts/1024 \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

Response:

```json
{
  "data": {
    "post_id": "1024",
    "platforms": {
      "instagram": {
        "platform": "instagram",
        "platform_post_id": "17895695668004550",
        "metrics": {
          "likes": 214,
          "comments": 12,
          "shares": 9,
          "impressions": 4880,
          "reach": 3990,
          "engagement": 235
        },
        "thread_parts": 1,
        "collected_at": "2026-08-19T14:00:11Z"
      },
      "linkedin": {
        "platform": "linkedin",
        "platform_post_id": "urn:li:share:7231...",
        "metrics": { "likes": 88, "comments": 6, "impressions": 2143 },
        "thread_parts": 1,
        "collected_at": "2026-08-19T14:00:12Z"
      }
    }
  }
}
```

One entry per platform the post went to. The metric fields inside `metrics` are platform-specific, stored as the platform reports them. A missing key means the platform does not report it; a `0` is a measured zero. A post with no collected analytics yet returns an empty `platforms` object, not a 404.

### Metric keys by platform

| Platform | Keys you can expect |
|----------|---------------------|
| Instagram | `views`, `reach`, `likes`, `comments`, `saves`, `shares`, `reposts`, `engagement`, `profile_visits`, `follows`, `profile_activity` + `profile_activity_breakdown`. Reels add `avg_watch_time`, `total_watch_time`, `watch_time_percentage`, `skip_rate`, `duration`. Stories add `replies`, `navigation` + `navigation_breakdown`, `link_clicks`, `completion_analysis`. |
| Facebook | `views`, `reach`, `likes`, `total_reactions`, `reactions` (by type, including `care`), `comments`, `shares`, `clicks`, `clicks_breakdown`, `video_views`, `avg_watch_time`, `total_watch_time`. Reels add `replays`, `follows`. |
| LinkedIn profile | `impressions`, `reach`, `likes`, `reactions` (by type), `comments`, `top_level_comments`, `shares`, `saves`, `sends`, `clicks`, `follows`, `profile_visits`. |
| LinkedIn page | `impressions`, `unique_impressions`, `likes`, `reactions`, `comments`, `shares`, `clicks`, `engagement_rate`, plus video keys for video posts. |
| YouTube | `views`, `likes`, `dislikes`, `comments`, `duration`. With the Analytics API enabled: `shares`, `engaged_views`, `estimated_minutes_watched`, `average_view_duration`, `average_view_percentage`, `subscribers_gained`, `subscribers_lost`, `playlist_adds`, `traffic_sources`. |
| TikTok | `views`, `likes`, `comments`, `shares`, `duration`, `share_url`. With the Business API authorization: `average_time_watched`, `full_video_watched_rate`, `completion_rate`, `watch_time_percentage`, `total_time_watched`, `favorites`, `reach`, `follows`, `profile_visits`, `impression_sources`, `audience_types`, `audience_countries` (see [TikTok watch-depth analytics](/platforms/tiktok#watch-depth-analytics)). |
| X | `views`, `likes`, `comments`, `reposts`, `quotes`, `bookmarks`. For posts under 30 days old: `link_clicks`, `profile_visits`, `engagement`. Video posts add `video_views`, `completion_rate`, `duration`. |
| Pinterest | `impressions`, `saves`, `pin_clicks`, `outbound_clicks`, `likes`, `comments`, `profile_visits`, `follows`, `save_rate`, `pin_click_rate`, `outbound_click_rate`, `engagement`. Video pins add `video_views`, `video_starts`, `completion_rate`, `avg_watch_time`, `total_watch_time`. |
| Threads | `views`, `likes`, `replies`, `reposts`, `quotes`, `shares`, `engagement`. |
| Bluesky | `likes`, `comments`, `reposts`, `quotes`, `bookmarks`. Bluesky exposes no views. |
| Mastodon | `likes`, `comments`, `reposts`, `quotes`, `poll` (votes per option), `tags`. Mastodon exposes no views. |
| Google Business | No per-post metrics (Google discontinued them in 2023). The entry carries `state`, `search_url`, `metrics_unavailable: true` and a `note`. |

Durations are in seconds, rates in percent (0 to 100). Breakdown objects (`reactions`, `traffic_sources`, `impression_sources`, ...) map a category to a count or a percentage.

### Threads count as one post

On X, Bluesky, Mastodon, and Threads a post can publish as a thread of several parts. The API sums each part's metrics into the platform entry, so you get the thread's true total (the same number the OmniSocials dashboard shows). `thread_parts` tells you how many parts were summed, 1 for a normal post, and `platform_post_id` is always the thread's root part.

Multi-slide stories work the same way: each slide is its own story on Instagram or Facebook, so the platform entry sums the slides and `thread_parts` is the number of slides with stats. The entry also carries a `story_slides` array with each slide's own metrics in publish order:

```json
"instagram": {
  "platform": "instagram",
  "platform_post_id": "1789...",
  "metrics": { "views": 910, "reach": 620, "replies": 3, "content_type": "story" },
  "thread_parts": 2,
  "story_slides": [
    { "index": 0, "platform_post_id": "1789...", "metrics": { "views": 540, "reach": 400, "replies": 2 }, "collected_at": "2026-08-21T12:00:00Z" },
    { "index": 1, "platform_post_id": "1790...", "metrics": { "views": 370, "reach": 220, "replies": 1 }, "collected_at": "2026-08-21T12:00:00Z" }
  ],
  "collected_at": "2026-08-21T12:00:00Z"
}
```

`story_slides` is absent on single-slide stories and on every other post type.

## Bulk: up to 100 posts per call

One request to `GET /analytics/posts` replaces up to 100 single-post calls. It exists so a nightly sync doesn't burn the [100 requests/minute rate limit](/rate-limits-and-errors).

```bash
curl "https://api.omnisocials.com/v1/analytics/posts?ids=1024,1025,1026" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

The response `data` array returns one entry per requested id, in request order. Ids with no analytics yet come back with an empty `platforms` object rather than being omitted, so you can map results 1:1 to what you sent. More than 100 ids returns `400 too_many_ids`; page through larger sets in batches.

## Workspace overview

Roll-up across every platform for a time window:

```bash
curl "https://api.omnisocials.com/v1/analytics/overview?period=30d" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

```json
{
  "data": {
    "total_posts": 42,
    "total_platforms": 5,
    "total_engagement": 3811,
    "total_impressions": 92150,
    "average_engagement_rate": 4.13,
    "top_performing_platform": "instagram",
    "platform_breakdown": {
      "instagram": {
        "posts": 18,
        "total_engagement": 2410,
        "total_impressions": 61200,
        "average_engagement": 133.9,
        "engagement_rate": 3.94
      }
    }
  },
  "period": "30d",
  "start_date": "2026-07-21",
  "end_date": "2026-08-20",
  "current_date": "2026-08-20"
}
```

Pick the window one of two ways:

- `period`: `7d`, `30d` (default), or `90d`.
- `start_date` + `end_date`: accepts `YYYY-MM-DD`, a `YYYY-MM` month shorthand (`2026-04` expands to the whole month), or a full ISO 8601 datetime.

The response echoes the resolved `start_date`, `end_date`, and the server's `current_date`, so a client (or an AI agent) can verify what was actually queried instead of guessing.

## Account analytics

Daily snapshots of account-level metrics for every connected account. Each snapshot holds stock values (`followers`, `following`, `posts`, and platform extras such as Pinterest `monthly_views`, YouTube `total_views`, X `listed`, Google Business `average_rating` and `review_count`) and day values for the snapshot date (`impressions`, `reach`, `engagement`, `likes`, `comments`, `shares`, `profile_views`, `link_clicks`, `follows_gained`, `follows_lost`, and platform extras such as Google Business `calls`, `direction_requests`, `website_clicks`). Rows with day values carry `period: "daily"`.

Audience data, when the platform provides it: `demographics` (age, gender, country, city on Instagram, Threads and TikTok, which need 100 or more followers; function, seniority, industry and company size on LinkedIn pages, no minimum) with `demographics_unit`, and `online_followers` (UTC hour to followers online) for Instagram and TikTok Business accounts.

```bash
curl "https://api.omnisocials.com/v1/analytics/accounts?platform=instagram" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

`date` (`YYYY-MM-DD`) returns the latest snapshot on or before that date; it defaults to today. To measure growth over a window, diff two snapshots. For example, compare `date=2026-08-01` against `date=2026-07-01`.

### Metric semantics vary by platform

The scope of an account metric is platform-defined, and it differs. Where the semantics are non-obvious, the `metrics` object carries a `note` field explaining them. The important ones:

- **LinkedIn profile impressions.** Profiles keep a lifetime cumulative total under `impressions_lifetime` (all of the account's content, including posts published outside OmniSocials). When LinkedIn serves a daily breakdown, `impressions` is that day's value and the row carries `period: "daily"`; otherwise it carries `period: "lifetime"`. Pages always report day values.
- **Instagram and Threads day values** come from the platform insights for that date. A missing field means "not reported by the platform for this account", not zero. Accounts under 100 followers get no demographics or audience-online data.
- **YouTube and TikTok day values** need the YouTube Analytics API scope, or the TikTok Business authorization. Without them the snapshot holds the stock values only.
- **Google Business Profile has no follower concept**, and Google deprecated per-post insights. Account rows hold a 30-day `daily` series (impressions split by surface in `impressions_breakdown`, calls, direction requests, website clicks, bookings), `average_rating`, `review_count`, and monthly `search_keywords`. Google's data lags 2 to 3 days; `data_date` says which day the top-level values describe.

## Best posting times

Recommended day/hour slots for a platform, computed from your workspace's own posting history: publish time crossed with engagement for every analyzed post, recency-weighted and outlier-damped.

```bash
curl "https://api.omnisocials.com/v1/analytics/best-times?platform=tiktok&timezone=Europe/Amsterdam" \
  -H "Authorization: Bearer $OMNISOCIALS_API_KEY"
```

The response contains a `grid` (one scored cell per day/hour that has data, scores relative 0-100 within the platform) and `recommendations`: the top 3 slots, never two on the same day within 3 hours of each other.

Check the `basis` field before trusting the numbers:

- `own_data`: computed from your workspace's posts.
- `own_data_and_audience`: your posts blended with when your followers are online (Instagram, TikTok Business). The response also carries `audience_online` (hour to followers online, in the response timezone).
- `audience`: fewer than 15 analyzed posts, so the recommendation comes from the audience-online profile only.
- `defaults`: a static industry-average table, returned when the workspace has fewer than 15 analyzed posts on the platform and no audience data. `posts_needed` says how many more published posts unlock personalized recommendations.

Times are expressed in the `timezone` you pass (any IANA identifier). It defaults to your account's timezone, then UTC.

## How engagement is calculated

Every OmniSocials surface (dashboard, API, CLI, MCP server) normalizes metrics the same way:

- **Engagement** is the sum of the platform's real interaction fields. Where a platform provides a precomputed total (Instagram's `total_interactions`, for example) we use it; otherwise we sum likes, comments, shares, reposts, quotes, and the platform's other interaction counts. For LinkedIn this includes link clicks, matching LinkedIn's own definition. For X it includes quotes and bookmarks.
- **Impressions** is the best available "how many times was this seen" number: total views where the platform provides them, falling back to impressions, then reach.
- **Engagement rate** is engagement divided by impressions, as a percentage capped at 100. Below 10 impressions the rate is reported as 0, because a tiny denominator produces a meaningless percentage, not a signal.

## Troubleshooting

| Symptom | Cause |
|---------|-------|
| `400 invalid_post_id` | You passed a platform post id (tweet id, YouTube video id). Use the numeric `id` from `GET /posts`. |
| Empty `platforms` object | No analytics collected yet. The post is very fresh, still scheduled, or failed to publish. |
| Zeros right after publishing | Normal. Platforms need time to count; the first refresh lands within 2 hours. |
| Google Business entry has `metrics_unavailable: true` | Google deprecated per-post insights; see the `note` in the response. Account-level metrics still work. |
| A metric key is missing for one platform | The platform does not report it (for example views on Bluesky and Mastodon), or the account lacks the permission (YouTube Analytics scope, TikTok Business authorization). |
| LinkedIn `impressions_lifetime` looks far too big | It is a lifetime total, not a window. Use `impressions` with `period: "daily"`, or diff two snapshots. |

## Other ways to read analytics

- **CLI**: [`omnisocials analytics:overview`, `analytics:post`, `analytics:best-times`](/cli/analytics) return the same data in your terminal, with `--json` for pipelines.
- **MCP server**: the [`get_post_analytics`, `get_posts_analytics`, `get_analytics_overview`, `get_account_analytics`, and `get_best_times` tools](/mcp-server) let Claude, ChatGPT, and other AI agents answer "how did last week perform?" directly.
- **Webhooks**: subscribe to post events instead of polling. See [Webhooks](/webhooks).
