Satellite - Hashtag Lookups (TikTok, Instagram & YouTube)
Hashtag lookups give you the same deep-dive treatment Satellite already gives creators and sounds — but applied to every video posted under a hashtag. You get the hashtag's page metadata, the full normalized video list, aggregated stats (views, engagement, velocity, top creators, related hashtags, top sounds, duration distribution), and — when trend_analysis=true — an LLM-derived trends block with mechanically-computed time_windows, resurged, and momentum per trend.
Hashtag lookups support TikTok (native challenge feed), Instagram (Google-indexed public reels — best-effort coverage), and YouTube (native hashtag page, Shorts only). Pass the hashtag with or without the leading # (URL-encode it as %23 if you include it) — it's normalized to lowercase either way. Invalid input (multiple tags, spaces, over 100 chars) returns a 400 and is not charged. Base URL: https://api.virlo.ai/v1/satellite. All parameter names and response fields use snake_case. All responses are wrapped in a { "data": { ... } } envelope.
Per-platform field caveats. Not every platform exposes every field on its hashtag feed. YouTube Shorts are enriched per-video through Virlo's video-details pipeline — exact views, likes, comments, publish dates, durations, channel subscriber counts, and sound attribution — but shares/collects remain 0 (YouTube exposes no public counts) and author is the channel. The rare Short that can't be enriched (deleted or region-locked) degrades to views + title only. On Instagram, shares/collects are 0 and is_duet/is_stitch are false. See Per-platform coverage for the full matrix — and never compare engagement_rate across platforms.
Start Hashtag Lookup
Queue a hashtag lookup job. Returns immediately with a job_id. Poll the status route for results.
Pricing summary:
- $0.50 base (50 credits) — covers fetch + normalization + stats at
depth=standard(up tomax_videosvideos). - + $0.50 surcharge (50 credits) when
trend_analysis=true. That flag also forces a deeper fetch (~300 videos) and ignoresmax_videos. The surcharge uses the samesatellite_trend_analysispricing key as creator and sound lookups. - Depth tiers:
depth=deep(~300 videos) adds $0.50 (50 credits);depth=full(~500 videos) adds $1.50 (150 credits). The deep surcharge is waived whentrend_analysis=true— trends already fetch ~300 videos — so totals come out to 50 credits standard, 100 for deep or trends (deep + trends stays 100), 200 for full, 250 for full + trends. - All polling and re-reads via
/v1/satellite/runs/:run_idare free. - Repeat lookups are deduplicated: re-requesting the same lookup within 6 hours returns the existing completed run instantly with
cached: trueand no charge. The cache only applies when the stored run covers what you asked for — samesort, trend analysis already present if you requesttrend_analysis=true, and a storeddepthequal to or deeper than the one you request (a deeper cached run satisfies a shallower request for free). Changing those parameters runs (and bills) a fresh lookup.
Path parameters
- Name
platform- Type
- string
- Required
- *
- Description
tiktok,instagram, oryoutube. TikTok reads the native challenge feed; Instagram reads Google-indexed public reels (best-effort coverage); YouTube reads the native hashtag page (Shorts only). See Per-platform coverage.
- Name
hashtag- Type
- string
- Required
- *
- Description
The hashtag to look up, with or without the leading
#(URL-encode#as%23). Normalized to lowercase. Must be a single tag — no spaces — and at most 100 characters. Invalid input returns400and is never charged.
Query parameters
- Name
trend_analysis- Type
- boolean
- Description
When
true, runs LLM-based trend detection over a deep-fetched corpus of ~300 videos and returns thetrendsblock. Adds the $0.50 surcharge. Whentrue,max_videosis ignored. Defaultfalse.
- Name
max_videos- Type
- integer
- Description
Number of videos to fetch when
trend_analysisisfalse(1–100). Default50. Ignored whentrend_analysis=true. Also ignored whendepthisdeeporfull.
- Name
sort- Type
- string
- Description
Order of the returned
videos[]:top(default, views descending) orrecent(publish date descending). The platforms expose no server-side sort on hashtag feeds, so ordering is applied to the collected corpus —statsare order-independent either way.
- Name
depth- Type
- string
- Description
How deep to collect:
standard(default — up tomax_videosvideos),deep(~300 videos, +$0.50 / 50 credits), orfull(~500 videos, +$1.50 / 150 credits). Same tier shape as tracking's post-collection.deepandfulloverridemax_videos— it's ignored on those tiers. The deep surcharge is waived whentrend_analysis=true(trends already include a ~300-video fetch), so deep + trends costs the same $1.00 as trends alone; full + trends is $2.50. Instagram supportsdepth=standardonly — its Google-indexed feed caps at ~11 pages, sodeep/fullon Instagram return400before billing (never charged). TikTok and YouTube support all three tiers.
Request
curl -G https://api.virlo.ai/v1/satellite/hashtags/tiktok/cleantok \
-H "Authorization: Bearer {token}" \
-d trend_analysis=true
Response
{
"data": {
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing"
}
}
Poll Hashtag Status
Poll for results — polling is free. Returns processing while the job is running. When completed, returns the full result envelope described below.
Status results are cached for 24 hours for fast repeat polling. After that window, re-read for free via GET /v1/satellite/runs/:run_id — every completed hashtag lookup is persisted as a durable satellite run.
Result shape (when completed)
- Name
run_id- Type
- string
- Description
Durable run identifier. Save it — re-reads via
/v1/satellite/runs/:run_idare free forever, unlike the 24-hour status cache.
- Name
request- Type
- object
- Description
Echo of the request parameters (
hashtag,platform,trend_analysis,depth,max_videos,sort).max_videosechoes the effective target: your requested value atdepth=standard(50 default, up to 100),300atdeep,500atfull.
- Name
hashtag- Type
- object
- Description
{ name, platform, page_url }— the normalized tag and the platform hashtag page it was collected from.
- Name
data_captured_at- Type
- string
- Description
ISO 8601 timestamp when the scrape finished. Use this to know how stale the data is.
- Name
credits_charged- Type
- number
- Description
What the run cost when it was created (
50standard,100withdepth=deepor trend analysis,200fordepth=full,250for full + trends). Cache hits return the original run's stored envelope, so this field keeps its original value — the free-ness of a cached read is signaled bycached: trueon the start response andX-Credits-Used: 0on the response headers, not here.
- Name
stats- Type
- HashtagStats
- Description
Aggregate statistics across the sampled videos. See Stats block.
- Name
sample_quality- Type
- HashtagSampleQuality
- Description
{ truncated_by_cap, pages_fetched, note }.noteis one ofinsufficient_corpus,small_corpus_workable,healthy_corpus,deep_corpus.
- Name
trends- Type
- HashtagTrends
- Description
Identical shape to the sound lookup's trends block. When
trend_analysis=falseit hasanalyzed: falseandstatus: "skipped". See Trends block.
- Name
videos- Type
- HashtagCustomerVideo[]
- Description
The full normalized video list — same per-video schema as sound lookups (
id,url,description,publish_date,duration_seconds,views,likes,comments,shares,collects,hashtags,is_duet,is_stitch,region, andauthor { unique_id, nickname, is_verified, follower_count }). Returned in the requestedsortorder.
Request
curl https://api.virlo.ai/v1/satellite/hashtags/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
-H "Authorization: Bearer {token}"
Response
{
"data": { "status": "processing" }
}
Stats block
Computed mechanically from the sampled videos. Not LLM-derived. Because ordering is applied to the collected corpus (see the sort param), stats are identical regardless of the requested sort.
- Name
videos_analyzed- Type
- number
- Description
Number of videos actually used to compute the stats.
- Name
views- Type
- object
- Description
{ total, avg, median, max, min }— basic view-count distribution.
- Name
engagement- Type
- object
- Description
total_likes,total_comments,total_shares,total_collects, theiravg_*counterparts, andengagement_rate. Not comparable across platforms — YouTube and Instagram zero out some inputs (see Per-platform coverage).
- Name
velocity- Type
- object
- Description
videos_per_week(ISO-week buckets, capped at 26 weeks),last_4w_avg_videos_per_week,prior_4w_avg_videos_per_week, andis_accelerating(±15% deadband — anything inside the band is not flagged as accelerating).
- Name
verified_creator_count / verified_creator_pct- Type
- number
- Description
Count and share of unique verified creators among the sample.
- Name
duration_distribution- Type
- object
- Description
{ under_15s, between_15_30s, between_30_60s, over_60s }— counts only.
- Name
top_creators- Type
- HashtagStatsCreator[]
- Description
Top 10 creators by
total_viewsaccrued under this hashtag, deduplicated byunique_id.
- Name
related_hashtags- Type
- HashtagStatsRelatedTag[]
- Description
Top 20 co-occurring hashtags across the sample (the looked-up tag itself is excluded), normalized to lowercase.
- Name
top_sounds- Type
- HashtagStatsSound[]
- Description
Top 10 sounds used across the sample. Each entry:
{ external_id, title, artist, is_original, used_count, total_views, avg_views }. Empty on YouTube — Shorts hashtag pages carry no audio attribution.
- Name
top_video- Type
- HashtagCustomerVideo | null
- Description
Highest-view video in the sample. Null on empty corpora.
Trends block
Only meaningful when trend_analysis=true. Identical shape to the sound lookup's trends block — the block is always present so polling code can branch on trends.analyzed without optional chaining.
- Name
analyzed- Type
- boolean
- Description
True iff
trend_analysis=truewas on the request.
- Name
status- Type
- 'ok' | 'insufficient_corpus' | 'skipped'
- Description
ok= trends were generated.insufficient_corpus= the sample was too small to be statistically meaningful.skipped= the caller didn't ask for trends.
- Name
summary- Type
- string
- Description
Plain-language overview of what's happening under the hashtag. Suitable for direct rendering in dashboards.
- Name
trends- Type
- HashtagTrendItem[]
- Description
Individual trend entries — each with
name,description,tactics[],confidence,video_count,evidence_video_ids[],time_windows[],resurged, andmomentum, exactly as documented for sound trends.
- Name
model_used- Type
- string | null
- Description
Which LLM finished the job.
- Name
cost_usd / tokens_used- Type
- number | null
- Description
Bookkeeping. Useful for cost dashboards on the customer side.
No date hallucination is possible. time_windows[], resurged, and momentum are computed mechanically from the real publish_date values of the evidence videos in your sample — the LLM only clusters video IDs into named patterns.
Per-platform coverage
Each platform's hashtag surface exposes a different slice of data. Document-honest field gaps:
- Name
tiktok- Description
Native challenge feed. The richest source — full engagement fields,
region,is_duet/is_stitch, and sound attribution (top_sounds) all populated. Supports alldepthtiers (standard/deep/full).
- Name
instagram- Description
Google-indexed public reels — best-effort coverage. Upstream depth is capped at ~11 pages, so corpora run shallower than TikTok or YouTube.
depth=standardonly —deep/fullreturn400before billing (never charged).sharesandcollectsare always0,is_duet/is_stitchare alwaysfalse, andregionisnull.
- Name
youtube- Description
Native hashtag page, Shorts only. Supports all
depthtiers (standard/deep/full). Each Short is enriched through Virlo's video-details pipeline: exactviews,likes,comments,publish_date,duration_seconds, channelfollower_count, and sound attribution (sotop_soundsworks on YouTube).sharesandcollectsremain0— YouTube exposes no public counts.authoris the channel (unique_idis the channel id) andregionisnull. A Short that can't be enriched degrades to views + title only.
Because the engagement inputs differ per platform, never compare engagement_rate across platforms. Compare hashtags within a single platform instead.
Durable runs (re-read for free)
Every completed hashtag lookup is persisted as a satellite_run row (type hashtag_lookup) owned by your team. You can re-read it for free, forever via:
GET /v1/satellite/runs/:run_id— full result envelope, just like the status poll.GET /v1/satellite/runs?type=hashtag_lookup&platform=tiktok— paginated list of your hashtag runs (thetypefilter enum includeshashtag_lookup).GET /v1/satellite/runs/:run_id/videos?limit=50&offset=0— paginated slice of the videos array (useful when the run has ~300 entries).
The run_id is included on every completed result envelope and never expires, unlike the 24-hour status cache. To refresh data, start a new lookup — that will cost credits again. This is the same "pay once, read forever" contract that applies to every Satellite endpoint.
See the Satellite overview for the run-read endpoints and the Webhooks reference for the satellite.lookup.completed event (with type: "hashtag_lookup" discriminator) that fires on completion or failure.
Error responses
- Name
400 Bad Request- Description
Invalid platform (must be
tiktok,instagram, oryoutube) or an invalid hashtag (multiple tags, spaces, over 100 characters). Rejected before billing — no credits are charged.
- Name
401 Unauthorized- Description
Missing or invalid API key.
- Name
402 Payment Required- Description
Insufficient prepaid balance. The full price of the requested configuration must be available:
$0.50standard,$1.00withtrend_analysis=trueordepth=deep,$2.00fordepth=full,$2.50fordepth=full+ trends.
- Name
404 Not Found- Description
Job ID not found or expired (24h status-cache TTL). Use the
run_idto re-read for free.
Async workflow & timing
Status flow
processing → completed
→ failed
Timing expectations
| Configuration | Typical Duration |
|---|---|
Default (max_videos=50) | 1-3 minutes |
trend_analysis=true (~300) | ~8 minutes average, plan for up to 20 |
Always continue polling until status reaches completed or failed. Don't hardcode timeouts. Polling is free.
Recommended polling interval: Every 10-15 seconds (every 30 seconds for trend_analysis runs).
Notes
- The start endpoint returns within 1–2 seconds. All fetching, normalization, stats computation, and LLM analysis happen in the background.
- The hashtag is normalized to lowercase before collection and dedup —
#CleanTok,cleantok, and%23cleantokare the same lookup. - Re-requesting the same platform + hashtag within 6 hours returns the cached run free (
cached: true) — provided the stored run covers the request: samesort, trends already analyzed if you ask fortrend_analysis=true, and a storeddepthequal to or deeper than the one you request. A request the cache can't satisfy (differentsort, a trend upgrade, or a deeperdepth) re-scrapes immediately and bills normally. sortonly changes the order ofvideos[](top= views descending,recent= publish date descending). The platforms expose no server-side sort on hashtag feeds, so ordering is applied to the collected corpus;statsare order-independent.sample_quality.truncated_by_captells you the fetch hit its page cap;pages_fetchedandnote(insufficient_corpus→deep_corpus) tell you how much corpus backs the stats. Instagram corpora run shallower by design (upstream ~11-page depth cap).- Trends are intentionally NOT branded as "rising/peaking/fading" — the honest framing is the explicit
time_windows[]+resurged+momentumtriple, which tells you when each pattern fired and whether it came back stronger.
