Computed fields
How engagement_rate, language, content_category and estimated_reach are calculated, and when each one is null.
Profile, post and comment responses include a computed block next to the platform data, wherever the endpoint supports it. Its shape is identical on every platform, so an Instagram creator's engagement rate can be compared directly with a TikTok creator's.
| Field | Type | Range | null when |
|---|---|---|---|
engagement_rate | number | null | 0.0 to 1.0 | The divisor is missing or zero. On posts, also when likes, comments and shares are all null. On profiles, also when the raw ratio is above 1.0. |
language | string | null | ISO 639-1 (en, ko, ja, ...) | Latin-script text that is too short, too few words, or reads like a headline. Non-Latin scripts have no length floor. Also when the language is not recognized. |
content_category | string | null | 14 categories or "other" | The text is shorter than 10 characters after trimming. This gate is separate from, and looser than, the one on language. |
estimated_reach | number | null | integer, 0 or more | Posts: views is missing or 0, which includes every post without a view count, such as Instagram photos. Profiles: always null. |
Real numbers or honest nulls
A computed field is either a real value or null. It is never 0 standing in for "unknown", because that difference matters as soon as you sort or filter.
When a value had to be pushed back into range, most often an engagement_rate above 1.0, a note explaining it is added to data._warnings.
Where the block appears
- Author:
data.computedon profile responses. - Post:
data.computedon single-post responses, and on each item of a post list. - Comment:
data.computedon single-comment responses, and on each item of a comment list. Comments get a smaller block, just{ language }, pluslabelson the comment lists that are judged (see Labels). The other three fields need signals a comment lacks: there is no view count to divide by, and categories and reach do not make sense for a one-line reply.
Transcript responses carry no computed block. Items on the post search lists also get computed.relevance next to the four fields on this page.
Labels sit alongside
The four fields here come from code working on the row's own numbers and text, so the same input always gives the same output. computed.labels and computed.relevance are different: each is a model's reply to a single yes-or-no style question about the row, such as whether a post is sponsored or whether a result matches your query, given as a probability or a level. They are on by default and add no credits. Labels covers which lists carry which labels, the shapes, and judgments=off.
engagement_rate
A comparable engagement signal between 0.0 and 1.0, rounded to 6 decimal places.
Profiles
engagement_rate = author.likes_count / author.followersThe result is null when followers is 0 or likes_count is missing. No data and zero engagement are kept apart.
Instagram. Instagram profiles never fill author.likes_count. On Instagram profile endpoints the rate is instead built from up to about 12 recent posts included in the same response:
engagement_rate = (mean(post_likes) + mean(post_comments)) / followersOnly posts with both likes_count and comments_count are used. With no usable posts the rate is null. No other platform uses this fallback.
Posts (single posts and list items)
engagement_rate = (likes + comments + shares) / viewsA missing or zero views gives null, on purpose. Older tweets and posts on surfaces with no view count get no rate rather than a made-up one. Dividing by 1 instead would surface the raw engagement total (say 26573) as if it were a rate.
The rate is also null when likes, comments and shares are all null. A real 0 in any of them is data, so a post with genuinely no engagement reports 0. Because both fields gate on views on their own, a post can have an estimated_reach and a null engagement rate at the same time.
The same formula on every platform. What varies is which inputs each platform provides:
- Missing
likes,commentsorsharescount as0. Instagram and YouTube do not report shares, so their numerator islikes + comments. Nothing is invented, and one missing term does not null the rate. This is the most common reason a hand calculation disagrees: if you add a shares figure on Instagram or YouTube, your number will differ from ours, where shares contributed0. - Missing
viewsstaysnull, and anulldivisor makes the whole ratenull.
Rows filled by include
Some list endpoints accept an include parameter that joins each row to its own detail record in the same call, for example /v1/facebook/profile/posts?include=engagement. When a join adds data to a row, that row's engagement_rate and estimated_reach are recalculated from the fuller numbers, so the row matches what a direct call for that item would return.
Only those two fields are recalculated. language and content_category read the caption, which a join does not change.
When your own calculation will not match
If you check engagement_rate by recomputing it from the raw fields, the table below covers every mismatch. In each case our value is correct and the gap comes from an input you do not have.
| Platform | Matches (likes+comments+shares)/views? | Why |
|---|---|---|
| TikTok | Yes, exactly | Views, likes, comments, shares and saves are all present. |
| Twitter/X | Yes, where the rate is not null | Many tweets have no view count (older tweets and a lot of replies), so their rate is null. That means no view count, not zero engagement. Filter to non-null rates before using the field as a feature. |
| No | Shares and saves are not public, so shares count as 0 and the rate is effectively (likes + comments) / views. Photos have no view count and get null; reels have views and get a rate. | |
| YouTube | No | Shares are not returned either, so the rate is (likes + comments) / views. |
| Partly | Facebook's like figure appears to count all reactions, not just likes, so the numerator can be higher than a likes-only calculation. Treat the Facebook rate as including all reactions. | |
| YouTube live streams | No (0 or null) | Live and upcoming list rows have a view count but no likes or comments, giving 0, or no view count before the broadcast, giving null. Fetch a finished stream through the single-video endpoint for full engagement. |
| Reddit, Threads (list items) | No (always null) | Neither shows a per-post view count on lists or feeds. On Reddit, use score as the engagement signal. A single Threads post fetched by URL does have a view count and gets a rate. |
A very small share of post rows compute above 1.0; those are capped at 1.0 with a warning.
Out of range: posts cap, profiles null
Posts cap at 1.0. A post ratio above 1.0 (an old tweet with more likes than recorded views, or reshares outnumbering views) still says something, so it is set to 1.0 and a warning is added:
{
"data": {
"computed": { "engagement_rate": 1.0 },
"_warnings": [
"computed.engagement_rate: value exceeded 1.0 (raw: 1.42); clamped"
]
}
}Profiles return null. A profile ratio above 1.0 only happens when lifetime likes (TikTok, YouTube and Facebook profiles) are divided by today's follower count. That is not an engagement rate, so the field is null with a warning rather than a fake 1.0:
{
"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"
]
}
}There is also a lower bound at 0, but positive inputs cannot produce a negative result, so it almost never applies.
language
A two-letter ISO 639-1 code. Regional variants are not used: you get pt, never pt-BR.
| Object | Text read |
|---|---|
| Author | author.bio |
| Post | post.content.text |
| Comment | comment.text, with a detector tuned for comments (see below) |
Missing or non-string text gives null. After that, the rules depend on the script:
- Non-Latin scripts (Korean, Japanese, Chinese, Arabic, Devanagari, Thai) are recognized by their Unicode ranges at any length. A three-character Korean bio returns
ko. - Latin scripts use statistical classification, which needs real sentences. URLs,
@mentions,#hashtagsand list markers are removed first; what is left must be at least 30 characters and 3 words. Short, mostly title-cased text, such as a name or a headline, returnsnull.
Those limits exist because below them the classifier is wrong too often. No answer is better than a confident wrong one.
How detection works
- Script check first. If the text contains characters from these ranges, the code is returned straight away, whatever the length:
- Hangul gives
ko - Hiragana or Katakana gives
ja - CJK ideographs give
zh - Arabic gives
ar - Devanagari gives
hi - Thai gives
th
- Hangul gives
- Statistical classification for everything else, on the cleaned text that passed the limits above. If the classifier is not confident, the result is
null.
Codes you can receive
Only these codes are returned. Anything else becomes null.
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 zhThat is 31 codes.
Comments
Comments are much shorter and messier than bios or captions, and the standard classifier would leave about half of them null. Comments therefore go through their own steps, in this order:
- Clean the text. URLs,
@mentions,#hashtagsand emoji are removed. A comment that was only emoji, a date or a handle returnsnull. - Script check. The same ranges as above, plus Hebrew (
he) and Greek (el), at any length.좋아요returnsko. - Noise check. At least 2 words and 4 letters must remain. Anything shorter is a name or an exclamation and returns
null. - The platform's own tag. TikTok tags each comment with a language, and Threads sometimes does. If that tag is one of the 31 codes, it is used.
- Short-text classification. Everything else goes to a detector built for short strings, and only clear, reliable answers are kept. That is why
Great video!getsenas a comment but would be under the 30-character limit in a bio.
The same 31 codes apply. Expect about a quarter of real comments to come back null: emoji reactions, dates and one-word replies carry no language signal.
content_category
One of 14 fixed categories, or "other". It is null when the text is missing or under 10 characters after trimming. That gate differs from the one on language, so a bio can have a category and no language, or the other way round.
| Category | Example 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 | The text was long enough to classify, but no category matched two different keywords. |
Matching rules
Two keywords to win
A category needs at least two different keywords to be chosen. One stray word ("team", "protein", "producer") gives "other" instead of a wrong label. This is the usual reason an obviously on-topic bio comes back as "other".
- Short keywords of 3 characters or fewer, such as
"ai","tv"and"dj", must match a whole word. They match"building with ai"but not"hair"or"said". - Those short words mean other things in other languages (
"ai"in Portuguese and French), so they only count whencomputed.languageisenornull.content_categoryis therefore calculated afterlanguageand can change with it. - Longer or multi-word keywords match on word boundaries.
"machine learning"matches in"I love machine learning!"without spilling into neighboring words. - If several categories reach two keywords, the highest score wins. Ties go to the category listed first in this order:
tech, food, gaming, fashion, beauty, fitness, travel, music, education, entertainment, sports, business, news, lifestyle.
Treat content_category as a quick first pass, not a taxonomy: it is keyword-based, fast, repeatable and noisy. On post lists, the niche label places a caption in a published niche taxonomy and returns null when the caption is too thin; see Labels. For anything more, run your own model over the bio and text fields.
estimated_reach
A rough upper estimate of how many distinct accounts a post reaches. Always a whole number of 0 or more, or null.
Profiles: always null
On profiles, estimated_reach is always null. A follower count and an engagement rate are not enough to estimate reach, and a profile has no view signal the way a post does. An older formula (followers * engagement_rate * 0.1) could return less than a single post's likes on big accounts, so it was removed. The field stays in the block so the shape does not change.
Posts
estimated_reach = round(views * 1.2)Views already track reach closely, so a small multiplier gives an estimate of unique accounts that allows for some repeat viewing.
The field is null whenever views is missing or 0. Views are the only condition, so whether you get a value depends on whether the platform shows a view count for that kind of media:
- Instagram: reels and videos have a play count and get an estimate. Photo posts and carousels with only photos have no view count, so both
estimated_reachandengagement_rateare alwaysnull, while likes and comments still fill in. A carousel with a video in it does get an estimate. - Twitter/X: tweets without a view count (older tweets, many replies) return
null. - Reddit and Threads list items: no per-post views, so always
null.
Limits
This is an estimate, not a measurement. It works for:
- Ranking posts or creators by rough reach when the platform does not publish reach.
- Sizing the order of magnitude of a creator's impact for outreach.
It does not work for:
- Forecasting paid-media returns.
- Comparing reach between platforms that count views very differently, such as TikTok's looping views against YouTube's longer view threshold.
Example response
The values below are illustrative.
{
"success": true,
"platform": "tiktok",
"endpoint": "/v1/tiktok/profile",
"data": {
"author": {
"username": "khaby.lame",
"followers": 160000000,
"likes_count": 2500000000,
"bio": "If u wanna laugh u r in the right place"
},
"computed": {
"engagement_rate": null,
"language": "en",
"content_category": "other",
"estimated_reach": null
},
"_warnings": [
"computed.engagement_rate: author ratio exceeded 1.0 (raw: 15.625); returned null. A lifetime likes/followers ratio is not a real engagement rate"
]
},
"credits_used": 20,
"credits_remaining": 9480,
"request_id": "req_1a2b3c4d5e6f",
"cached": false,
"idempotent_replay": false,
"charge_reason": "miss",
"free_call": false
}What this shows:
- TikTok reports lifetime likes against current followers, so the ratio is far above
1.0. The profile rule returnsnullwith a warning instead of a fake1.0. estimated_reachisnullbecause this is a profile.content_categoryis"other"because the bio does not hit any category's keywords twice.
When to be careful
- The raw value was out of range. Look for
clamped(posts) orreturned null(profiles) in_warnings, then decide whether to use our value or your own calculation. - The post has no
views. Common on Instagram photos, older tweets, some Reddit endpoints and some Facebook surfaces. On Instagram this depends on the media type: a reel gets a rate and a reach estimate, a photo from the same account an hour later getsnullfor both. languageisnullon a Latin-script bio. After removing links, handles and hashtags, the text was probably under 30 characters or 3 words, or read like a title. Checkauthor.bioyourself. This never happens to Korean, Japanese, Chinese, Arabic, Devanagari or Thai text.