Analytics
Read your own LinkedIn performance and search the viral-post inspiration feed. Everything here is read-only and gated by one scope:analytics:read — grantable to
API keys, so this whole family works
with plain keys.
Full schemas: OpenAPI reference → Analytics.
Semantics worth knowing
- Multi-project aggregation. All three endpoints share one resolver,
so
projectIdaccepts a comma-separated list everywhere —/overview,/postsand/reportalike.projectIdsis an accepted alias, and the two are merged if you send both. Omitting them aggregates across every project the caller can reach. Either way the set is capped at 50 projects: more than 50 explicit ids returns400 invalid_request, while the omit-everything branch silently stops at 50. - Rows carry three ids; only
post_idis actionable. A/v1/analytics/postsrow describes a post that is live on LinkedIn:idis the measurement row (no endpoint accepts it),post_idis the Scripe post (nullfor anything not published through Scripe — most of a mature account’s back catalogue), andpermalinkopens the LinkedIn copy. Usepermalinkto show the user the post; usepost_idwith/v1/posts/{postId}. - Asking about one post? Pass
postId./v1/analytics/poststakespostId(repeatable, up to 50post_…ids) and answers with just those rows plus ametablock:meta.unmeasurednames each requested post that has no numbers and why —not_published(never went out) ornot_measured(published, but LinkedIn’s sync hasn’t landed; it usually does within the hour). An id the project does not contain is a400, never an empty page. ItsdateFrom/dateTofilter publication dates, so they line up withpublishedFromwindows on/v1/posts. - Post bodies are excerpts by default.
/v1/analytics/poststakes the samecontent=preview|full|nonemodes as/v1/posts, withcontent_truncatedbeside each body — see Conventions § Large text fields. sortdoes not rank by rate.impressionsranks by views andengagementby total interactions; if your user means engagement rate, sort client-side onmetrics.engagement_rate.- The report is a file.
GET /v1/analytics/reportreturns a binary body (application/pdfortext/csv) with aContent-Dispositionfilename — executive summary, earned media value, per-author performance, every post in the period (highest-impression first, up to 3,000 rows — when a period holds more, the file says so and points atGET /v1/analytics/posts, which pages through all of them), daily engagement series, follower timeline.currency(EUR/USD/GBP) andcpmtune the earned-media computation. The MCP toolget_analytics_reportreturns the same figures as JSON, with the post table capped to the genuine top 20 of the period (itspostsTotalcounts every post; page the rest vialist_post_analytics). The 3,000-row ceiling is there for pathological ranges, not as a ranking cut, and when it is reached the file says so above the post table, naming how many posts were dropped, never silently. - Two engagement rates in the report. The daily series carries
engagementRate— that day’s own engagement over that day’s impressions,0on a day with no impressions — andcumulativeEngagementRate, the running rate from the start of the range (what the in-app KPI card plots). The CSV carries both columns; the PDF’s Daily Metrics table shows the per-day rate only. Read the per-day rate to compare days, the cumulative one for a trend line. - Three click figures, three sources — and the dashboard never adds
them up. The report’s
summary(and theget_analytics_reportJSON) carriestotalLinkClicks, LinkedIn’s count of clicks on links inside the period’s posts, attributed to each post’s publish date — the dashboard’s Link clicks KPI card;totalTrackedLinkClicks, clicks on the workspace’s Scripe tracked links (scripe.io/link/…and custom link domains) whose click date falls in the period — the dashboard’s conversion-funnel Tracked link clicks stage (labelled Link clicks before SCR-1719, so an older screenshot still maps) and its Click tracking section, always workspace-wide because tracked links are workspace-owned, so aprojectIdselection does not narrow it; andtotalPremiumCtaClicks, LinkedIn’s clicks on the profile’s Premium call-to-action button, which is not a link click. Eachposts[]row carries the first and third per post. A reading off the funnel can only be rebuilt fromtotalTrackedLinkClicks; a reading off the KPI card only fromtotalLinkClicks.totalTrackedLinkClicksisnullonly when the report was built without a workspace, which no request on this API is. - Viral-post search excludes your own author — it’s an inspiration
feed, not self-analytics. It is bounded before it ranks (last 90
days, the project’s language,
minEngagement≥ 100 by default), so an empty result means nothing recent cleared the filters — widenlanguageor lowerminEngagementbefore concluding the topic is uncovered.metrics.viewsisnull, not0, when LinkedIn withholds the count (it does for ~97% of third-party posts), andmeta.returnedis the number of rows served — there is no paging and no corpus-wide total. - Metrics lag LinkedIn. Figures reflect Scripe’s most recent sync, not a live LinkedIn call.
Across every workspace you can reach
Every other endpoint on this API runs against one active workspace, and that is structural rather than a gap: a project id from another workspace answers404, on purpose. The two
/v1/analytics/cross-workspace/* endpoints are the exception — they
answer “how are all my clients doing” in one call instead of a
switch → list → measure loop per client.
- Selection. Omit
workspaceIdfor every reachable workspace (allis an explicit spelling of the same thing), or pass one id / a comma-separated list. A named workspace you cannot reach is refused withworkspace_unavailable— never silently omitted, because an omitted client reads as a client with no activity. - Naming more workspaces than the cap is refused, not truncated.
That is the opposite of the default
all_reachableselector, which truncates loudly (meta.truncated) rather than refusing. The asymmetry is deliberate: silently dropping some of a list you named explicitly would be worse than telling you outright — split the request across calls instead. OmittingworkspaceIdis what truncates. - Reach is the same model
GET /v1/workspacesreports: Clerk membership or the billing-based agency-owner grant, echoed per row asworkspace.reach. Inside each workspace the ordinary project rule applies, so a plain member still sees only their assigned projects. totalsis a query, not a sum. It re-measures the union of every project in the response, because an engagement rate has no meaningful average and reconstructing engagement counts fromrate × impressionsdrops every post LinkedIn withheld impressions for. It isnullwhen there was nothing to total or the union was too large for another aggregate pass;meta.totals_omitted_reasonsays which, and you should report no total rather than adding the rows up.meta.skippedis not zero activity. A reachable workspace whose projects you cannot read is named there instead of being dropped.meta.truncatedmeans you are reading the firstmeta.max_workspacesreachable workspaces — pass the rest explicitly inworkspaceId, at mostmeta.max_workspacesper call (a longer list is refused, per the bullet above). The report endpoint visits fewer per call than the overview, because it returns a whole document each.meta.workspaces_reachableis your full reach, on both selectors — not the number you named, and not a truncation signal. On an explicit selection it normally exceedsmeta.workspaces_returnedwhilemeta.truncatedis false; readtruncatedfor that.meta.projects_truncatednames any workspace holding more readable projects than one call measures, whose row and share oftotalstherefore cover only its newest projects. Empty when every workspace fitted; measure those workspaces through/v1/analytics/overviewwith explicitprojectIds.- The report variant is JSON only. The PDF and CSV renderers lay out
one organisation’s report; fetch the file per workspace from
GET /v1/analytics/reportwith aScripe-Workspace-Idheader. Each workspace’sreport.posts[]holds only the topmeta.posts_per_workspaceof the period whilereport.postsTotalcounts them all. - OAuth only, and
projectIdis rejected. An API key is bound to one workspace at creation, so there is nothing to aggregate across (400 invalid_request), and a project id is a workspace-scoped id arriving at a workspace-crossing call — use/v1/analytics/overviewfor specific projects. Both endpoints needanalytics:readandworkspace:read, since the response enumerates workspaces. - The single-workspace endpoints are unchanged. Nothing above alters
what
/v1/analytics/overview,/postsor/reportreturn.
Viral-post search returns outliers, not big numbers
GET /v1/viral-posts narrows to posts that beat their OWN author’s
median engagement by at least meta.min_outlier_multiplier — 2 by
default — so a large account’s ordinary post is not reported as a
hit. Each post carries that ratio as outlier_multiplier (capped at
10) and the denominator behind it as outlier_baseline: only AUTHOR
compared the post to its own writer, so only AUTHOR supports a
sentence about that person’s usual performance. null means the post is
not scored — too fresh, or below the scoring floor — and is reachable
only with minOutlierMultiplier=0, which opts out of the floor
entirely.
Three parameters narrow the search and meta reports all three back:
minOutlierMultiplier, mediaFormats (a comma-separated list —
carousel is a swipeable deck, multi_image several photos, image at
most one) and publishedWithinDays (90 by default, 180 max).
meta.narrowed_to is present when you narrowed beyond the default (a
floor above 2, any media format, or a window under 90 days) and the
answer is short, and names what to widen. One of those filters may be
why — a short answer under a carousel filter is more often the
corpus’s shape than a missing topic — so offer to widen rather than
conclude the topic is unwritten; the search cannot tell a thin topic
from a filtered one.
Each post also carries media_format, classified from the stored media
payload first and the media type only as a fallback, so it agrees with
the shape the Scripe app shows for the same post. A post whose media type
says carousel but whose payload proves neither a deck nor several images
is other: that type alone is written for both shapes, so a carousel
filter never serves it.
Agent access
get_analytics_overview, list_post_analytics, search_viral_posts,
get_analytics_report, the two cross-workspace twins
(get_cross_workspace_analytics_overview,
get_cross_workspace_analytics_report), and the plan-gated
get_posting_times (best posting times, Advanced+ plans) are the MCP
tools — see MCP tools.