Skip to content

Aggregate analytics

Project-level aggregate endpoints for leaderboards, heatmaps, histograms, scatter charts, and membership overlap.

Updated Oct 10, 2026 · 10 min read

Overview#

The aggregate analytics endpoints return pre-computed project-level summaries that power the Analytics section (Video, Creator, and Campaign sub-tabs). They differ from the unified /api/v2/analytics endpoint in that they aggregate across all entities in a project rather than for a single entity.

All endpoints:

  • Accept both Bearer API key and session cookie authentication
  • Return the standard { data, error, meta } envelope
  • Require the caller to have access to the specified project_id
  • Are not cached (no Vercel Runtime Cache wrapper): responses are fresh on every request

Base path: /api/v2/analytics/aggregate


Totals#

GET /aggregate/summary#

Headline totals for a project, one campaign, or every campaign at once, over a date window. The dashboard, every Insights summary strip and Program Health read their headline numbers from this query, so the numbers match the app.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequiredProject to total
fromYYYY-MM-DD29 days before toPeriod start (inclusive, org time zone)
toYYYY-MM-DDtoday (org time zone)Period end (inclusive)
campaign_idUUIDNoneOnly this campaign (sub-campaigns and member creators included)
campaignsallNoneThe distinct union of every campaign's videos, each video counted once (ignored with campaign_id)
platformtiktok | instagram | youtubeNoneOnly one platform

Response data

JSON
{
  "tracked_posts": 2246,
  "views": 10171040,
  "likes": 1711759,
  "comments": 6972,
  "shares": 26461,
  "saves": 33878,
  "engagement_rate": 17.49,
  "creators_posting": 85,
  "accounts_posting": 85,
  "roster_creators": 106,
  "roster_accounts": 110,
  "platforms": 2,
  "total_followers": 208181,
  "follower_growth": 3958,
  "pending_review_posts": 61,
  "posts_without_history": 177,
  "stats": { "posts": 2246, "within_48h": 1690, "never_scraped": 0, "oldest_at": "2026-09-11T10:30:30Z", "newest_at": "2026-10-09T19:31:00Z" }
}

views is the current lifetime views of the tracked posts posted in the window (not views gained during it). engagement_rate is a percentage, null without views. follower_growth is measured per account (an account added mid-window counts from its first snapshot) and summed. pending_review_posts are posts in the window awaiting review; they are not in any other number. stats says how fresh the video stats behind the numbers are. meta echoes from, to and timezone.


Video endpoints#

GET /aggregate/video-leaderboard#

Returns a ranked list of tracked videos in the project for the given date range.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequiredProject to query
fromYYYY-MM-DDNonePeriod start (inclusive)
toYYYY-MM-DDNonePeriod end (inclusive)
orderenumview_countSort column: view_count, like_count, comment_count, share_count, save_count, posted_at
platformtiktok | instagram | youtubeNoneRestrict to videos on a single platform
campaign_idUUIDNoneRestrict to videos the campaign resolves to (must belong to project_id)
limitint100Max rows (1 to 200)
offsetint0Pagination offset

Response data array: each item:

JSON
{
  "video_id": "uuid",
  "title": "string | null",
  "thumbnail_url": "string | null",
  "url": "string | null",
  "view_count": 12345,
  "like_count": 678,
  "comment_count": 45,
  "share_count": 12,
  "save_count": 89,
  "posted_at": "2026-04-01T12:00:00Z",
  "creator_id": "uuid",
  "handle": "string",
  "display_name": "string | null",
  "avatar_url": "string | null",
  "total_count": 87
}

total_count is the total matching rows (before limit/offset). handle is the creator's primary account handle. Pass platform to restrict the leaderboard to videos on one platform.


GET /aggregate/posting-heatmap#

Returns a DOW × hour grid of post counts and average view counts, bucketed in the org's timezone.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDrequired
toYYYY-MM-DDrequired
tzIANA stringorg timezoneOverride timezone
platformtiktok | instagram | youtubeNoneRestrict to posts on a single platform

Response data array: each item:

JSON
{ "dow": 1, "hour": 14, "post_count": 5, "avg_views": 12430.50 }

dow: 0=Sunday … 6=Saturday. Only cells with at least one post are returned; missing cells = 0.


GET /aggregate/view-distribution#

Returns a histogram of video view counts with percentile markers.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDrequired
toYYYY-MM-DDrequired
bin_countint20Number of histogram bins (5 to 50)
platformtiktok | instagram | youtubeNoneRestrict to videos on a single platform

Response data object:

JSON
{
  "total": 87,
  "min": 100,
  "max": 5200000,
  "p50": 12000,
  "p75": 48000,
  "p90": 210000,
  "p99": 1800000,
  "bins": [
    { "bin_index": 0, "bin_lower": 100, "bin_upper": 260250, "video_count": 61 },
    ...
  ]
}

GET /aggregate/outliers#

Returns videos that outperform their creator's median view count by at least min_x_multiple, sorted by x_multiple descending.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDNoneOptional period filter
toYYYY-MM-DDNoneOptional period filter
min_x_multiplenumber1.5Minimum outperformance ratio
platformtiktok | instagram | youtubeNoneRestrict to videos on a single platform
limitint50Max rows (1 to 100)

Response data array: each item includes video_id, creator_id, view_count, median_views, x_multiple, posted_at, plus enriched title, thumbnail_url, url, handle, display_name, avatar_url.


Creator endpoints#

GET /aggregate/creator-leaderboard#

Returns creators in the project ranked by views in the period, with follower delta and engagement metrics.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDNone
toYYYY-MM-DDNone
platformtiktok | instagram | youtubeNoneRestrict to creators/videos on a single platform
limitint100Max rows (1 to 200)
offsetint0

Response data array: each item:

JSON
{
  "creator_id": "uuid",
  "handle": "string",
  "display_name": "string | null",
  "avatar_url": "string | null",
  "current_followers": 45000,
  "follower_delta": 1200,
  "total_views": 980000,
  "posts_in_period": 14,
  "posts_per_week": 1.0,
  "engagement_rate": 3.45,
  "growth_rate": 2.74,
  "total_count": 7
}

follower_delta = follower count at end of period minus start of period (from creator_stats_history). Zero when no snapshots in range.

engagement_rate = (likes + comments + shares) ÷ views × 100 for videos posted in the period.


GET /aggregate/posting-consistency#

Returns per-creator, per-day post counts for building a punch-card matrix.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDrequired
toYYYY-MM-DDrequired
tzIANA stringorg timezone
platformtiktok | instagram | youtubeNoneRestrict to posts on a single platform

Response data array: sparse (only days with posts):

JSON
{ "creator_id": "uuid", "day": "2026-04-05", "post_count": 2 }

Every (creator, day) pair in the range is returned, ordered by creator then day. (Before 2026-10-08 the response was silently truncated at 1,000 rows.)


GET /aggregate/over-posting-risks#

Flags creator/day combinations where a creator posted enough times in a single day that some videos were held back from tracking (suppressed) to control scrape cost.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDrequired
toYYYY-MM-DDrequired
tzIANA stringorg timezoneOverride timezone
platformtiktok | instagram | youtubeNoneRestrict to posts on a single platform

Response data array: each item:

JSON
{
  "creator_handle": "string",
  "creator_id": "uuid",
  "post_date": "2026-04-05",
  "total_posts": 9,
  "suppressed_count": 3,
  "suppressed_video_ids": ["uuid", "uuid", "uuid"],
  "creator_last_scraped_at": "2026-04-06T02:00:00Z"
}

creator_last_scraped_at is the most recent scrape across all of the creator's platform accounts (null if never scraped).


GET /aggregate/creator-scatter#

Returns per-creator (follower change, engagement rate) pairs for scatter chart rendering.

Query parameters: project_id, optional from/to, optional platform (tiktok | instagram | youtube) to restrict to a single platform.

Response data array:

JSON
{
  "creator_id": "uuid",
  "handle": "string",
  "follower_delta": 1200,
  "engagement_rate": 3.45,
  "posts_count": 14
}

Only creators with at least one tracked post in the period are included.


GET /aggregate/goal-compliance#

Returns a project-wide roll-up of creator goal statuses across every creator with active goals.

Query parameters: project_id.

Response data object:

JSON
{
  "on_track": 3,
  "at_risk": 1,
  "missed": 1,
  "total_goals": 5,
  "creators_with_goals": 5,
  "with_advisories": 4
}

on_track includes completed statuses. at_risk includes behind_pace.

with_advisories is the count of goals where the advisory.has_advisory flag is true, i.e., goals whose displayed progress may not yet be final (period in progress, period recently closed, stale scrape, or goal not yet started). See the Goal Compliance endpoint for per-goal advisory detail.


Campaign endpoints#

GET /aggregate/campaign-leaderboard#

Returns campaigns in the project sorted by views, with prior-period delta comparison.

Query parameters: from, to (optional; default the last 30 days including today, org time zone); project_id and/or campaign_id. Pass project_id alone for every campaign in the project, or campaign_id to return just that one campaign's row, in which case project_id is optional: it's resolved from the campaign (a campaign outside your org returns 404). Add platform (tiktok | instagram | youtube) to count only that platform's posts, in both the current and the prior period. The response meta echoes the resolved project_id and platform.

Response data array:

JSON
{
  "campaign_id": "uuid",
  "name": "string",
  "total_views": 1200000,
  "total_likes": 45000,
  "video_count": 32,
  "creator_count": 5,
  "preview_thumbnail_url": "string | null",
  "prior_views": 980000,
  "views_delta": 220000,
  "views_delta_pct": 22
}

Prior period is automatically computed as the same duration immediately preceding from. Each row counts its campaign's videos once; a video in two campaigns appears in both rows, so don't add rows up for a total: use /aggregate/summary with campaigns=all. creator_count is the number of creators with a tracked post in the window.


GET /aggregate/campaign-timeseries#

Returns the trend series behind the Campaign Insights charts: impressions, posts, distinct creators posting, and first-time creators per time bucket, zero-filled across the range.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequiredProject to query
campaign_idUUIDNoneScope to one campaign's resolved videos. Omit for every active tracked video in the project
fromYYYY-MM-DDrequiredRange start (inclusive, org-TZ calendar day)
toYYYY-MM-DDrequiredRange end (inclusive)
granularityday | week | month | yeardayBucket width. Weeks start on Monday
tzIANA tzNoneOverride the org timezone used for day boundaries
platformtiktok | instagram | youtubeNoneOnly count posts on this platform (every series, including first-time creators)

Response data array: one item per bucket, ordered by bucket_start:

JSON
{
  "bucket_start": "2026-08-10",
  "impressions": 456467,
  "posts": 436,
  "creators_posting": 39,
  "new_creators": 4
}
  • impressions is the views gained during the bucket, attributed to the day the snapshot moved (not the video's posting day). A video's first snapshot (its pre-tracking views) is credited to its posting day when that day is inside the range.
  • posts and creators_posting are by posted-at date.
  • new_creators counts creators whose earliest post in the scope falls in the bucket; each creator counts once over all time, not once per range.
  • Impressions per post is not returned; compute impressions / posts per bucket client-side and treat buckets with posts = 0 as undefined.

meta echoes from, to, tz, granularity, and campaign_id.


GET /aggregate/member-overlap#

Returns entities (creators or videos) that appear as direct members in campaigns, with the list of campaigns each belongs to. Capped at 50 rows.

Query parameters

ParameterTypeDefaultDescription
project_idUUIDrequired
dimensioncreator | videocreatorEntity type to list

Response data array:

JSON
{
  "entity_id": "uuid",
  "entity_label": "handle or title",
  "entity_avatar": "string | null",
  "campaign_ids": ["uuid", "uuid"],
  "campaign_names": ["Campaign A", "Campaign B"],
  "campaign_count": 2
}

Only direct membership (via campaign_items) is considered, not recursive resolution through nested campaigns.


Conversion aggregates#

Three endpoints summarise recorded conversions. All of them ignore voided conversions entirely, because a void is a retraction and never appears in a total.

Unlike the conversions list, which takes full ISO-8601 timestamps, these take bare YYYY-MM-DD dates like the other aggregates. to is inclusive, resolved against the organization's timezone.

GET /api/v2/analytics/aggregate/conversion-summary#

ParameterTypeDefaultDescription
project_idUUIDrequired
fromYYYY-MM-DDNoneInclusive start
toYYYY-MM-DDNoneInclusive end
conversion_event_idUUIDNoneRestrict to one event type

Returns zero rows when nothing was recorded in range: there is no correct currency to denominate an empty total in, so render an empty state rather than a zero.

JSON
{
  "total_conversions": 128,
  "total_quantity": 194,
  "total_value_cents": 486000,
  "currency": "USD",
  "distinct_creators": 12,
  "distinct_events": 3
}

GET /api/v2/analytics/aggregate/conversion-leaderboard#

Creators ranked by conversion value. Accepts the summary parameters plus limit and offset.

Rows are per (creator, currency), so a creator with conversions in two currencies appears twice. total_count counts groups, not creators.

Totals include the full rollup: a creator's figure covers conversions recorded against their accounts and their individual videos, not just those attributed to the creator directly.

JSON
{
  "creator_id": "uuid",
  "display_name": "Jules",
  "handle": "wyndywithjuls",
  "avatar_url": "string | null",
  "conversion_count": 34,
  "total_quantity": 41,
  "total_value_cents": 128000,
  "currency": "USD",
  "total_count": 12
}

GET /api/v2/analytics/aggregate/conversion-timeseries#

Daily buckets in the organization's timezone, one row per (day, currency). The series is sparse: days with no conversions are omitted, so fill the spine client-side.

JSON
{ "day": "2026-07-18", "conversion_count": 9, "total_value_cents": 22500, "currency": "USD" }