Guides

Analytics

Read how your posts performed, per post, or across every network at once, in one normalised response. Same metric names everywhere, so you (or an agent) can compare platforms without special-casing each one.

Per-post analytics

Fetch the metrics for a single post across all the accounts it went to:

curl https://api.postlake.dev/v1/posts/post_a1b2c3/analytics \
  -H "Authorization: Bearer YOUR_API_KEY"

The response includes measuredAt and one targets[] entry per destination. Each target has reports[], the metric names its adapter can expose, and metrics. A null metrics value means PostLake could not read that target's insights; it does not mean the post had zero engagement. In the dashboard, open a published post to see these figures beside its content. That view refreshes while open and also has a manual Refresh button. Networks may update their counts later than PostLake checks them.

Cross-platform analytics

Roll up performance across every connected account over a period (this is the differentiator. One call, every network):

curl "https://api.postlake.dev/v1/analytics?period=30d" \
  -H "Authorization: Bearer YOUR_API_KEY"

Reports cover all collected publishing destinations, without a recent-50-post sample or live provider calls. Totals, charts, comparisons and breakdowns use the same mode and filters.

Shared posts contribute only matching destinations. Moving a channel changes current profile attribution. posts counts distinct tracked posts with collected readings published in the range, not every published post. topPosts ranks up to 20 destinations across the complete matching population; one shared post can appear more than once. Rankings do not cap totals.

Refresh without waiting

Selected metrics and rankings

Use sort=views, likes, comments, engagement, engagementRate, impressions or clicks. The default is engagement. Sorting happens across the complete matching population before the 20-destination limit, not just the already-returned rows. rankingMetric confirms the chosen sort. Missing readings rank last. Dashboard metric cards select the chart and ranking together. Clicks and impressions remain under More metrics when supported.

Count bars show a channel or network's share of the selected reported metric. Rate bars compare actual percentages with the highest available rate; percentages are never added into a contribution total. A daily rate uses that day's eligible activity or content results, not an average of post percentages. Gaps mean no usable audience, not zero engagement.

In Content performance, averageViews contains value and destinations: lifetime views divided by destinations explicitly returning views, including reported zeros. Each channel of a shared post counts separately. Unknown readings are excluded. Period performance returns null; new activity divided by newly published posts would be misleading.

POST /v1/analytics/refresh queues collection and returns immediately. GET /v1/analytics/refresh reads queued, running, complete or failed, with posts checked and errors. Active jobs are reused; new starts have a five-minute cooldown. Complete does not mean every provider returned every field. Previous valid readings survive provider failures.

Normal visits read a private saved report rather than rebuilding it from the primary database. Reports are isolated by workspace, filters and UTC dates. A first visit or a completed collection refresh can prepare a new saved report through a globally limited read-only builder. Busy cold builds return HTTP 429 with Retry-After: 60; database failures return HTTP 503. Reuse the existing report while waiting, not another publishing request.

Completed collection invalidates saved reports immediately, without an additional edge-cache delay. If rebuilding fails, the same-filter previous report can remain available with snapshot.stale: true. snapshot.generatedAt is when the saved report was prepared; measuredAt is when metrics were collected. Neither means the platform has reported real-time data. Derived saved reports expire after seven days, are limited per workspace, and are removed during account erasure. This does not delete posts, uploads or source metric history.

Opening Analytics loads saved readings immediately and requests an update only when the workspace collection is overdue. It uses POST /v1/analytics/refresh with { "ifStale": true }: an active job or one updated within fifteen minutes is reused. Filter changes do not start more jobs. The page checks progress only while a job is active and reloads the report after completion. Refresh data requests a collection explicitly, subject to the five-minute start cooldown and each provider's eligibility. This does not guarantee instantly updated network counters.

When collection is enabled, due work is checked every 15 minutes. Content up to seven days old is eligible every 15 minutes, 8-28 days hourly, 29-90 days daily, and older content weekly. Refresh respects eligibility and provider cooldowns. Provider reporting can lag. Collection can be paused independently of saved-report reads.

Each collection fixes its due-work cutoff at the job's startedAt. Content becoming due later waits for the next collection. This keeps large jobs finite rather than repeatedly checking their first posts when those posts become eligible again. Optional metric failures do not block a whole channel when its adapter returns valid measured fields. Real rate limits and complete failures still apply cooldowns.

During reliability incidents, new collection can be paused to prioritise publishing and account access. Existing readings and scheduled posts are not deleted. A refresh request then returns HTTP 503 with analytics_refresh_unavailable and Retry-After: 60. An existing job's progress includes collectionPaused: true; its queued or running state does not mean collection is currently advancing. Read stored reports without repeatedly starting refresh jobs.

MCP: call get_analytics with the same filters and refresh: true, then call again for progress. SDK: client.analytics.get(), client.analytics.refresh() and client.analytics.refreshStatus().

Engagement rate and metric definitions

Views are the network's reported view counter. Impressions count displays where a network exposes that metric. Neither is a unique-person count, and a video view is not interchangeable with an impression. PostLake does not estimate impressions from views or interactions.

Engagement is likes or reactions + comments or replies + shares, reposts or quotes + saves + clicks, using the counters available from each network. Missing counters are not measured zero. Clicks are not necessarily outbound link clicks. One person can perform several actions.

Per-network engagement rate is engagement divided by the audience counter, multiplied by 100. PostLake uses views when reported, otherwise impressions. The denominator is not followers or reach. Without a positive usable audience count, the rate is unavailable, not zero.

Across networks, PostLake divides the summed eligible engagement by the summed audience counts, then multiplies by 100. It does not average network percentages. Networks with no usable audience count contribute to neither side of the rate, although their measured engagement can still appear in the engagement total.

Eligibility is checked per destination, even when other posts on that channel report an audience. A reported zero views count does not fall back to impressions. Rates use the latest reported-field coverage; unknown legacy readings use adapter capabilities as a fallback. Daily rates use the same eligible numerator and denominator rules.

For example, 100 actions on 1,000 TikTok views and 10 actions on 200 Pinterest impressions give (100 + 10) / (1,000 + 200) × 100 = 9.2%, rounded to one decimal. This is a blended action rate across different measurement rules, not a unique-person rate or the native rate shown by either platform. Multiple actions per exposure can produce rates above 100%.

The selected mode applies to both sides: Period mode uses observed activity increments; Content mode uses lifetime readings for content published in the chosen dates. Different networks expose different actions, so compare trends within a network as well as the blended headline. Account-wide insights are never added to post totals.

The dashboard's metric information controls explain these definitions. Post Detail keeps network-specific impressions separate and labels unavailable readings N/A. A reported zero remains 0. API clients and agents can read the same rules through GET /v1/analytics/methodology.

Reconciliation methodology

Call GET /v1/analytics/methodology for machine-readable definitions.

The metric shape

Every platform maps to the same Metrics object, so a chart or an agent reads one set of fields regardless of source:

FieldMeaning
impressionsTimes the post was shown.
viewsViews or watches, where reported.
reachUnique accounts that saw it.
likesLikes / reactions.
commentsComments / replies.
sharesReposts / shares.
savesSaves / bookmarks.
clicksLink / profile clicks.
followersFollower count / growth for the account.

Not every network exposes every metric. Target reports[] lists adapter capabilities; metrics.reported lists counters actually returned by that reading, including measured zero. An absent field is not measured zero. Incomplete readings preserve earlier valid lifetime values. Older evidence without reported has unknown coverage. Channel fieldCoverage counts fields present in latest observations, scoped by observation date in Period mode or publication date in Content mode, not historical daily coverage. Channel metadata and avatar bytes refresh in bounded background work and preserve previous valid images on failures. See the analytics methodology for machine-readable reconciliation rules.

Why an agent cares. Because the response is normalised, an assistant can ask "which of last week's posts did best, and on which network?" and reason over one consistent shape. Then decide what to post next.