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.
mode=period(default): positive counter growth observed during the selected UTC dates, including older content.mode=content: latest lifetime counters for destinations published during those dates, not activity received in that period.period=7d,30dor90d: inclusive UTC calendar-day windows ending today.from=2026-09-01&to=2026-09-30: inclusive UTC dates, maximum 365 days.profile: profile username, using its current channels.channel: social account ID.network: platform name.agent: stable creator client ID.origin: recorded interface (dashboard,apioragent). Historical attribution is not guessed. Account-wide supplements are omitted with either identity filter.
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
- Identity includes workspace, PostLake post, connected channel and remote object ID. Channels on the same network do not collide.
- A first reading within 24 hours of publication counts from zero; a later first reading establishes a baseline.
- Period activity counts growth above the previous highest counter. Downward corrections stay visible in lifetime readings; recovery is not counted twice.
- Growth across gaps belongs to its observation date. Missing event dates and unidentified counter resets cannot be reconstructed.
- Previous periods use adjacent equal-length UTC date windows with the same filters. Today can be incomplete. Null means no usable observations, not zero.
- Account insights in
byChannel.accountare separate. Native extras are supplemental lifetime counters, labellednativeExtrasBasis: latest_lifetime, not period activity. - Disconnecting and reconnecting: saved post analytics are retained. Reconnecting the same verified provider account and destination in the same PostLake profile reuses its original channel ID. A username alone cannot establish a match. Accounts disconnected before identity preservation may need support to verify and restore the link.
byChannel[].connecteddistinguishes current connections from saved history. A disconnected channel can still have available historical metrics. The channel selector marks archived entries as Disconnected and includes their ID suffix to distinguish them. Legacy entries without a saved name show the network and ID suffix rather than inventing a profile identity.- Threads profile insights: the separate dashboard panel reads the native rolling 30-day profile window, not a sum of PostLake posts.
accountPeriod.daysandaccountPeriod.capturedAtidentify its window and freshness, independent of the post-report range. Custom dates, agent and interface filters omit this profile reading. Profile-only rows have zero posts and unavailable post metrics. - Threads profile views sum daily profile views. Native likes and replies are totals; replies map to comments and reposts plus quotes map to shares. Link clicks sum link counters. Followers are the current total, read without date filters, or null.
account.reporteddistinguishes returned counters from N/A. No reach or demographics are invented. - Collection checks at most two profiles per continuation and reuses same-UTC-day results, including unavailable attempts. Refresh does not bypass this protection or force Meta to update instantly. The reads require
threads_basicandthreads_manage_insights. See Meta's definitions. Individual-post metrics remain separate in Post Detail. - Raw evidence retains the latest daily observation. Atomic updates maintain rollups; duplicate timestamps and older readings cannot inflate them.
- Legacy readings migrate only when one destination is identifiable. Ambiguous evidence remains retained, not multiplied.
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:
| Field | Meaning |
|---|---|
impressions | Times the post was shown. |
views | Views or watches, where reported. |
reach | Unique accounts that saw it. |
likes | Likes / reactions. |
comments | Comments / replies. |
shares | Reposts / shares. |
saves | Saves / bookmarks. |
clicks | Link / profile clicks. |
followers | Follower 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.