Zernio
Zernio
API Reference

Posting Analytics

Cross-platform

Get post analyticsGETAnalytics changed since a cursorGETSync an external postPOSTGet post analytics timelineGETGet daily aggregated metricsGETGet best times to postGETGet content performance decayGETGet frequency vs engagementGETGet follower statsGET

Instagram

Get Instagram insightsGETGet Instagram follower historyGETGet Instagram demographicsGET

YouTube

Get YouTube channel insightsGETGet YouTube daily viewsGETGet YouTube video retention curveGETGet YouTube demographicsGET

LinkedIn

Get LinkedIn aggregate statsGETGet LinkedIn org analyticsGETGet LinkedIn post statsGETGet LinkedIn post reactionsGET

Facebook

Get Facebook Page insightsGETGet Facebook post reactionsGETGet Facebook post monetization earningsGET

TikTok

Get TikTok account-level insightsGET

Google Business

Get Google Business Profile performance metricsGETGet Google Business Profile search keywordsGET

Other

Get an analytics dashboardGET
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources
Posting Analytics

Sync an external post

Fetch an account's latest external posts (published directly on the platform, not through Zernio) on demand, so a newly published post is retrievable within seconds instead of waiting for the background sync (which refreshes each account at most every ~90 minutes).

Primary use case: verifying a submitted post. When a user publishes on the platform and immediately pastes the post URL into your app, call this with accountId plus url (or postId) to confirm the post exists and return its metadata.

Behavior:

  • Account access and connection state are checked before any platform call, including requests inside the debounce window.
  • Inactive accounts or accounts marked needsReconnection return 409 with code ads_connection_required. Stop scheduled retries for that account until it is reconnected, then read GET /v1/accounts for its current account ID.
  • For connected accounts, we fetch the latest posts live from the platform, then match and return the submitted post.
  • Requests are debounced per account (~15s): if the account was synced inside that window, the live fetch is skipped.

accountId is required, because a post URL or id alone cannot be resolved to an account, and the account must be connected to Zernio (we use its token to read the platform). Supported for every platform with a listing API (Instagram, Facebook, TikTok, YouTube, X, Threads, Pinterest, Reddit, Bluesky, Google Business Profile, and LinkedIn organization accounts).

LinkedIn personal profiles: LinkedIn has no listing API for personal profiles, so a url is REQUIRED and imports that single post. Pass any LinkedIn post URL (linkedin.com/posts/…, linkedin.com/feed/update/urn:li:activity:…) or a urn:li:share:… / urn:li:ugcPost:… URN. Works for posts published outside Zernio and before the account was connected, any age; the post must be authored by the connected member. Imported posts return full analytics (impressions, reach, reactions, comments, reshares, saves) and keep refreshing on the background analytics cycle, but carry no content/media (LinkedIn does not expose them for personal profiles). publishedAt comes from the post id: an activity URL (linkedin.com/feed/update/urn:li:activity:…) carries the publication time, while a urn:li:share:… / urn:li:ugcPost:… URN only carries the creation time, which is earlier for a post scheduled in another tool. Pass the activity URL for the exact publication date; importing it again for a post already imported by its URN corrects publishedAt on the same record. Right after an import, the per-post GET /v1/analytics?postId= read can show zeros for a few minutes while the metrics are ingested; this endpoint's response carries the fetched values immediately.

url accepts any format the platform uses (e.g. instagram.com/p/…, instagram.com/reel/…, youtu.be/…, youtube.com/shorts/…, tiktok.com/@user/video/…, vm.tiktok.com short links, pinterest.com/pin/… on any regional domain, and pin.it short links). Pass postId (the platform media/video/pin id) as an alternative locator.

Note: post-level analytics (reach, impressions) still carry the platform's own delay (e.g. ~24h on Instagram). This endpoint confirms the post exists and returns its metadata plus basic engagement (likes, comments), not delayed insights.


PlatformsTikTokLinkedInPinterest
POST
/v1/posts/sync-external

Authorization

bearerAuth
AuthorizationBearer <token>

API key authentication: send your Zernio API key in the Authorization header, prefixed with Bearer.

In: header

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

application/json

application/json

application/json

application/json

application/json

application/json

import Zernio from '@zernio/node';const zernio = new Zernio({ apiKey: process.env.ZERNIO_API_KEY });const { data } = await zernio.analytics.syncExternalPosts({  body: {    accountId: 'account_abc123',  },});console.log(data);
{  "synced": {    "postsFound": 0,    "postsSynced": 0,    "skipped": true  },  "found": true,  "post": {    "platform": "string",    "platformPostId": "string",    "platformPostUrl": "string",    "content": "string",    "publishedAt": "2019-08-24T14:15:22Z",    "mediaType": "string",    "thumbnailUrl": "string",    "mediaItems": [      {}    ],    "mediaProductType": "string",    "isAiGenerated": true,    "isSharedToFeed": true,    "mediaAudioType": "string",    "analytics": {      "likes": 0,      "comments": 0,      "shares": 0,      "saves": 0,      "sends": 0,      "clicks": 0,      "views": 0,      "reach": 0,      "impressions": 0,      "engagementRate": 0,      "lastUpdated": "2019-08-24T14:15:22Z"    }  },  "posts": [    {      "platform": "string",      "platformPostId": "string",      "platformPostUrl": "string",      "content": "string",      "publishedAt": "2019-08-24T14:15:22Z",      "mediaType": "string",      "thumbnailUrl": "string",      "mediaItems": [        {}      ],      "mediaProductType": "string",      "isAiGenerated": true,      "isSharedToFeed": true,      "mediaAudioType": "string",      "analytics": {        "likes": 0,        "comments": 0,        "shares": 0,        "saves": 0,        "sends": 0,        "clicks": 0,        "views": 0,        "reach": 0,        "impressions": 0,        "engagementRate": 0,        "lastUpdated": "2019-08-24T14:15:22Z"      }    }  ]}
Was this page helpful?

Analytics changed since a cursor

Cursor feed of the analytics snapshots that CHANGED, across every account you can read, in one paginated stream. Built for integrations that would otherwise call `GET /v1/analytics` once per connected account. Each page carries changes from many accounts at once, so your call count scales with how much actually changed rather than with how many accounts you have. Measured against a fleet of roughly 1,600 connected accounts: about 1,599 per-account analytics calls an hour became about 205 delta calls an hour, a 7.8x reduction. **Bootstrap once, then stay in sync.** Take the cursor FIRST: call this endpoint with NO `cursor` and it answers with an empty `data` array plus the feed's current position in `nextCursor`. Then load your baseline from `GET /v1/analytics`, the historical endpoint, because this one is a rolling 7-day change log and cannot replay history. Then resume from the cursor you took before the baseline. Taking the cursor afterwards instead drops every change that lands while the baseline is loading: it is in neither the row you already read nor the feed you resume behind it. The overlap this order creates is safe, because metrics are absolute values rather than increments, so draining it leaves every post on its newest value. `nextCursor` is present on every response, empty pages included, so you always have something to advance with. **Ordering.** Entries come back oldest first, in the order the feed received them. That order is NOT `syncedAt`: `syncedAt` is stamped when an account's sync cycle started, and a slow cycle writes its rows after a faster cycle that started later, so `syncedAt` can go backwards between consecutive entries. Do not sort, filter or resume on it. The cursor is the only stable position, and it is opaque: pass it back verbatim, and do not parse, construct or compare cursors. **`hasMore: false` does not mean the feed ended.** This stream has no end and `nextCursor` is never null. `hasMore: true` means more changes are already waiting, so call again straight away. `hasMore: false` means you are caught up: keep the cursor and poll again on your normal interval. **The newest changes settle before they are served.** The feed deliberately holds back its last few seconds of writes, so that a row can never become visible behind a cursor you have already advanced past. A read issued the instant an `analytics.synced` webhook lands will therefore often return an empty page for that account. Do not read an empty page as "nothing changed": poll again with the SAME cursor you last used rather than advancing. **Repeats inside one instant.** A sync cycle occasionally records the same post twice at the same feed position. When that happens the feed delivers one of those rows, not both. Measured over a day of production traffic, about 1.3% of rows fall in such a group and 99.4% of those groups are identical rows, so this is far more often deduplication than loss. Metrics are absolute values rather than increments, so a later entry for the same post supersedes an earlier one. **Retention is 7 days.** Changes older than that leave the feed. A cursor older than 6 days is rejected with a `400` (a day of margin, because expiry is lazy). Recover the same way you bootstrapped: take a fresh cursor from a call to this endpoint with no `cursor`, then re-load from `GET /v1/analytics`, then resume from that cursor. A consumer that polls at least daily never reaches this. Pairs with the `analytics.synced` webhook, so changes can be read on notification instead of on a timer. That event carries no cursor of its own: keep using the `nextCursor` this endpoint gave you. Requires the same analytics access as `GET /v1/analytics`, and shares the stricter per-second rate-limit window applied to analytics endpoints.

Get post analytics timeline

Returns a daily timeline of analytics metrics for a specific post, showing how impressions, likes, and other metrics evolved day-by-day since publishing. Each row represents one day of data per platform. For multi-platform Zernio posts, returns separate rows for each platform. Requires the Analytics add-on.

accountId*string

SocialAccount ID whose posts to sync. Must be connected to Zernio.

url?string

The post URL to locate. Optional. Provide url or postId to return a specific post; omit both to refresh and return the account's recent posts.

postId?string

The platform post/media/video id to locate, as an alternative to url. Optional.