Pulling social analytics into your own dashboard
Sync Versely social analytics on a schedule: overview, per-post snapshots, and history, so reporting lives in your dashboard instead of theirs.
Versely's in-app analytics are a reading surface, not a warehouse. If the number has to sit next to revenue, a client name, or last week's brief, it has to live in your dashboard. The /social-analytics group is how you copy it out: four routes, an analytics scope, snapshots you can store, and a five-minute cache you should not fight.
This is not a replacement for YouTube Studio or TikTok Analytics. Those still own retention curves, traffic sources, and anything Versely never saw. This is the rollup for posts that went out through Versely, pulled on a schedule, so the weekly review happens in one place. The in-app version of the same numbers is brand video analytics. What follows is the version you own.
What the four routes actually return
Mint a key with scopes: ["analytics"] and nothing else. Overview and history are reads. Collect is a POST that refreshes a snapshot. None of these generate media, and none of them publish.
GET /api/v1/social-analytics/overview
Account-level rollup for the authenticated user. Cached for five minutes.
{
"success": true,
"overview": {
"total_posts": 128,
"total_engagement": 18420,
"avg_engagement_rate": 4.12,
"totals": {
"views": 447000,
"likes": 12100,
"comments": 980,
"shares": 2100,
"saves": 3240,
"impressions": 447000
},
"platforms": [
{ "platform": "tiktok", "posts": 40, "views": 220000, "likes": 6400, "comments": 400, "shares": 1200, "saves": 1800 }
],
"top_post_ids": ["…", "…", "…", "…", "…"]
}
}
Totals are the latest snapshot per (post_id, platform), not a sum of every collect you ever ran. total_engagement is likes + comments + shares + saves. avg_engagement_rate is that sum over views, as a percentage. top_post_ids is five ids, ranked by that same engagement sum. impressions is currently stored as the views figure from the fetch; do not treat it as a distinct ad-impression metric.
GET /api/v1/social-analytics/:postId
On a cache miss: live fetch, snapshot write, then a five-minute cache keyed on the post. A hit returns the cached payload and does not insert another row. The payload is an analytics array, one object per destination the post actually reached:
platform,username,postUrlsuccess/errorstats:views,likes,comments,shares,saves
Live fetchers exist for TikTok, Instagram, YouTube, X, Facebook, and LinkedIn. A post that only went to Pinterest, Bluesky, or Threads will not grow a stats row from this pull. A post that never went out through Versely has no published URL to fetch, so the array is empty. That is expected, not an outage.
POST /api/v1/social-analytics/:postId/collect
Same fetch, forced. Use this from a cron when you want a new snapshot even if the GET cache is still warm. Each successful collect inserts a post_metrics row (views, likes, comments, shares, saves, impressions, engagement_rate, fetched_at). That row is the history.
GET /api/v1/social-analytics/:postId/history?limit=50
Those rows, newest first. limit defaults to 50. There is no cursor. If you need more, raise limit on the next call rather than inventing a page token the handler does not read.
You still need the post catalog to know which ids to collect. GET /api/v1/social/posts?limit=20&offset=0 is a post-scoped read. Either grant the reporter post as well, or have the publisher write the post id into your database at create time (the cleaner split). The agent equivalent, if a person is poking a single post, is check social performance.
A sync loop that respects the cache
Live pulls are cached for five minutes. Hammering /overview every 15 seconds gets you the same payload and a hotter rate limiter. Per-key RPM defaults to 60. There is no separate analytics limiter, so the key RPM is the one that bites.
A loop that works:
- On publish, store the Versely post id, the platforms, the caption, the media URL, and your own content id. Do this in the poster, not in the reporter.
- Every 15 minutes,
GET /social-analytics/overview. Upsert the totals and the per-platform breakdown into your warehouse withfetched_at = now(). If the payload matches the previous one byte-for-byte, you are still inside the cache window; keep the row anyway so the chart does not have holes. - Every 15 minutes, for posts younger than 72 hours,
POST /social-analytics/:postId/collect. New posts move in the first three days. After that, collect daily. After two weeks, collect weekly or stop. A post that flatlined at hour 8 does not need a 15-minute poll on day 12. - Once a day,
GET /social-analytics/:postId/history?limit=50for the posts you still care about, and replace your local series with it. The server series is the source of truth for "what did we store." Your dashboard series is the source of truth for "what do we show next to the brief." - On 429, sleep
retryAfterseconds. On 403API key lacks required scope: analytics, stop. That is a minting error, not a transient one. On 404, the post id is wrong or not yours; drop it from the collect set.
Do not parallelise collect across every post you have ever published. A year of daily posts is hundreds of POSTs. Bound the in-flight set to "young or still moving," and walk the rest with offset on the catalog.
Trend research is a different group in the same scope: /trend-analysis and /trending-feed. That is input (what to make), not output (what you made). The agent entry is find what's trending. Keep it off the reporting cron. Mixing "what is trending on TikTok" into a client dashboard is how a reporting job turns into a content-strategy job without anyone naming the change.
What you can honestly chart
These stats are public-looking counts on the published URL: views, likes, comments, shares, saves. They are good for:
- Ranking last week's Versely posts against each other.
- Seeing whether a format is sliding across consecutive runs.
- Spotting a destination that always under-indexes (LinkedIn views next to TikTok views on the same cut).
- Picking the five ids in
top_post_idsand actually watching them before you make "three more like this."
They are not good for:
- Audience retention, average view duration, viewed-versus-swiped-away, or traffic sources. Those stay in the native product. Cross-platform analytics is the comparison framework; this API does not ingest those charts.
- Declaring a winner on CTR. Nothing here is impressions-in-the-ad-auction sense, even though the snapshot field is named
impressions. - Posts you uploaded by hand in the TikTok app. No Versely post row, no URL in the result, no fetch.
Engagement weighting is your job once the numbers are local. Likes are cheap. Comments and shares are not. A workable private weighting is the one in the in-app guide: views as context, likes low, comments and shares high. Put the weights in your dashboard, not in the sync job, so you can change them without re-collecting.
The other habit that survives contact with a real reporting table: pick the question before you look. "Did question hooks beat statement hooks this week, on TikTok, for posts we shipped through Versely?" is a query. "How did social go?" is a screenshot. Creative analytics is the metric list. This API is how those metrics stop living in someone else's UI.
FAQ
Why is a post I know went out returning empty analytics?
Usually the live fetch has no published URL to hit. The pull uses the stored post result's platform_data.url. If the result never landed, or the destination is not one of TikTok, Instagram, YouTube, X, Facebook, or LinkedIn, you get an empty or unsuccessful array. Wait, re-collect, and confirm the post row actually reached posted rather than processing or failed.
Will two collects ten seconds apart give me two history rows?
The GET is cached for five minutes, so two GETs in that window return the same payload and the first one already wrote a snapshot. Two POSTs to /collect both fetch and both insert. Prefer GET for reads and POST for the cron, and keep the cron at or above the five-minute cache.
Can I page through history with a cursor?
No. limit defaults to 50. There is no documented cursor. For overview, there is nothing to page: it is one rollup. For the post catalog, use limit and offset on GET /social/posts.
Does an analytics key let me post or generate?
Not if you minted it with analytics only. /social-analytics is that scope. /social/posts is post. /generate is generate. A reporter that can publish is a different incident than a reporter that can only read. Keep it read-shaped, and put the numbers next to the brief in a dashboard you actually open.