# Metric substitutions and structural nulls (/docs/metric-substitutions)



Metric substitutions and structural nulls [#metric-substitutions-and-structural-nulls]

SocialCrawl maps 48 platforms onto one canonical schema. Not every platform exposes every canonical field. When an upstream cannot supply a field, we follow one rule: &#x2A;*return an honest `null`, never a fabricated value, and never a different metric wearing the field's name.** Where a platform exposes a closely-related metric worth having, it goes under an `ext.*` namespace so the canonical leaf keeps meaning exactly what it says.

This page is the source of truth for those cases. If a field is listed here as a structural null for a platform, its absence is by upstream design, not a bug.

For how the derived `computed` block (engagement\_rate, language, content\_category, estimated\_reach) is calculated, see [Computed fields](/docs/computed-fields).

Metric substitutions [#metric-substitutions]

There are currently no metric substitutions anywhere in the API. Every canonical field holds the metric its name implies, or `null`. Where a platform exposes a closely-related metric that does not fit a canonical slot, it is namespaced under `ext.*` rather than folded into the nearest-looking leaf — see the Spotify entry below for the one case that used to work the other way.

Structural nulls by platform [#structural-nulls-by-platform]

These canonical fields are `null` across a platform's surfaces because the upstream never exposes them. They will not populate no matter how you call the endpoint.

Instagram [#instagram]

* `post.engagement.saves` is always `null`. Instagram exposes only a per-viewer `viewer_has_saved` boolean, never an aggregate count. This is platform-private.
* `post.engagement.shares` is `null` on the base endpoints. A reshare count is only available on `post/stats` and the `*-full` composites (`instagram/profile/reels/full`, `instagram/profile/posts/full`), which pull the mobile `reshare_count`. Read shares from those surfaces.
* `post.engagement.views` (and the shares count above) exist only on **video media**: reels, videos, and carousels containing a video. Photo posts and photo-only carousels have no public view or share count on Instagram, so both fields are `null` there on every endpoint, and the views-derived `computed.engagement_rate` and `computed.estimated_reach` are `null` with them. Likes and comments populate on every media type.
* **Stories carry no engagement metrics at all.** Story views, replies, likes, and reach are owner-only analytics Instagram exposes to no third party. `/v1/instagram/stories` returns each active story's media, timestamps, and IDs with the engagement leaves `null`; no API from any provider can return story metrics or story reach for an account you don't own.

YouTube [#youtube]

* `post.engagement.shares` and `post.engagement.saves` are `null`. Both are owner-only analytics upstream.

Reddit [#reddit]

* `post.engagement.views`, `post.engagement.shares`, and `post.engagement.saves` are `null`. Reddit does not expose view or share counts, and "saved" is a per-viewer flag upstream, never a count.
* `author.verified` is `null` (not exposed).

Facebook [#facebook]

* `post.engagement.saves` is `null` (not exposed).
* On list surfaces, list-item `author.avatar_url` and `post.media_urls` can be `null` because the list-item shape is slimmer than the single-object shape.

Naver [#naver]

Naver's Search corpora (blog, news, cafe, book, webkr, image, kin, shop, doc, encyc) are search indexes, not social feeds, so:

* `post.engagement.views`, `post.engagement.likes`, and `post.engagement.comments` are `null` on every Naver Search corpus. There are no engagement signals to return.
* `author.avatar_url`, `post.media_urls`, and `post.published_at` vary by corpus and are `null` on the corpora that do not carry them (for example news and image differ from blog).

Spotify [#spotify]

* `author.posts_count` is `null` for artists. Spotify artists have no posts. The headline reach metric is surfaced separately as `author.ext.monthly_listeners` — it is not folded into `posts_count`, because a listener figure sitting in a "number of posts" leaf corrupts any cross-platform posts aggregate.
* `author.following`, `author.likes_count`, and `author.verified` are `null` for artists (not exposed upstream).
* Album, track, and podcast rows carry no `post.engagement.*`.

Google Finance [#google-finance]

* `quote.price.previous_close` is `null` when the upstream quote omits it. It is a structural null, not a fixed value. It can be derived client-side from the current price and change when you need it.

Google Shopping [#google-shopping]

* `product.brand` is `null`. Google Shopping does not return a clean brand field. Only Amazon products carry a populated `brand`. If you need the brand for a Shopping product, it can often be lifted from `product.specifications[]`.

Other platforms [#other-platforms]

* **Rumble**: search (SERP) rows carry no `post.engagement.*`, `author.avatar_url`, or `post.published_at`.
* **Threads**: slim search-user rows omit `author.avatar_url`, `author.bio`, and `author.followers`.
* **Twitter/X**: the single-tweet shape carries no `post.pinned` flag; it defaults to `false`.
* **GitHub**: `author.avatar_url` is `null` on several archetypes, and there is no like primitive so `post.engagement.likes` is `null`.
* **TikTok**: root comments carry no `post.parent_id` by design (only replies do).
* **Google Business**: `business/updates` rows carry no `author.username`.

Changed defaults [#changed-defaults]

Two endpoints apply a SocialCrawl-chosen default when you omit a sort/type parameter, so results are predictable:

* **`google_play/app-reviews`** (and `app_store/app-reviews`) default `sort_by` to newest-first when you omit it. Google Play's own upstream default is "most relevant"; we override it so an unspecified sort returns the newest reviews. The per-store value is `newest` for Google Play and `most_recent` for the App Store. Passing an explicit `sort_by` always wins.
* **`instagram/search/hashtag`** defaults `type` to `top` when you omit it. Pass `type=recent` or `type=clips` to change it.
