# Metric substitutions (/docs/metric-substitutions) Where a platform's upstream cannot supply a canonical field, SocialCrawl returns an honest null rather than a fabricated value or a substituted metric. This page lists every structural null by platform. SocialCrawl maps 69 platforms onto one canonical schema, and not every platform exposes every canonical field. When an upstream cannot supply a field, the response returns an honest `null`. A canonical field never carries a fabricated value, and never carries a different metric wearing that 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 is calculated, see [Computed fields](/docs/computed-fields.md). ## The one substitution GitHub is the single exception to the rule above. `post.engagement.likes` carries **reactions** on issues, pull requests, and comments, or **stars** on repos, because GitHub has no like primitive. Treat those values as GitHub's approval signal, not Instagram-style likes. Every other close-but-different metric lives under `ext.*`. Spotify's `author.ext.monthly_listeners` is the reference case. ## Instagram Instagram needs more than one line because its nulls are media-type dependent. - `post.engagement.saves` is `null` on every surface except `post/stats` with `include=saves`, which reads the save and repost counts for one post. On lists no source carries them at list speed, so they stay `null` there. - `post.engagement.likes` and `.comments` are Instagram's own counts. A post that is also shared to Facebook carries its Facebook likes and comments in some upstream reads; those are subtracted, so the number matches what instagram.com shows on the post. Accounts that hide their like count return `null` rather than the placeholder Instagram sends. - When that subtraction happens, the response says so rather than leaving you to notice. `post.ext.facebook_likes` and `post.ext.facebook_comments` carry the Facebook share that was excluded, and a note in [`data._warnings`](/docs/response-schema.md#data_warnings-partial-data-channel) names the rows it affected. Add the two together if you want the combined figure. Both keys are absent on a post with no Facebook share, and each is absent on its own when nothing was excluded from that count. - `post.engagement.shares` is `null` on the base endpoints. A reshare count is available only 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, 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 that 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 do not own. ## Pinterest Pinterest splits its counts between the list surfaces and the pin itself. - Search rows carry no counts at all: `post.engagement.saves`, `.likes` (reactions), `.comments` and `.shares` are `null` on a plain `search` call. Add `include=engagement` and every row is filled from the per-pin lookup in the same call, at 1 extra credit per row filled. `/v1/pinterest/pin` returns the same counts for a single pin at 1 credit. - `post.engagement.views` is `null` on search rows. Pinterest does not publish a view count there. - Board rows carry saves, comments and shares, but no date: `post.published_at` is `null` on a plain `board` call, because the board payload has no date field. `include=engagement` fills it from the pin, along with a missing reaction count. - `post.content.text` is the pin's title, or its description when the title is empty. Many pins have neither, so `null` there means the pin genuinely carries no text. Pinterest's machine-generated image caption is never mapped into it. ## Threads Threads splits its numbers between the single-object surfaces and the lists, and the lists fill on request. - `post.engagement.views` is `null` on `user/posts` and `search`, because Threads publishes a view count on a single post's page only. `GET /v1/threads/post` returns it for one post at 1 credit, and `include=engagement` fills it on each row of either list in the same call, at 1 extra credit per row filled. On `search` that covers the first 20 rows. - A view count also does not exist yet on a **brand-new post**, on any surface: Threads publishes one once it has it. A row like that is free on `search`, where the join charges only for a landed view count or display name, and it still receives its pinned flag. Measured 12/09/2026 on a page of very fresh posts: 11 of 19 carried a view count, and a direct single-post call on the other 8 had none either. - `post.flags.pinned` is `null` on `search` rows, where the upstream omits the field rather than reporting false. The same `include=engagement` fills it. - `author.followers` and `author.bio` are `null` on `search/users`, because the account-search index carries identity only. `include=profile` fills both, plus the private flag and the bio link, at 1 extra credit per account filled. - `post.author.display_name` is `null` on the `user/posts` window and filled by `include=engagement`, or by asking for more than 15 posts, which moves the call to a source that carries it. - Every join bills only for rows that come back filled, refunds the rest, serves a recently fetched row from cache for nothing, and reports what it did in `data.hydration`. See [Threads](/docs/threads.md). ## LinkedIn LinkedIn needs its own section because two `author` leaves are weaker on a people-list row than on the member's own profile, and both are recoverable on the same call. - `author.followers` on a `search/people`, `company/people`, or `post/reactions` row is LinkedIn's **rounded display bucket** rather than a census: 39,000 for a member whose real count is 39,278, or 2,000 for one on 2,344. Those rows set `author.ext.followers_approximate` to `true`, so you can always tell a published bucket from an exact figure. Many rows publish no figure at all, and `author.followers` is `null` there rather than guessed at. - `author.following` is `null` on every plain people-list row. - Neither is a permanent null. Send `include=profile` and each row is joined to that member's own profile lookup inside the one call: `author.followers` becomes the exact follower count, `author.following` is filled with the member's connection count (on LinkedIn a member's following number is the number of connections), and `author.location`, `author.joined_at`, and the country, website, cover image and profile flags on `author.ext` land where the row lacks them. Every row the lookup fills also sets `author.ext.followers_approximate` to `false`, including rows that arrived with no count at all, so the flag answers "is this figure exact" on every row rather than only on the ones that came in rounded. A row the lookup could not fill keeps the flag it arrived with. The list stays 10 credits and the join adds 4 credits for each row it fills, up to 50. Rows already in cache are free, rows it cannot fill are refunded, and `limit` (1 to 10) caps the rows and the extra credits together. See [LinkedIn](/docs/linkedin.md). ## Facebook Facebook publishes several per-post numbers on the post's own permalink and not on the list that names it, so the list rows arrive `null` there until you ask for the join. - `post.engagement.shares` is `null` on every `profile/posts` row, and `post.engagement.views` and `post.content.duration_seconds` are `null` on every reel in that feed, because Facebook carries all three on the individual post only. Send `include=engagement` and each row is filled from its own post record in the same call, at 1 extra credit per row filled. A post Facebook publishes no share count for stays `null` rather than being given a `0`, and that row is free. - `post.engagement.saves` is `null` everywhere. Facebook publishes no save count on any surface. - A `profile/photos` row is the image, its caption and its permalink: `post.author.display_name`, `post.author.avatar_url`, `post.engagement.likes`, `.comments`, `.shares` and `post.published_at` are all `null` on a plain call. `include=details` fills them from that photo's own post, at 1 extra credit per photo filled. A photo posted on its own carries the post's counts, while a photo inside a multi-photo post carries its own, which are usually far lower. Photos have no view count, so `post.engagement.views` stays `null` either way. - An event has no views and no likes, so on `profile/events` and `events` the RSVP counts ride those two leaves: `post.engagement.views` carries the interested count and `post.engagement.likes` the going count, with the same numbers under their own names on `post.ext.event.interested_count` and `going_count`. The substitution is the same for values the row arrives with and values `include=details` fills. - `post.ext.event.end_timestamp` and `post.ext.event.ticket_url` are a genuine platform null on both event lists, with or without the join. Facebook published neither on any event measured, so the join does not promise them. - Every Facebook join bills only for rows that come back filled, refunds the rest, serves a recently fetched row from cache for nothing, and reports what it did in `data.hydration`. See [Fill the rows, or fetch them?](/docs/choosing-endpoints.md#facebook-include). ## 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. | Platform | Fields that stay `null` | Why | | ------------------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | YouTube | `post.engagement.shares`, `post.engagement.saves` | Both are owner-only analytics upstream | | Reddit | `post.engagement.views`, `.shares`, `.saves` | Reddit exposes no view or share counts, and "saved" is a per-viewer flag upstream, never a count | | Reddit | `author.verified` | Not exposed | | Facebook | `post.engagement.saves` | Not exposed | | Facebook, list surfaces | `author.avatar_url`, `post.media_urls` | Slimmer rows; a photo's author fills with `include=details` | | Naver Search corpora | `post.engagement.views`, `.likes`, `.comments` | The corpora are search indexes, not social feeds, so there are no engagement signals to return | | Naver Search corpora | `author.avatar_url`, `post.media_urls`, `post.published_at` | These vary by corpus and are `null` on the corpora that do not carry them, for example news and image differ from blog | | Spotify artists | `author.posts_count` | Spotify artists have no posts, see the note below | | Spotify artists | `author.following`, `author.likes_count`, `author.verified` | Not exposed upstream | | Spotify, non-artist rows | `post.engagement.*` on album, track, and podcast rows | Those rows carry no engagement block | | Google Finance | `quote.price.previous_close` | `null` when the upstream quote omits it, a structural null rather than a fixed value | | Google Shopping | `product.brand` | Google Shopping returns no clean brand field | | Rumble, search rows | `post.engagement.*`, `author.avatar_url`, `post.published_at` | SERP rows carry none of them | | Threads | `author.following`, `author.posts_count`, `author.likes_count`, `author.joined_at` | Threads publishes none of the four on a public profile | | Threads | `comment.parent_id` | Nothing upstream says which reply a nested reply answers | | GitHub | `author.avatar_url` on several archetypes | Not exposed | | TikTok | `post.parent_id` on root comments | By design, only replies carry a parent | | Google Business | `author.username` on `business/updates` rows | Not carried on those rows | Three of those rows have a workaround worth knowing: - **Spotify** surfaces the headline reach metric 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. - **Google Finance** `previous_close` can be derived client-side from the current price and the change when you need it. - **Google Shopping** brand can often be lifted from `product.specifications[]`. Only Amazon products carry a populated `brand`. One field is a default rather than a null: the Twitter/X single-tweet shape carries no `post.pinned` flag, so it defaults to `false`. ## Changed defaults Two endpoints apply a SocialCrawl-chosen default when you omit a sort or type parameter, so results are predictable: - **`google_play/app-reviews`** and **`app_store/app-reviews`** default `sort_by` to newest-first. Google Play's own upstream default is "most relevant", and it is overridden so that an unspecified sort returns the newest reviews. The per-store value is `newest` for Google Play and `most_recent` for the App Store. An explicit `sort_by` always wins. - **`instagram/search/hashtag`** defaults `type` to `top`. Pass `type=recent` or `type=clips` to change it. ## Next steps - [Computed fields](/docs/computed-fields.md): How engagement_rate, language, category, and reach are derived. - [Unified schema](/docs/schema.md): Every canonical field on every object. - [Schema compatibility](/docs/schema-compatibility.md): What may change in a published schema, and what never does. - [Response schema](/docs/response-schema.md): The envelope, headers, and partial-data warnings.