SocialCrawl

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 68 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.

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 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.

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.

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?.

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.

PlatformFields that stay nullWhy
YouTubepost.engagement.shares, post.engagement.savesBoth are owner-only analytics upstream
Redditpost.engagement.views, .shares, .savesReddit exposes no view or share counts, and "saved" is a per-viewer flag upstream, never a count
Redditauthor.verifiedNot exposed
Facebookpost.engagement.savesNot exposed
Facebook, list surfacesauthor.avatar_url, post.media_urlsSlimmer rows; a photo's author fills with include=details
Naver Search corporapost.engagement.views, .likes, .commentsThe corpora are search indexes, not social feeds, so there are no engagement signals to return
Naver Search corporaauthor.avatar_url, post.media_urls, post.published_atThese vary by corpus and are null on the corpora that do not carry them, for example news and image differ from blog
Spotify artistsauthor.posts_countSpotify artists have no posts, see the note below
Spotify artistsauthor.following, author.likes_count, author.verifiedNot exposed upstream
Spotify, non-artist rowspost.engagement.* on album, track, and podcast rowsThose rows carry no engagement block
Google Financequote.price.previous_closenull when the upstream quote omits it, a structural null rather than a fixed value
Google Shoppingproduct.brandGoogle Shopping returns no clean brand field
Rumble, search rowspost.engagement.*, author.avatar_url, post.published_atSERP rows carry none of them
Threadsauthor.following, author.posts_count, author.likes_count, author.joined_atThreads publishes none of the four on a public profile
Threadscomment.parent_idNothing upstream says which reply a nested reply answers
GitHubauthor.avatar_url on several archetypesNot exposed
TikTokpost.parent_id on root commentsBy design, only replies carry a parent
Google Businessauthor.username on business/updates rowsNot 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

On this page