# Post schema (/docs/schema/post) The unified SocialCrawl post schema. Every field, its type, per-platform availability, and a machine-readable JSON Schema, generated from the canonical Zod source. Every SocialCrawl endpoint that returns a post gives you this exact shape, whatever the source platform. Write your parser once and the same code reads post data from every platform below. That is the unified schema: one contract instead of a dozen raw upstream formats. ## Field reference | Field | Type | Nullable | Description | | ----- | ---- | -------- | ----------- | | `id` | `string` | No | Platform-specific post ID (always a string; numeric upstream IDs are stringified) | | `url` | `string` | Yes | Direct URL to the post on the source platform | | `content.text` | `string` | Yes | Post caption, description, or text content | | `content.media_urls` | `string` or `string[]` | Yes | URL(s) of the primary media. Single string for video/photo posts; array of strings for carousels. | | `content.thumbnail_url` | `string` | Yes | URL to thumbnail image | | `content.duration_seconds` | `integer` | Yes | Video/clip duration in seconds (null for non-video posts) | | `author.username` | `string` | Yes | Author username | | `author.display_name` | `string` | Yes | Author display name | | `author.avatar_url` | `string` | Yes | URL to author profile picture | | `author.verified` | `boolean` | Yes | Whether the author account is verified | | `engagement.views` | `integer` | Yes | View count (if available). For Instagram video/reels this is the play count: the headline 'Views' the IG app shows. Instagram removed per-post play counts from its public web pages in August 2026, so `GET /v1/instagram/post` now fills the count automatically from a second source within the same call: videos and reels keep a numeric `views` with no extra endpoint or workaround. Note: since mid-July 2026 Instagram's play count is Instagram-only. It no longer includes Facebook crosspost views (Meta-side change; the app UI changed identically). For combined reach, also fetch the Facebook crosspost via `/v1/facebook/post`. | | `engagement.likes` | `integer` | Yes | Like / reaction count | | `engagement.comments` | `integer` | Yes | Comment count. Note: on `GET /v1/instagram/post` this is Instagram's `edge_media_to_parent_comment.count` (top-level comments); the IG list endpoints report the mobile `comment_count` total (includes replies). Both land on this same field. | | `engagement.shares` | `integer` | Yes | Share / repost / retweet count. On Instagram this is `reshare_count` (the paper-plane Share / Send count) and it exists only on Instagram's mobile surface, so it is returned by `/v1/instagram/post/stats`, `/v1/instagram/profile/reels/full`, and `/v1/instagram/profile/posts/full`. The web-sourced Instagram list and search endpoints return `null` (never fabricated). | | `engagement.saves` | `integer` | Yes | Save / bookmark count. `null` on Instagram: the save count is platform-private and Instagram exposes no numeric save metric on any surface (never fabricated). | | `flags.nsfw` | `boolean` | Yes | NSFW flag (null when platform does not surface) | | `flags.spoiler` | `boolean` | Yes | Spoiler flag (null when platform does not surface) | | `flags.pinned` | `boolean` | Yes | Pinned-to-profile flag (null when platform does not surface) | | `flags.deleted` | `boolean` | No | Whether the post is tombstoned (always present) | | `flags.likes_hidden` | `boolean` | Yes | Present and `true` when the creator has hidden the like count. `engagement.likes` is `null` in that case (never the platform's decoy preview number). Absent on normal posts. | | `flags.comments_hidden` | `boolean` | Yes | Present and `true` when the upstream reports the comment count as hidden. `engagement.comments` is `null` in that case. Absent on normal posts. | | `flags.shares_hidden` | `boolean` | Yes | | | `flags.views_hidden` | `boolean` | Yes | | | `flags.saves_hidden` | `boolean` | Yes | | | `published_at` | `string` or `integer` | Yes | Post creation timestamp as an ISO 8601 UTC string. When the upstream sent a Unix epoch it is converted here and the raw epoch is preserved under `post.ext.published_at_epoch` for one deprecation cycle. | ## Extension fields (`ext`) Platform-specific passthrough. Each field is present only on the platforms that expose it and is absent everywhere else, so treat every `ext.*` field as optional. These carry the richer, per-platform signals the unified leaves cannot hold, and the join keys that chain endpoints together. | Field | Type | Nullable | Description | | ----- | ---- | -------- | ----------- | | `ext.music_id` | `string` | Yes | TikTok music/clip id (exact string): pass to `/v1/tiktok/song/videos?clipId=` to find videos using the same sound | | `ext.author_id` | `string` | Yes | The creator's platform-native numeric user id. On TikTok search and list items, pass to `/v1/tiktok/profile?user_id=` for the creator's current follower count (survives username changes). On Instagram `search/reels` items it is present on every row from every serving source, accepted by `/v1/instagram/basic-profile?userId=`. On Facebook it appears when the upstream exposed a numeric actor id and no real handle. | | `ext.author_followers` | `integer` | Yes | The creator's follower count as embedded in the search payload itself, when the search source happens to carry one. On Instagram `search/reels` it is null on a plain call, because Instagram stopped sending follower counts in its search payload in August 2026; send `include=creator` (or `country`) and it is filled from the creator's profile, 2 credits per creator looked up. It stays null when that lookup does not resolve. | | `ext.author_following` | `integer` | Yes | | | `ext.author_posts_count` | `integer` | Yes | | | `ext.author_country` | `string` | Yes | The creator's country as they declare it on Instagram's About this account panel, as a country name (for example `Spain`). It is not where the reel was filmed. Present only on `/v1/instagram/search/reels` when the request sends `include=creator` (or `country`), filled by looking the creator up. Null when Instagram does not publish a country for the creator or the lookup did not resolve, and nothing is guessed; absent on a plain call. | | `ext.author_public_email` | `string` | Yes | The public contact email on the creator's profile. Present only on `/v1/instagram/search/reels` when the request sends `include=creator`, filled by looking the creator up. Null when the creator lists no public email or the lookup did not resolve; absent on a plain call. | | `ext.author_public_phone` | `string` | Yes | The public contact phone number on the creator's profile. Present only on `/v1/instagram/search/reels` when the request sends `include=creator`, filled by looking the creator up. Null when the creator lists no public phone or the lookup did not resolve; absent on a plain call. | | `ext.download_count` | `integer` | Yes | How many times the video has been saved to a device. TikTok only, and distinct from `engagement.saves`: a save keeps the video in a private collection on the platform, a download takes a copy off it, and TikTok counts the two separately. Present on TikTok post, search and post-list responses; absent everywhere else. | | `ext.region` | `string` | Yes | TikTok only: the country TikTok registers the video to, as an ISO 3166-1 alpha-2 code (normally the creator's account country when they posted). It is not the viewer's country, not the `region` you requested, and not a language. On `/v1/tiktok/trending`, `/v1/tiktok/search`, `/v1/tiktok/search/top`, `/v1/tiktok/profile/videos` and `/v1/tiktok/search/hashtag` it is how you tell which rows are from a given country: filter on it when you need only that country. A row whose source omits the country stays null. The `region` request parameter still only sets the proxy. | | `ext.subreddit` | `string` | Yes | Reddit subreddit name (search/list items): pass to `/v1/reddit/subreddit/details?subreddit=` | | `ext.title` | `string` | Yes | | | `ext.selftext` | `string` | Yes | | | `ext.upvote_ratio` | `integer` | Yes | Reddit only: the share of a post's votes that are upvotes, as a fraction from 0 to 1 and usually close to 1 (six live rows measured 1, 1, 1, 0.86, 0.75, 1). Fractional despite the integer type this schema emits for every numeric leaf, the same caveat as `author.ext.average_rating`. Populated on `/v1/reddit/search` rows whose source carries it, and null everywhere else on Reddit including `/v1/reddit/post` and `/v1/reddit/subreddit`. | | `ext.flair` | `string` | Yes | | | `ext.content_language` | `string` | Yes | The language the platform itself tags the post with, when it sends one: Reddit's own language tag on Reddit lanes, and TikTok's caption-language tag (for example `de`) on `/v1/tiktok/trending?feed=local`. Null when the platform could not tell (Reddit `und`, TikTok `un`, common on short or emoji-only captions); absent where the source sends no tag. Never inferred from the text by us: that is `post.computed.language`. | | `ext.type` | `string` | Yes | | | `ext.content_type` | `string` | Yes | | | `ext.ticker_symbols` | `string[]` | Yes | | | `ext.video_view_count` | `integer` | Yes | Instagram's legacy 3-second-view count. Historical: Instagram stopped serving it in August 2026 (removed from its web post surface along with the web play count), so fresh `/v1/instagram/post` lookups no longer carry it and no source can restore it. Use `engagement.views` (the play count) instead. | | `ext.ig_play_count` | `integer` | Yes | Instagram-only play count (`ig_play_count`). Since mid-July 2026 Instagram's headline play count (`engagement.views`) no longer includes Facebook crosspost views and equals this value; it is surfaced explicitly so you can tell the Instagram-only figure apart and detect any future re-divergence. For combined Instagram + Facebook reach, also fetch the Facebook crosspost via `/v1/facebook/post`. Present on `/v1/instagram/post/stats` and, since the August 2026 views fix, on `/v1/instagram/post` for video posts. | | `ext.repost_count` | `integer` | Yes | | | `ext.media_type` | `string` | Yes | | | `ext.text_truncated` | `boolean` | Yes | | | `ext.ip_location` | `string` | Yes | | | `ext.carousel_count` | `integer` | Yes | | | `ext.remix_count` | `integer` | Yes | | | `ext.audio_cluster_id` | `string` | Yes | Instagram's audio cluster for a reel, as an exact digit string: the id Instagram uses to group different uploads of the same sound. Present on `/v1/instagram/audio/reels` rows and on `/v1/instagram/profile/reels` with `include=stats`; absent on a row when it is not available and on every other surface. | | `ext.facebook_likes` | `integer` | Yes | Instagram only: the Facebook cross-post share that was EXCLUDED from `engagement.likes` on this row. A post shared to Facebook arrives from some sources with its Facebook likes already folded into the Instagram count, so the share is subtracted and published here, which keeps `engagement.likes` the Instagram-only figure instagram.com shows on the post. Add the two together for the combined number. Absent when the read carried no Facebook share, and absent on a post whose owner hides the like count, because nothing can be subtracted from a null. A response that excluded a share also carries a note in `data._warnings`. | | `ext.facebook_comments` | `integer` | Yes | Instagram only: the Facebook cross-post share that was EXCLUDED from `engagement.comments` on this row, the comment counterpart of `post.ext.facebook_likes`. Each leaf appears only for the count it actually changed, so a post whose likes are hidden but whose comments were reconciled carries this one alone. | | `ext.published_at_epoch` | `integer` | Yes | Raw Unix epoch for `published_at` (seconds, or milliseconds when the upstream sent millis). Present only when the upstream sent a numeric epoch that was normalised to the ISO 8601 `published_at` string. Kept for one deprecation cycle for integrations pinned to the numeric form. | | `ext.usertags` | `string[]` | Yes | | | `ext.coauthors` | `string[]` | Yes | Instagram collaborative posts (the native "Collab" feature). The full list of co-author accounts on the post, as `{ id, username, full_name, is_verified, profile_pic_url }`. A collab post has ONE producer and appears in every co-author's grid, so `post.author` is whichever account created it, which is not necessarily the profile you queried. The complete set of accounts on a post is `post.author.username` plus every `username` in this array. An empty array means Instagram reports the post as NOT a collab; the field is absent on surfaces that carry no co-author signal, including `/v1/instagram/post` (its web source ships the field permanently empty, so use `/v1/instagram/post/stats` for a single post). | | `ext.music` | `string` | Yes | | | `ext.sponsor_tags` | `string[]` | Yes | | | `ext.location` | `string` | Yes | | | `ext.quoted_post` | `string` | Yes | Threads quote posts: the post this one quotes, as `{ id, url, author: { id, username }, text, media_urls, thumbnail_url }`. A quote post usually carries no caption or media of its own, and the text and video you see on the post's page belong to this attachment, by a different author. It is returned here rather than merged into `post.content`, so `content.*` always stays the outer post's own content and one account's media is never attributed to another. Absent on posts that are not quote posts. | | `ext.retweeted_post` | `string` | Yes | | | `ext.quote_count` | `integer` | Yes | | | `ext.all_media_urls` | `string[]` | Yes | | | `ext.reaction_counts` | `string[]` | Yes | | | `ext.share_urn` | `string` | Yes | | | `ext.post_type` | `string` | Yes | | | `ext.activity_id` | `string` | Yes | | | `ext.published_at_precision` | `string` | Yes | | | `ext.author_urn` | `string` | Yes | | | `ext.author_headline` | `string` | Yes | | | `ext.author_type` | `string` | Yes | | | `ext.is_repost_quote` | `boolean` | Yes | | | `ext.article` | `string` | Yes | | | `ext.reaction_type` | `string` | Yes | | | `ext.download_media_urls` | `string[]` | Yes | | | `ext.tags` | `string[]` | Yes | | | `ext.categoryId` | `string` | Yes | | | `ext.categoryTitle` | `string` | Yes | | | `ext.topicCategories` | `string[]` | Yes | | | `ext.duration` | `string` | Yes | | | `ext.license` | `string` | Yes | | | `ext.madeForKids` | `boolean` | Yes | | | `ext.defaultAudioLanguage` | `string` | Yes | | | `ext.hasPaidProductPlacement` | `boolean` | Yes | | | `ext.caption` | `string` | Yes | | | `ext.position` | `integer` | Yes | | | `ext.playlistId` | `string` | Yes | | | `ext.videoOwnerChannelId` | `string` | Yes | | | `ext.videoPublishedAt` | `string` | Yes | | | `ext.description` | `string` | Yes | | | `ext.default_language` | `string` | Yes | | | `ext.localizations` | `string` | Yes | | | `ext.playlist_item_id` | `string` | Yes | | | `ext.playlist_owner_channel_id` | `string` | Yes | | | `ext.playlist_owner_title` | `string` | Yes | | | `ext.channel_id` | `string` | Yes | | | `ext.published_label` | `string` | Yes | | | `ext.published_precision` | `string` | Yes | | | `ext.title_maybe_translated` | `boolean` | Yes | | | `ext.video_count` | `integer` | Yes | | | `ext.commerce` | `string` | Yes | | | `ext.on_screen_texts` | `string[]` | Yes | | | `ext.topic_tag` | `string` | Yes | | | `ext.reshare_count` | `integer` | Yes | | | `ext.dsp_ids` | `string` | Yes | | | `ext.topic_tag_id` | `string` | Yes | | | `ext.amazon_shop_lists` | `string[]` | Yes | | | `ext.amazon_shop_trending_picks` | `string[]` | Yes | | | `ext.amazon_shop_curations` | `string[]` | Yes | | | `ext.amazon_shop_socials` | `string[]` | Yes | | | `ext.ad` | `string` | Yes | Ad Library envelope (Facebook `adlibrary/ad` and `adlibrary/company/ads` items): page_id, currency, spend, reach_estimate, publisher_platforms, categories, targeted_or_reached_countries, cta_text, cta_type, link_url, is_active, end_date_iso, display_format, title, video_hd_url, video_sd_url. Absent on every non-ad surface. | | `ext.apple_music` | `string` | Yes | | | `ext.feedback_id` | `string` | Yes | | | `ext.question_id` | `string` | Yes | Naver KnowledgeiN (`/v1/naver/kin/search`) only: the question's id (Naver's `docId`), read from the row's link. Kin returns one row per answer, so several rows can share it; send `group=question` for one row per question. | | `ext.answer_no` | `integer` | Yes | Naver KnowledgeiN only: which answer of the question this row is (Naver's `answerNo`), read from the row's link. | | `ext.answer_nos` | `integer[]` | Yes | Naver KnowledgeiN with `group=question` only: every answer number of the question that was on the page, best-ranked first. | | `ext.published_at_est` | `string` | Yes | Naver KnowledgeiN only: the question's date ESTIMATED from its id (`YYYY-MM-DD`, Korea time), never a published date, so `published_at` stays null. Absent for questions older than the calibration table. | | `ext.date_basis` | `string` | Yes | How `post.ext.published_at_est` was derived. `kin_docid_estimate`: interpolated from the question id against question pages whose posted date was read. | | `ext.event` | `string` | Yes | | | `ext.trend` | `string` | Yes | Trend-board envelope: what TikTok's own trend board says about the row. On `/v1/tiktok/hashtags/popular`: `rank`, `country_code`, `period_days`, `posts` and `views` for the window (not lifetime totals), `industry` / `industry_id` / `industry_label` for the first board the hashtag was read from (null for the overall board), `boards` listing EVERY board it ranks on as `{ industry, industry_id, industry_label, rank }` in board order (so a hashtag in the top 3 overall and on an industry board shows both), `popularity_curve` as `{ date, value }` with value 0 to 100 relative to the window's peak, and `top_creators`. On `/v1/tiktok/videos/popular`: `rank`, `country_code`, `period_days`, `order_by`, `period_views`, `organic_views`, `engagement_rate`, `six_second_view_through_rate`, `content_tags`. Absent on every non-board surface. | | `ext.updated_at` | `string` | Yes | | ## Platform availability 27 platforms return the `Post` shape. `id`, `flags.deleted` are never null on any platform. The fields below vary: **yes** means the platform populates it (the value may still be null); a blank means the platform never provides it, so it is always null.
| Platform | `author.verified` | `engagement.saves` | `engagement.shares` | `content.duration_seconds` | `engagement.views` | `author.avatar_url` | `engagement.comments` | `content.media_urls` | `url` | `engagement.likes` | `content.thumbnail_url` | `flags.pinned` | | -------- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Amazon | yes | | | | | yes | | | | | yes | | | Apple Music | | | | yes | | | | yes | yes | | yes | | | Bluesky | | | yes | | | yes | yes | | | yes | | | | Douyin | yes | yes | yes | yes | | yes | yes | yes | yes | yes | yes | yes | | Facebook | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | | GitHub | | | yes | | | yes | yes | yes | yes | yes | | | | Google | | | | | yes | | | yes | yes | | yes | | | Hacker News | | | | | | | yes | yes | | yes | | | | Instagram | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | | Kick | yes | yes | yes | yes | yes | yes | yes | yes | | yes | yes | | | Kwai | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | | LinkedIn | | | yes | yes | yes | yes | yes | yes | yes | yes | yes | | | Naver | | | | | | | | yes | yes | | yes | | | Pinterest | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | | Product Hunt | | | | | | | | | yes | | | | | Quora | | | yes | | yes | | yes | | yes | yes | | | | Reddit | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | Rumble | | | | yes | yes | | | | yes | yes | yes | | | Spotify | | | | yes | yes | yes | | yes | | | yes | | | Telegram | | | | yes | yes | | | | yes | yes | yes | | | Threads | yes | | yes | | yes | yes | yes | yes | yes | yes | yes | | | TikTok | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | yes | | Truth Social | yes | yes | yes | yes | yes | yes | yes | | yes | yes | yes | | | Twitch | yes | yes | yes | yes | yes | yes | yes | yes | | yes | yes | | | Twitter/X | yes | yes | yes | yes | yes | yes | yes | yes | | yes | yes | | | Xiaohongshu | | yes | yes | yes | | yes | yes | yes | yes | yes | yes | yes | | YouTube | | | | yes | yes | yes | yes | yes | yes | yes | yes | |
## Machine-readable schema Validate responses programmatically against the JSON Schema (2020-12): [`/schemas/post.json`](/schemas/post.json) Point a validator (Ajv, `jsonschema`, or your framework's) at that URL, or hand it to an agent so it can check the shape without a live call. ## Returned by Endpoints with the `Post` / `PostList` archetype return this shape: [Amazon](/platforms/amazon) · [Apple Music](/platforms/apple_music) · [Bluesky](/platforms/bluesky) · [Douyin](/platforms/douyin) · [Facebook](/platforms/facebook) · [GitHub](/platforms/github) · [Google](/platforms/google) · [Hacker News](/platforms/hackernews) · [Instagram](/platforms/instagram) · [Kick](/platforms/kick) · [Kwai](/platforms/kwai) · [LinkedIn](/platforms/linkedin) · [Naver](/platforms/naver) · [Pinterest](/platforms/pinterest) · [Product Hunt](/platforms/producthunt) · [Quora](/platforms/quora) · [Reddit](/platforms/reddit) · [Rumble](/platforms/rumble) · [Spotify](/platforms/spotify) · [Telegram](/platforms/telegram) · [Threads](/platforms/threads) · [TikTok](/platforms/tiktok) · [Truth Social](/platforms/truthsocial) · [Twitch](/platforms/twitch) · [Twitter/X](/platforms/twitter) · [Xiaohongshu](/platforms/xiaohongshu) · [YouTube](/platforms/youtube) See how the same fields map to each platform's raw upstream names in the [cross-platform field equivalence table](/docs/schema/field-equivalence.md).