Computed fields
Every profile, post, and comment response carries a computed block. Engagement_rate, language, content_category, estimated_reach. Here is exactly how each is calculated.
Profile, post, and comment responses carry a computed block alongside the upstream data. It has the same shape on every platform, so a TikTok creator's engagement rate is directly comparable to an Instagram one.
| Field | Type | Range | When it is null |
|---|---|---|---|
engagement_rate | number | null | 0.0, 1.0 | Divisor missing or zero; on posts, also when all of likes/comments/shares are null; on profiles, when the raw ratio exceeds 1.0 |
language | string | null | ISO 639-1 (en, ko, ja, ...) | Latin scripts: cleaned prose under 30 chars, under 3 words, or a title-cased headline. Non-Latin scripts: never length-gated. Also when unrecognised |
content_category | string | null | 14 categories or "other" | Input text under 10 trimmed chars (a looser, separate gate from language's) |
estimated_reach | number | null | integer ≥ 0 | Posts: views missing or 0 (so any post without a view count, e.g. Instagram photos). Profiles: always null, see below |
Every value is a real number or an honest null. A 0 is never substituted
for "we do not know", because the difference matters when you sort or filter.
If a value was forced into range, typically engagement_rate exceeding 1.0, an explanatory string lands in data._warnings so you can see it happened.
Where the block attaches
Author:data.computedon profile responses.Post:data.computedon single-post responses and on every item of aPostList.Comment:data.computedon single-comment responses and on every item of aCommentList. Comments carry a slimmer block, just{ language }. The other three fields need signals a comment does not have:engagement_rateneeds a views divisor, andcontent_categoryandestimated_reachdo not generalize to one-line replies. Always-null columns would be shape noise.
The Audience, Transcript, and SearchResult archetypes carry no computed block.
engagement_rate
A normalised, comparable engagement signal in the range [0.0, 1.0], rounded to 6 decimals.
Author variant (profiles)
engagement_rate = author.likes_count / author.followersReturns null when followers is 0 or likes_count is absent. "Zero engagement" and "we do not have the data" are different states, and the second one is reported honestly.
Instagram fallback. Instagram's profile payload never populates author.likes_count. On an Instagram profile endpoint, up to ~12 recent posts embedded in the same response are read instead:
engagement_rate = (mean(post_likes) + mean(post_comments)) / followersA post counts only if both likes_count and comments_count are present. Zero usable posts returns null. This fallback is Instagram-only today.
Post variant (single posts and PostList items)
engagement_rate = (likes + comments + shares) / viewsReturns null when views is missing or 0. This is intentional. Pre-views-era tweets and platforms that do not report views must be honest. The previous implementation fell back to divisor = 1, which silently surfaced the numerator (for example 26,573) as an engagement rate. That is worse than null.
It also returns null when the numerator is structurally unavailable, meaning all three of likes, comments, and shares are null (Twitch VODs, Spotify tracks, Rumble list items). An explicit upstream 0 in any one of the three is real data, so a genuine zero-engagement post still reports 0. A post can therefore carry a populated estimated_reach (views present) alongside a null engagement rate (no engagement signals). The two fields gate independently on views.
One formula, everywhere. This is the same calculation on every platform. There is no per-platform variant of the post-level formula. What changes between platforms is which inputs the platform exposes:
likes,comments,sharesare treated as0when absent. If a platform does not report a shares count (Instagram and YouTube do not), the numerator becomeslikes + comments. A shares number is never invented, and the whole rate is not nulled because one addend is missing. This is the most common reason a hand-recomputed rate does not match: if you reproduce the formula assuming a non-nullshareson Instagram or YouTube, your result differs, because on our sidesharescontributed0.viewsis treated asnullwhen absent, and anulldivisor makes the whole ratenull. Unlike the addends, a missingviewsis not coerced to0, because dividing by zero is meaningless, so the rate is honestlynull.
Why your recomputation might not match
If you validate engagement_rate by recomputing it from the raw fields, this table explains every case of disagreement. In each case the returned value is correct, and the mismatch comes from an input you may not have.
| Platform | Reproducible from (likes+comments+shares)/views? | Why |
|---|---|---|
| TikTok | Yes, exactly | All five engagement fields (views, likes, comments, shares, saves) are present. Your recomputation will match. |
| Twitter/X | Yes, on rows where engagement_rate is not null | Where a rate is returned, it reproduces exactly. A large share of tweets return engagement_rate: null because Twitter did not report a native views count for that tweet (older tweets and many replies). null here means "no view count available", not "zero engagement". Filter on engagement_rate IS NOT NULL before using it as a feature. |
| No | shares (and saves) are not exposed on the public upstream, so shares = 0 and the rate is effectively (likes + comments) / views. Photo posts also have no view count, so their rate is null; Reels have views and do get a rate. A shares-inclusive reproduction cannot match either branch. Use the returned value directly. | |
| YouTube | No | Same mechanism as Instagram: shares is not returned, so the rate is (likes + comments) / views. |
| Partially | The upstream like_count is mapped verbatim into likes. That field appears to be the aggregate reaction count (like, love, haha, and the rest), not likes alone, so the numerator can run higher than a like-only reproduction. This is being confirmed upstream; treat the Facebook rate as reaction-inclusive for now. | |
| YouTube livestreams | No (rate is 0 or null) | The live/upcoming list shape carries a view count but no like or comment counts, so the numerator is 0 (rate 0) or the view count is absent pre-broadcast (rate null). Hydrate a finished stream through the single-video endpoint to get full engagement. |
| Reddit, Threads (list items) | No (rate is null) | Neither exposes a per-post views count on list or feed items, so no rate is possible. For Reddit, use score (upvotes) as the engagement proxy. A single Threads post fetched by URL does carry a view count and will get a rate. |
The small share of post rows where the raw math exceeds 1.0 are clamped to 1.0 with a _warnings note. That is a fraction of a percent of rows and does not affect the population.
Out-of-range values: posts clamp, profiles go null
The two archetypes handle a raw value above 1.0 differently, because the causes differ.
Post rows clamp to 1.0. A post ratio above 1.0 (an old tweet whose likes outweigh its under-reported views, or heavy reshare activity exceeding views) is a real if unbounded signal, so it is pinned to 1.0 and a warning is appended:
{
"data": {
"computed": { "engagement_rate": 1.0 },
"_warnings": [
"computed.engagement_rate: value exceeded 1.0 (raw: 1.42); clamped"
]
}
}Profile (Author) rows return null. An author ratio above 1.0 only ever arises from dividing a cumulative lifetime like count (TikTok, YouTube, Facebook profiles) by the current follower count. That is not a real engagement rate, so instead of fabricating 1.0 the response returns null with an explanatory warning:
{
"data": {
"computed": { "engagement_rate": null },
"_warnings": [
"computed.engagement_rate: author ratio exceeded 1.0 (raw: 86.32); returned null. A lifetime likes/followers ratio is not a real engagement rate"
]
}
}The defensive lower clamp at 0 exists for the same reason but rarely fires, because the arithmetic cannot produce negatives from positive inputs.
language
An ISO 639-1 two-letter code. Regional forms are not used: pt-BR is never emitted, only pt.
| Archetype | Input |
|---|---|
Author | author.bio |
Post | post.content.text |
Comment | comment.text, via a comment-tuned detector described below |
Returns null when the input is missing or non-string. Beyond that, the floors depend on the script:
- Non-Latin scripts (Korean, Japanese, Chinese, Arabic, Devanagari, Thai) are detected by Unicode range and classify at any length. A three-character Korean bio returns
ko. - Latin scripts go through trigram classification, which needs real prose to be trustworthy. URLs,
@mentions,#hashtags, and list markers are stripped first, then the remaining prose must be at least 30 characters and at least 3 alphabetic words. A short, mostly title-cased string (a headline or a name, not prose) also returnsnull.
Those floors are confidence gates. Below them the classifier misfires more often than it helps, so nothing is returned rather than a confident wrong code.
Detection strategy
Two passes:
-
Unicode fast-path for non-Latin scripts, applied first and at any input length. If the input contains characters in these ranges, the language code is returned directly:
- Korean (
가to, Hangul Jamo) givesko - Japanese (Hiragana / Katakana) gives
ja - CJK Unified Ideographs gives
zh - Arabic gives
ar - Devanagari gives
hi - Thai gives
th
- Korean (
-
Trigram classification via
franc-minfor everything else, run on the cleaned prose that cleared the floors above. franc returnsundwhen below its internal confidence threshold, and that maps tonullrather than a guess.
Supported codes
If franc's ISO 639-3 code maps to one of the codes below, you get the two-letter form. Anything else collapses to null, because an obscure three-letter code leaking through the public surface is worse than nothing.
ar bg ca cs da de el en es fa fi fr he hi hu
id it ja ko nl no pl pt ro ru sv th tr uk vi zh31 codes total.
On comments
Comments are far shorter and noisier than captions or bios. The trigram classifier misfires badly below its floors, which would leave roughly half of all real comments at null. Comment responses therefore use a separate detection stack, in order:
Prose strip. URLs, @mentions, #hashtags, and emoji are removed first. A comment that is only emoji, only a date stamp, or only a handle returns null.
Unicode fast-path. The same non-Latin script ranges as above, plus Hebrew (he) and Greek (el), at any length. 좋아요 gives ko.
Noise gate. What is left must be at least 2 words and 4 letters of prose. Below that a comment is a name or an interjection, and no classifier is trustworthy there, so the result is null rather than a guess.
The platform's own label. TikTok attaches a per-comment language label upstream, and Threads sometimes does. When it is one of the 31 published codes it is returned directly.
Short-text classification. Everything else runs through ELD, a detector built for short text, and only a reliable, clear-margin answer is returned. This is why Great video! gets en on a comment while the same string in a bio falls under the 30-character floor.
The published code set is identical to the table above, and anything outside it collapses to null. Expect roughly a quarter of real-world comments to return null. Emoji-only reactions, date stamps, and one-word replies carry no language signal, and an honest null beats a confident wrong label when you are filtering a corpus.
content_category
One of 14 hand-curated categories or "other". Returns null if the input text is missing or under 10 trimmed characters. That is a different, looser gate than language's, so a bio can carry a category and no language, or the reverse.
| Category | Sample matched keywords |
|---|---|
tech | programming, developer, software, ai, saas, blockchain, frontend |
food | cooking, recipe, chef, restaurant, baking, vegan |
gaming | gaming, esports, twitch, fortnite, valorant, fps |
fashion | fashion, outfit, designer, ootd, streetwear |
beauty | makeup, skincare, lipstick, serum, moisturizer |
fitness | workout, gym, cardio, yoga, marathon, protein |
travel | adventure, destination, vacation, backpacking, wanderlust |
music | song, artist, album, concert, producer, spotify |
education | learning, course, tutorial, university, lecture |
entertainment | movie, tv, netflix, celebrity, comedy, series |
sports | football, basketball, nba, olympics, championship |
business | entrepreneur, ceo, marketing, finance, fundraising |
news | politics, economy, election, journalist, parliament |
lifestyle | wellness, mindfulness, productivity, minimalism, diy |
other | fewer than two distinct in-category keywords matched (the input was long enough to evaluate, but nothing corroborated a single category) |
Matching rules
A category must match at least two distinct keywords to win at all. A single
incidental word ("team", "protein", "producer") resolves to "other" rather
than a confident wrong label. This is the most common reason a bio you would
call obviously on-topic comes back as "other".
- Short keywords (3 characters or fewer, single word) such as
"ai","tv", and"dj"require an exact token match. They match"building with ai"but not"hair"or"said". - Those same three tokens are ambiguous across languages (
"ai"is colloquial Portuguese and French), so they count only whencomputed.languageisenornull.content_categoryis therefore computed afterlanguageand can depend on it: the same bio scores differently once the text is known not to be English. - Longer or multi-word keywords use a Unicode word-boundary regex.
"machine learning"matches inside"I love machine learning!"but does not bleed into adjacent words. - Among categories that clear the two-keyword bar, the highest-scoring one wins. Ties resolve to whichever category is declared first in the source order (
tech, food, gaming, fashion, beauty, fitness, travel, music, education, entertainment, sports, business, news, lifestyle), not alphabetically.
If you build product on top of content_category, treat it as a rough first-pass classifier, not a taxonomy. It is keyword-based: fast, deterministic, and noisy. For nuanced classification, layer your own model on top of the bio and text fields.
estimated_reach
A rough upper-bound estimate of how many distinct accounts a post is reaching. Always an integer ≥ 0, or null.
Author variant (profiles): always null
On the Author archetype, estimated_reach is always null. Reach is not computable from a follower count plus a followers-normalised engagement rate. A profile payload carries no impressions or views signal the way the Post archetype has views. An earlier formula (followers * engagement_rate * 0.1) produced numbers that could fall below the like count of a single post on large accounts, which is a fabrication rather than an estimate, so it was removed. The field is kept in the block for shape stability.
Post variant
estimated_reach = round(views * 1.2)For posts, views is already a stronger reach signal than impressions, so a modest multiplier estimates unique-account reach, assuming a small fraction of repeat views.
Returns null whenever views is missing or 0. views is the only gate, so whether reach populates on a given post is entirely a question of whether the platform exposes a view count for that media type:
- Instagram: reels and videos carry a play count, so they get a reach estimate. Photo posts and photo-only carousels have no view count on Instagram, so
estimated_reachandengagement_rateare alwaysnullon them, whilelikesandcommentsstill populate normally. A carousel that contains a video child does carry a play count and does get a reach estimate. - Twitter/X: tweets without a native view count (older tweets, many replies) return
null. - Reddit, Threads list items: no per-post views, so always
nullthere.
Caveats
This is a heuristic, not a measurement. It is useful for:
- Sorting posts or creators by approximate reach when the platform does not expose reach directly.
- Estimating order-of-magnitude impact for influencer outreach.
It is not useful for:
- Forecasting paid-media ROI.
- Comparing reach across platforms with very different view-counting rules, TikTok's auto-loop views against YouTube's 30-second threshold for example.
Example response
{
"success": true,
"platform": "tiktok",
"endpoint": "/v1/tiktok/profile",
"data": {
"author": {
"username": "mrbeast",
"followers": 95000000,
"likes_count": 8200000000,
"bio": "I want to make the world a better place before I die."
},
"computed": {
"engagement_rate": null,
"language": "en",
"content_category": "other",
"estimated_reach": null
},
"_warnings": [
"computed.engagement_rate: author ratio exceeded 1.0 (raw: 86.315789); returned null. A lifetime likes/followers ratio is not a real engagement rate"
]
},
"credits_used": 1,
"credits_remaining": 8431
}Three things to read off that response. The likes_count / followers ratio blows past 1.0 because TikTok reports cumulative lifetime hearts against current followers, so the author path reports null with a warning rather than fabricating 1.0. estimated_reach is null because this is the Author archetype. And content_category is "other" because that bio, despite reading as broadly lifestyle-ish, matches no category keyword twice.
