The reference report's Social section (FB visits, IG profile visits, + engagement) needs on-platform
numbers GA4 can't see (impressions, reach, follows, likes). Two ways to get them; we choose the
developer way: go direct to each platform's own API, which is free for reading your OWN
account's analytics on every platform except X/Twitter (the one paid exception — handled below).
We skip Blotato-as-analytics (it's a publisher, not an analytics source) — BUT Blotato stays useful
for one thing: it returns the platform post ID when it publishes, which is how we attribute an
on-platform metric back to a calendar cell (see "Attribution" at the bottom).
Universal free fallback (already built): every platform's LINK CLICKS + downstream sessions +
conversions come free via the UTM→GA4 path (scripts/utm_links.py+metrics/ga4). So the
Reports tab shows per-channel traffic + revenue with ZERO social APIs. The native APIs below add
the on-platform layer (impressions/reach/follows/engagement) that GA4 cannot see. Do them in
difficulty order; ship value at every tier.
We already hold a Google OAuth token (secrets/gws/google_token.json) for GA4/GSC. YouTube rides the
SAME token — just add two scopes and re-run the consent.
youtubeAnalytics.reports.query) + Data API v3 (channel/video lists).scripts/_google_oauth.py SCOPES: https://www.googleapis.com/auth/yt-analytics.readonly and https://www.googleapis.com/auth/youtube.readonly,
then python scripts/_google_oauth.py and re-approve as temerariimedia. No new app, no key.
likes · comments · shares · impressions + impressionClickThroughRate (Analytics API). Per-video
via dimensions=video.
engine/integrations/metrics/social_youtube.py → googleapiclient.build('youtubeAnalytics','v2')..env: BLUESKY_HANDLE, BLUESKY_APP_PASSWORD. POST com.atproto.server.createSession → accessJwt.
likeCount · repostCount · replyCount · quoteCount via app.bsky.feed.getAuthorFeed?actor=<handle>. Follower count via app.bsky.actor.getProfile.
No impressions/reach (Bluesky doesn't expose them) → that cell renders "n/a (platform)".
social_bluesky.py (stdlib urllib; createSession → getAuthorFeed → sum counts/day).This is where the reference's "Facebook visits / Instagram profile visits" come from. Free API; the
friction is the App Review + business verification.
Business/Creator account linked to that Page · a long-lived Page Access Token**.
pages_read_engagement, read_insights, instagram_basic, instagram_manage_insights; for Threads add threads_basic, threads_manage_insights.
GET /{page-id}/insights?metric=page_impressions,page_views_total,page_fans,page_post_engagements.GET /{ig-user-id}/insights?metric=reach,impressions,profile_views,follower_count + per-media GET /{media-id}/insights?metric=impressions,reach,saved,likes,comments.
GET /{threads-user-id}/threads_insights?metric=views,likes,replies,reposts,quotes.META_PAGE_TOKEN, META_PAGE_ID, META_IG_USER_ID, META_THREADS_USER_ID in .env. Refresh long-lived token before expiry.
social_meta.py (FB+IG), social_threads.py (shares the token).Standard access after a short review).
pins:read, boards:read, user_accounts:read. Store PINTEREST_ACCESS_TOKEN.
GET /v5/user_account/analytics?metric_types=IMPRESSION,SAVE,PIN_CLICK,OUTBOUND_CLICK (account-level) + per-pin GET /v5/pins/{pin_id}/analytics.
social_pinterest.py.Community Management API product (approval can be slow/selective).
r_organization_social, r_organization_admin. Store LINKEDIN_ACCESS_TOKEN, LINKEDIN_ORG_URN (urn:li:organization:<id>).
organizationalEntityShareStatistics (impressions, clicks, likes, comments, shares, engagement rate) + networkSizes (followers) + organizationPageStatistics (page views).
social_linkedin.py.user.info.stats, video.list, analytics scopes. Store TIKTOK_ACCESS_TOKEN.shares, reach) via the Business API. (The basic Display API is lighter and gives less.)
social_tiktok.py. analytics reads. Basic = $100/mo**, Pro = $5,000/mo. Impressions (non_public_metrics) need the
owner context AND a paid tier.
sessions + conversions are already captured by utm_source=x), and pull on-platform numbers (likes,
RTs, impressions) by manual monthly export from X Analytics into the snapshot if you want them.
If X performance ever justifies $100/mo, social_x.py (v2 tweet.fields=public_metrics) is a small add.
GA4 sees UTM (so link clicks/sessions/revenue join automatically). But the native social APIs return
metrics per account/post, and platforms do NOT echo our UTM. To attribute an on-platform metric to
a calendar cell:
platform_post_id → {channel, campaign, cell} (a small content/_generated/post-ids.json).
rows keyed date · channel · campaign · cell · metric. Account-level metrics (followers, profile
views) store with campaign=null (they're not per-campaign).
This is the ONE reason Blotato stays in the loop — for the post-ID mapping, not for analytics.
Mirror metrics/resend.py + metrics/twilio.py:
verify() → reports auth state + exactly what .env keys are missing + free/paid/approval status.pull(start, end) → returns 0 rows until credentialed; then writes metrics-snapshot.json rows (date·channel·campaign·cell·metric·value) via engine.integrations.metrics.upsert.
utm_links.py → GA4 already gives per-channel traffic+revenue forALL 9 platforms. This is live the moment publishing starts.
_google_oauth.py) + enable the 2 APIs in GCP. (free)BLUESKY_HANDLE, BLUESKY_APP_PASSWORD. (free)META_* tokens.PINTEREST_ACCESS_TOKEN.LINKEDIN_*.TIKTOK_ACCESS_TOKEN.