Changelog

New endpoints, platforms, integrations, and fixes — everything we ship, dated and documented.

Breaking

Snapchat, Pinterest, Spotify, Twitch and Google Ad Library: native only, new status codes

Effective 2026-10-25. These endpoints now read only public pages and public endpoints; the extended fallback is gone, so source is always native and the extended per-result prices no longer apply. Error answers change: a target that doesn't exist is 404, malformed input is 400, and when the platform doesn't answer the call is 503, retryable, at 0 credits. Every 404, 400 and 503 is 0 credits. Fields: Snapchat hidden view counts are null instead of -1, Google advertiser-search adds adsCountIsApproximate, and analytics/post fills publishedAt for Pinterest pins. Prices per call, old → new: twitch/user-videos 2 on native or about 1.7 per video (minimum 2) on the extended source → flat 2; spotify/search and spotify/podcast-episodes 2 on native or about 1.15 per result (minimum 2) on the extended source → flat 2; google company-ads 2 on native or about 3.35 per ad on the extended source → flat 2; google ad-details 2 on native or 5 on the extended source → flat 2. Snapchat, Pinterest user-pins / user-boards / board / pin-details, Spotify artist / track / album / podcast, Twitch profile / user-schedule / clip and Google advertiser-search keep their native prices.

  • snapchat/user-profile: malformed username 400 (was 404); a name Snapchat shows no public profile for, or a page that isn't a profile, 404; Snapchat not answering 503 (was a slower extended retry, then 404)
  • snapchat/user-profile: spotlightHighlights[].engagement.views null when Snapchat hides the count (was -1)
  • pinterest user-pins, user-boards: malformed profile 400 (was 404); missing profile 404; Pinterest not answering 503 (was an extended retry, then 404 "No pins found" / "No boards found")
  • pinterest board: missing board 404; Pinterest not answering 503 (was an extended retry, then 404 "No pins found")
  • pinterest user-pins, board: an account or board with no pins is 200 with an empty list at 0 credits (was 404)
  • pinterest pin-details and analytics/post for Pinterest pins: no extended fallback; a pin the public page can't show is 404
  • analytics/post for Pinterest pins: publishedAt is the pin's creation time (was null)
  • twitch profile, user-videos, user-schedule: missing channel 404 (was 502); malformed channel 400 (was 502); Twitch not answering 503 (was 502)
  • twitch clip: missing clip 404; Twitch not answering 503 (was an extended retry)
  • twitch user-videos: a cursor that isn't a video id from this listing is 400 (was 200 with an empty page); filterBy / sortBy / cursor while Twitch doesn't answer 503 (was 502)
  • spotify search, podcast-episodes: Spotify not answering 503 (was the extended path, billed per result); podcast-episodes for a show that doesn't exist stays 404
  • spotify artist, track, album, podcast: when the web player data doesn't answer, Spotify's public embed data fills the response (as before); the extended step in between is gone
  • ad-library/google company-ads, ad-details, advertiser-search: Google not answering 503 (unchanged); a page that fails to load mid-lookup is 503 on ad-details (was sometimes 404 for a live creative)
  • ad-library/google ad-details: a creative that isn't under the given advertiser 404 AD_NOT_FOUND
  • ad-library/google advertiser-search: advertisers[].adsCountIsApproximate added (true for rounded counts of 1,000 or more and for ranges)
Breaking

GitHub and Spotify search: source is native

Effective 2026-10-25. data.source is native on /v1/github/trending-repositories, /v1/github/trending-developers, /v1/github/contributions and /v1/spotify/search, the same value as every other native response. GitHub's three endpoints used to name the github.com page the data came from (github.com/trending, github.com/trending/developers, github.com/users/{login}/contributions). Spotify search used to say pathfinder. Nothing else in these responses changes.

  • github/trending-repositories: source "github.com/trending" → "native"
  • github/trending-developers: source "github.com/trending/developers" → "native"
  • github/contributions: source "github.com/users/{login}/contributions" → "native"
  • spotify/search: source "pathfinder" → "native"
Breaking

Pinterest search: new result ids, no saves count, flat 2 credits

Effective 2026-10-25. /v1/pinterest/search now reads Pinterest's keyword search as a logged-out visitor sees it. Logged-out search names each pin by an opaque id (e1_…) instead of the numeric pin id, so results[].id and results[].url carry that id. IDs from search (e1_...) can't be passed to /pin-details. Search results already include title, description, link, images, created date, reactions, pinner and board. Saves, repin and share counts, the video flag and the original pinner are only in pin-details. results[].saves is removed: logged-out search doesn't show a save count. Price, effective 2026-10-25: each call is a flat 2 credits; it was 0.5 credits per result, rounded up (minimum 1 on the native source, 2 on the extended source). A call that returns 1 or 2 results can cost more: 2 credits where it was 1. 3 or 4 results cost the same, 2 credits. 5 or more results cost less: a full page of 25 was 13 credits and is now 2. No results is a 200 with an empty list at 0 credits. When Pinterest doesn't answer, the call is 503, retryable, at 0 credits; it no longer waits and then returns an empty list.

  • results[].id and results[].url: opaque e1_… id instead of the numeric pin id
  • IDs from search (e1_...) can't be passed to /pin-details; search results already include title, description, link, images, created date, reactions, pinner and board; saves, repin and share counts, the video flag and the original pinner are only in pin-details
  • results[].saves removed; reactionCount added
  • flat 2 credits per call (was 0.5 per result, rounded up): 1–2 results can cost more (1 → 2 credits), 3–4 the same, 5+ less; empty result 0 credits
  • truncatedReason: search-window (was serp-window)
  • Pinterest not answering: 503, retryable, 0 credits
Breaking

Transcription: a temporary speech-to-text outage is a retryable 502

When our speech-to-text step is unavailable, transcript endpoints now answer 502 UPSTREAM_UNAVAILABLE with retryable: true and a Retry-After header, at 0 credits. Before, the call ended in 500 INTERNAL_SERVER_ERROR, and Instagram transcripts could report no speech instead. If you branch on 500, also retry on 502.

  • tiktok/transcript, youtube/audio-transcript, instagram transcripts, video/transcript, video/summarize: speech-to-text unavailable is 502 UPSTREAM_UNAVAILABLE, retryable, Retry-After 60 (was 500)
  • instagram transcripts: the same outage no longer reads as "no speech"
  • 0 credits, as before
Breaking

Kick: native only, a fetch miss is 502, limit up to 100 returns full results

Effective 2026-10-24. /v1/kick/clip no longer has an extended fallback, so source is native or cache; the value extended is no longer returned. Breaking: when Kick can't be read, clip mode and channel mode answer 502 KICK_CLIPS_UNAVAILABLE (retryable, 0 credits) straight away; before, some of those calls were served by the extended path as a 200 with source extended. A clip or channel Kick does not have is still 404 CLIP_NOT_FOUND / CHANNEL_NOT_FOUND, an empty channel is still 200 at 0 credits, and a VOD or wrong-platform URL is still 400. Channel mode: limit up to 100 now returns up to limit clips, newest first; before, a native response held at most 20 clips whatever limit you passed. The default limit is now documented as 20 (it was documented as 30); the number of clips a default call returns is unchanged. Clips are always the full native shape (vod, creator, categoryBanner and parentCategory when Kick has them); the thinner extended rows are gone. Cached Kick responses from before this change are not served, so the first call per URL is a fresh fetch. Price unchanged: flat 1 credit on a 200 with rows.

  • source: native | cache; extended is no longer returned
  • Kick unreadable: 502 KICK_CLIPS_UNAVAILABLE, retryable, 0 credits (some were a 200 from the extended path)
  • unchanged: 404 CLIP_NOT_FOUND / CHANNEL_NOT_FOUND, empty channel 200 at 0 credits, 400 for VOD / wrong-platform URLs
  • channel mode: limit up to 100 returns up to 100 clips (a native response held at most 20)
  • limit default documented as 20 (was 30); default call returns the same count as before
  • clips: always the full native shape; thinner extended rows are gone
  • older cached responses are not served; first call per URL is a fresh fetch
  • price unchanged: flat 1 credit
Breaking

TikTok Shop: native-only search, new status codes, cheapest-SKU price

Shop Search reads only its native search window, with no slower fallback path. A query with no TikTok Shop products is 200 with an empty list at 0 credits; when the search window can't be read the call is 503, retryable, at 0 credits. Product Details answers 502 (retryable, Retry-After 30 seconds, 0 credits) when the product page could not be loaded, instead of 404; 404 now means the product is confirmed gone. On product pages without the full product data, price is the cheapest variant, so prices on multi-variant products can be lower than before. Product Reviews fills totalAvailable from the product's review count. TikTok Shop price fields are documented as number or null: a product that comes back without a price is a normal 2-credit response with price null, and only a call that returns no product data costs 0 credits.

  • tiktok-shop/shop-search: native window fails: 503 SERVICE_UNAVAILABLE, retryable, 0 credits (was: slower fallback path, 200 or 502)
  • tiktok-shop/shop-search: no products for the query: 200, products [], totalReturned 0, 0 credits (was: answered by the slower fallback path)
  • tiktok-shop/shop-search: windowKind is always serp-pdp (the value extended no longer appears); source is always native
  • tiktok-shop/shop-search: hasMore is true only when limit is below 10 and the window had more (no longer true on a full fallback page)
  • tiktok-shop/product-details: product page could not be loaded: 502 UPSTREAM_UNAVAILABLE, retryable, retryAfterSeconds 30 + Retry-After header, 0 credits (was 404 NOT_FOUND)
  • tiktok-shop/product-details: 404 NOT_FOUND only when the product is confirmed gone
  • tiktok-shop/product-details and shop-search: price is the cheapest variant and originalPrice that variant's list price when the page has no full product data; new priceSource value sku_min (was the first variant listed)
  • tiktok-shop/product-reviews: totalAvailable filled from the product's review count on the preview (was often null)
  • tiktok-shop endpoints: price, originalPrice and savingsAmount are typed number | null in the OpenAPI spec (some were integer)
  • tiktok-shop/product-details: product returned without a price is 200 with price null and priceStatus, 2 credits; no product data (404 or 502) is 0 credits
Breaking

Reddit: 404 for missing, banned, private and quarantined subreddits; no repeated search posts; analytics likes

Status codes and fields change on the Reddit endpoints. A subreddit that does not exist is now 404 NOT_FOUND on subreddit posts and subreddit search (it was a 503, or a 200 with no posts). A banned, private or quarantined subreddit is 404 with error.code SUBREDDIT_BANNED, SUBREDDIT_PRIVATE or SUBREDDIT_QUARANTINED and retryable false on subreddit details, subreddit posts and subreddit search (it was a 503, or a 200 with no posts). All of these cost 0 credits. Post transcript answers a post page it cannot read with 502 UPSTREAM_UNAVAILABLE, retryable, at 0 credits (it was a 422 VALIDATION_ERROR saying there was no transcript). An empty cursor on subreddit posts is the first page (it was a 400 INVALID_CURSOR). Search and subreddit search no longer repeat a post on a later page; nextCursor carries a new format, and cursors issued earlier still work. On /v1/analytics/post and /v1/analytics/compare, likes for a Reddit link is now the post score (upvotes - downvotes); it was always null. Reddit links there are read natively only. Prices are unchanged.

  • reddit/subreddit-posts, subreddit-search: missing subreddit 503 / empty 200 -> 404 NOT_FOUND, 0 credits
  • reddit/subreddit-details, subreddit-posts, subreddit-search: banned / private / quarantined 503 or empty 200 -> 404 SUBREDDIT_BANNED / SUBREDDIT_PRIVATE / SUBREDDIT_QUARANTINED, retryable false, 0 credits
  • reddit/post-transcript: unreadable post page 422 VALIDATION_ERROR -> 502 UPSTREAM_UNAVAILABLE, retryable, 0 credits
  • reddit/subreddit-posts: cursor="" 400 INVALID_CURSOR -> first page
  • reddit/search, subreddit-search: no post repeats across pages; new nextCursor format, earlier cursors still accepted
  • analytics/post, analytics/compare (Reddit links): likes null -> post score (upvotes - downvotes)
Breaking

Threads: every endpoint reads only Threads' public pages

Profile, user posts, search, search users, post details and Threads links in /v1/analytics/post now read only what a logged-out visitor sees on threads.net. The extended fallback is gone, so source is native or cache. Breaking: some status codes change. An unknown handle or post is 404; /threads/profile used to answer 200 with an almost empty card for a handle that is not on Threads. A page that can't be read within 60 seconds is 502 UPSTREAM_UNAVAILABLE, retryable, at 0 credits. Prices are unchanged.

  • threads/profile: unknown handle 200 (1 credit) -> 404 THREADS_PROFILE_NOT_FOUND (0 credits)
  • threads/profile: unreadable page -> 502 UPSTREAM_UNAVAILABLE, retryable, 0 credits
  • threads/user-posts: unknown handle 404 THREADS_POSTS_NOT_FOUND (unchanged), now without a slow second fetch
  • threads/user-posts: unreadable page 200 via the extended path -> 502 UPSTREAM_UNAVAILABLE, 0 credits
  • threads/search: unreadable page and no 6h last-good page -> 502 (unchanged), without the extended step
  • threads/search-users: unreadable page 200 via the extended path or 404 'No users found' -> 502 UPSTREAM_UNAVAILABLE, 0 credits
  • threads/post-details: unknown post 404 'Post not found' -> 404 THREADS_POST_NOT_FOUND
  • threads/post-details: unreadable page, or a short /t/ link that can't be confirmed -> 502 UPSTREAM_UNAVAILABLE, 0 credits
  • analytics/post (Threads links): unknown post 404, unreadable page 502, no extended path
  • every Threads endpoint: a request that can't finish within 60 s -> 502 UPSTREAM_UNAVAILABLE, 0 credits
  • source: native or cache on every Threads response (extended no longer appears); timings.path is native
  • post-details: author and url are the post's own, even when the URL names a different handle
  • post-details: no more media-only rows (text null, engagement 0) from the extended path
  • new field mediaUrlsExpireAt on posts, author cards, profiles and search-user cards (omitted when the link carries no expiry)
  • schema: engagement.likes/replies/reposts, text and displayName are nullable (null when Threads omits them)
Breaking

Facebook page-details, profile-photos, event-details: 404 only when Facebook says it's missing

These three endpoints answered 404 whenever we couldn't read the page, even when the page exists. Now a 404 NOT_FOUND (retryable: false) means Facebook itself says the page or event is missing or not public. When Facebook couldn't be read this time, the answer is 502 UPSTREAM_UNAVAILABLE with retryable: true and a Retry-After header; retry later. Both cost 0 credits. If you treat every 404 from these endpoints as "gone", that is now safe; add a retry for 502.

  • page-details: a failed read is 502 UPSTREAM_UNAVAILABLE, retryable, Retry-After 30 (was 404 "Page not found or not public")
  • profile-photos: a failed read, or a page whose photo grid didn't load, is 502, retryable, Retry-After 30 (was 404 "Photos not found")
  • profile-photos: a missing or non-public page is 404 NOT_FOUND "Page not found or not public" (was 404 "Photos not found")
  • event-details: a failed read is 502, retryable, Retry-After 30 (was 404 "Event not found")
  • event-details: a missing or non-public event is 404 NOT_FOUND "Event not found or not public"
  • 404s are retryable: false; all of these cost 0 credits; successful responses are unchanged
Breaking

X/Twitter: native only; status codes and fields changed on every Twitter endpoint

Effective 2026-10-23. Every /v1/twitter endpoint and X links in /v1/analytics/post and /v1/analytics/compare now read only X's public, logged-out surfaces; source is native. Status codes change: a tweet X hides from logged-out viewers is 404 TWEET_NOT_PUBLIC, a suspended account is 404 ACCOUNT_SUSPENDED (both retryable false), and a temporary read failure is a retryable 502 instead of a 404. A community with no posts is now 200 with an empty list instead of 404. Fields change: a retweet URL returns the retweet itself, hasMore is always false on the cursorless lists, and community cards always carry every key. All 4xx/5xx answers cost 0 credits. community-tweets is a flat 2 credits (it was up to 0.7 credits per tweet on the fallback path), and 0 credits when the community has no posts.

  • Status: tweet-details, transcript, analytics (X links): a tweet hidden from logged-out viewers is 404 TWEET_NOT_PUBLIC, retryable false (was 404 NOT_FOUND)
  • Status: tweet-details, transcript, analytics (X links): a temporary read failure is 502 UPSTREAM_UNAVAILABLE, retryable (was 404 NOT_FOUND); a tweet that does not exist stays 404 NOT_FOUND
  • Status: profile and user-tweets: a suspended account is 404 ACCOUNT_SUSPENDED, retryable false
  • Status: profile: 404 NOT_FOUND only when X says the account does not exist; a read failure is 502, retryable (was 404)
  • Status: user-tweets: an unknown handle is 404 only when X says the account does not exist; an unreadable profile is 502 (was 404)
  • Status: community: an unknown community is 404; a read failure is 502, retryable (the fallback path is gone)
  • Status: community-tweets: a community with no posts is 200 with tweets: [], truncatedReason no-tweets, 0 credits (was 404); an unknown community is 404; a read failure is 502
  • Field: tweet-details, transcript, analytics: the response is always the tweet whose id is in the URL; a retweet URL returns the retweet (isRetweet true, its author, text "RT @author: …") instead of the original post
  • Field: transcript publishedAt is ISO-8601
  • Field: user-tweets and community-tweets: hasMore is always false (no cursor is returned); truncatedReason gains the value limit (the window holds more; raise limit)
  • Field: community-tweets: listingHits equals the returned count; an X-Captapi-Warning header says when a limit above 20 stopped at X's public window
  • Field: community: all 12 keys are always present; memberCount, createdAt, creator, joinPolicy, isNsfw and rules are null when only the public page can be read
  • Field: tweets: possiblySensitive is present on every tweet (null when X leaves it unset); author.url is always the profile URL
  • Field: analytics/compare: failed[].reason and results[].error read "CODE: message" for structured errors
  • Price: community-tweets is a flat 2 credits (was up to 0.7 credits per tweet, minimum 2, on the fallback path) and 0 credits with no posts; no price increases
Breaking

Facebook: clearer status codes, free misses, and page-details fixes

Several Facebook endpoints now answer with status codes that match what happened. A page-details handle that Facebook does not show (non-existent or not public) is 404 at 0 credits; it used to be a billed 200 that echoed the handle as the page name. A missing required parameter (commentId on comment-replies, searchLocation on marketplace-search) is 422 VALIDATION_ERROR instead of 400, and input that is not a Facebook URL is 400 instead of a retryable 502. When comments, event-search, profile-events or profile-reels cannot read Facebook this time, the call is a retryable 503/502 at 0 credits. If you branch on 400 for missing parameters, also handle 422.

  • page-details: non-existent or non-public handle is 404 at 0 credits (was a billed 200)
  • page-details: followers and talkingAbout are filled again; verified is the badge Facebook shows next to the page name
  • comment-replies: missing commentId is 422 VALIDATION_ERROR (was 400)
  • marketplace-search: missing searchLocation is 422 VALIDATION_ERROR (was 400); a cursor this endpoint did not issue is 400 INVALID_CURSOR (was an empty 200)
  • comments and comment-replies: input that is not a Facebook URL is 400 (was 502)
  • profile-posts, profile-reels, profile-events: a missing page is a fast 404
  • comments, event-search, profile-events, profile-reels: a read failure is a retryable 503/502 at 0 credits
  • comments: Reel URLs return the comments Facebook shows for that video
  • marketplace-search details=true: returns the search page instead of 504 when the deadline is near; the extra 2 credits apply only to listings that were enriched (truncatedReason=enrich_deadline names the rest)
  • profile-photos: hasMore is always false (the grid has no cursor); truncatedReason=limit when more photos were available than limit
  • mediaUrlsExpireAt on rows with signed media URLs (details, reels, posts, photos, page-details, comments, replies)
Breaking

TikTok: native-only endpoints, 503 instead of 404 on a blocked hashtag fetch, channel-posts price cut to a flat 2 credits

Effective 2026-10-19. Channel posts, followers, followings, comments, comment replies, live, live info, profile region, audience demographics, top search, hashtag search, user search, search suggestions, popular creators, popular hashtags and TikTok links in /v1/analytics/post no longer have an extended fallback, so source is native. Breaking: /v1/tiktok/search/hashtag answers 503 UPSTREAM_UNAVAILABLE (retryable, 0 credits) when TikTok blocks the fetch; it used to answer 404 for a tag that exists. A tag TikTok does not have is still 404. Breaking: degraded responses are gone — channel-posts no longer serves stale snapshots (degraded, degradedReason, staleAgeMs, staleTtlMs, staleFinishedAt are no longer returned) and comments no longer returns a degraded first page. When TikTok blocks a fetch the call is a retryable 503 UPSTREAM_UNAVAILABLE at 0 credits (channel-posts, top-search and popular-hashtags keep their 502 UPSTREAM_UNAVAILABLE envelope); a profile TikTok does not have is 404. Price decrease: channel-posts is a flat 2 credits on every successful call (it was up to 10 credits when the extended path served a large page). Deprecated: the fallback parameter on /v1/tiktok/search/users has no effect; requests that send it get an X-Captapi-Warning header, and it will be removed on 2026-12-17 together with timings. Hashtag search pages past the first now come from TikTok's own feed. song-details is unchanged.

  • search/hashtag: blocked fetch is 503 (was 404); unknown tag still 404
  • channel-posts: no stale snapshots; degraded / stale* fields removed; 502 on a blocked fetch
  • channel-posts: price decrease to a flat 2 credits (was up to 10)
  • comments: no degraded first page; 503 on a blocked fetch
  • comment-replies, followers, followings, live, profile-region, search-suggestions, popular-creators: 503 on a blocked fetch, 0 credits
  • search/users: fallback deprecated (no effect, X-Captapi-Warning), removed 2026-12-17
  • audience-demographics: video sample from native posts only
Breaking

YouTube search: sortBy=rating returns 400

YouTube no longer orders search results by rating: sortBy=rating came back with the relevance results in a slightly different order, not sorted by likes. It now answers 400 SORT_NOT_SUPPORTED at 0 credits, the same as sortBy=date. Use relevance or views.

  • sortBy=rating: 400 SORT_NOT_SUPPORTED (0 credits)
  • relevance and views (alias popular) are unchanged
Breaking

Audio transcripts with no speech: 422 NO_SPEECH at 0 credits

When speech recognition finds no speech in a YouTube video's audio, /v1/youtube/audio-transcript now answers 422 NO_SPEECH and charges 0 credits. Before, it returned 200 with an empty transcript and billed the full duration. The result is remembered for 24 hours, so a repeat answers at once. /v1/instagram/transcript returns the same NO_SPEECH code for a Reel with no speech.

  • youtube/audio-transcript: no speech is 422 NO_SPEECH, 0 credits (was 200, billed)
  • instagram/transcript: no-speech error code is NO_SPEECH
Breaking

YouTube search: sortBy=date returns 400; type=shorts returns only Shorts

YouTube no longer orders search results by upload date, so sortBy=date used to come back in relevance order while claiming a date sort. It now answers 400 SORT_NOT_SUPPORTED at 0 credits; use uploadDate to limit results by age. type=shorts now uses YouTube's Shorts filter: results are only Shorts (before, short regular videos could be mixed in), and uploadDate and duration apply to Shorts too. With type=shorts only sortBy=relevance is supported; other sorts answer 400.

  • sortBy=date: 400 SORT_NOT_SUPPORTED (0 credits)
  • type=shorts: only Shorts; uploadDate and duration now apply
  • type=shorts with sortBy views, popular or rating: 400 SORT_NOT_SUPPORTED
Breaking

Response headers: X-Captapi-Source removed; source stays in the body

The X-Captapi-Source response header duplicated the source field in the response data and said unknown on routes that have no source. It is no longer sent. Read source from data instead: native, extended, cache or asr. X-Captapi-Credits, X-Captapi-Cache and X-Captapi-Request-Id are unchanged, and browsers can now read X-Captapi-Request-Id on cross-origin calls.

  • Removed: X-Captapi-Source response header (and from openapi.json)
  • Docs: /docs now documents the response headers and the source field values
  • X-Captapi-Request-Id is exposed to browser clients (CORS)
Breaking

timings leaves response bodies on 2026-12-17; use Server-Timing

timings will be removed from response bodies on 2026-12-17; use the Server-Timing header. Every response now carries Server-Timing: total;dur=<ms>, the total time the request took. Until 2026-12-17 the timings object stays in data, marked deprecated in the docs and openapi.json. source stays in the body as a documented field.

  • New: Server-Timing response header (total;dur=<ms>), readable by browser clients (CORS)
  • Deprecated: data.timings, removed from response bodies on 2026-12-17
  • Unchanged: data.source (native, extended, cache or asr)

Try what's new

Start with 100 free credits — no credit card required.