# Post schema (/docs/schema/post) {/* AUTO-GENERATED. Do not hand-edit. Source: packages/social-api/src/docs/generate-schema-pages.ts Regenerate with: pnpm -F @repo/social-api generate:docs Field tables derive from the canonical Zod schemas (packages/social-api/src/schemas/canonical.ts); availability + equivalence derive from the registry fieldMaps. */} Post schema [#post-schema] 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-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`) [#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. Historical on Instagram `search/reels`: Instagram stripped the follower count out of the search payload in August 2026, so it is now null on every Instagram row and no source can restore it. For a follower count call `/v1/instagram/profile?handle=` (1 credit, the exact current integer), or batch up to 50 creators through `POST /v1/prism/profiles`. | | `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.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.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 | | | `ext.music` | `string` | Yes | | | `ext.sponsor_tags` | `string[]` | Yes | | | `ext.location` | `string` | Yes | | | `ext.reaction_counts` | `string[]` | Yes | | | `ext.share_urn` | `string` | Yes | | | `ext.post_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.playlist_item_id` | `string` | Yes | | | `ext.playlist_owner_channel_id` | `string` | Yes | | | `ext.playlist_owner_title` | `string` | Yes | | | `ext.channel_id` | `string` | Yes | | | `ext.video_count` | `integer` | Yes | | | `ext.commerce` | `string` | Yes | | | `ext.on_screen_texts` | `string[]` | Yes | | | `ext.topic_tag` | `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 | | Platform availability [#platform-availability] 23 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.