# Metric substitutions and structural nulls (/docs/metric-substitutions) Metric substitutions and structural nulls [#metric-substitutions-and-structural-nulls] SocialCrawl maps 49 platforms onto one canonical schema. Not every platform exposes every canonical field. When an upstream cannot supply a field, we follow one rule: **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.md). Metric substitutions [#metric-substitutions] Canonical fields hold the metric their name implies, or `null`. Closely-related metrics that do not fit a canonical slot belong under `ext.*` (see Spotify `monthly_listeners`). GitHub is the remaining exception: `post.engagement.likes` carries **reactions** (issues/PRs/comments) or **stars** (repos), because GitHub has no like primitive. Treat those values as GitHub's approval signal, not Instagram-style likes. 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. `post.engagement.likes` is a substitution: issue/PR/comment **reactions** or repo **stars** (GitHub has no like primitive). See Metric substitutions above. * **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.