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.


GET/v1/satellite/hashtags/:platform/:hashtag

Start Hashtag Lookup

Queue a hashtag lookup job. Returns immediately with a job_id. Poll the status route for results.

Cost per request:$0.50

Pricing summary:

  • $0.50 base (50 credits) — covers fetch + normalization + stats at depth=standard (up to max_videos videos).
  • + $0.50 surcharge (50 credits) when trend_analysis=true. That flag also forces a deeper fetch (~300 videos) and ignores max_videos. The surcharge uses the same satellite_trend_analysis pricing 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 when trend_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_id are free.
  • Repeat lookups are deduplicated: re-requesting the same lookup within 6 hours returns the existing completed run instantly with cached: true and no charge. The cache only applies when the stored run covers what you asked for — same sort, trend analysis already present if you request trend_analysis=true, and a stored depth equal 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, or youtube. 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 returns 400 and 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 the trends block. Adds the $0.50 surcharge. When true, max_videos is ignored. Default false.

  • Name
    max_videos
    Type
    integer
    Description

    Number of videos to fetch when trend_analysis is false (1–100). Default 50. Ignored when trend_analysis=true. Also ignored when depth is deep or full.

  • Name
    sort
    Type
    string
    Description

    Order of the returned videos[]: top (default, views descending) or recent (publish date descending). The platforms expose no server-side sort on hashtag feeds, so ordering is applied to the collected corpus — stats are order-independent either way.

  • Name
    depth
    Type
    string
    Description

    How deep to collect: standard (default — up to max_videos videos), deep (~300 videos, +$0.50 / 50 credits), or full (~500 videos, +$1.50 / 150 credits). Same tier shape as tracking's post-collection. deep and full override max_videos — it's ignored on those tiers. The deep surcharge is waived when trend_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 supports depth=standard only — its Google-indexed feed caps at ~11 pages, so deep/full on Instagram return 400 before billing (never charged). TikTok and YouTube support all three tiers.

Request

GET
/v1/satellite/hashtags/:platform/:hashtag
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"
  }
}

GET/v1/satellite/hashtags/status/:job_id

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.

Result shape (when completed)

  • Name
    run_id
    Type
    string
    Description

    Durable run identifier. Save it — re-reads via /v1/satellite/runs/:run_id are 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_videos echoes the effective target: your requested value at depth=standard (50 default, up to 100), 300 at deep, 500 at full.

  • 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 (50 standard, 100 with depth=deep or trend analysis, 200 for depth=full, 250 for 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 by cached: true on the start response and X-Credits-Used: 0 on 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 }. note is one of insufficient_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=false it has analyzed: false and status: "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, and author { unique_id, nickname, is_verified, follower_count }). Returned in the requested sort order.

Request

GET
/v1/satellite/hashtags/status/:job_id
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, their avg_* counterparts, and engagement_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, and is_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_views accrued under this hashtag, deduplicated by unique_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.


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=true was 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, and momentum, 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.


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 all depth tiers (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=standard onlydeep/full return 400 before billing (never charged). shares and collects are always 0, is_duet/is_stitch are always false, and region is null.

  • Name
    youtube
    Description

    Native hashtag page, Shorts only. Supports all depth tiers (standard/deep/full). Each Short is enriched through Virlo's video-details pipeline: exact views, likes, comments, publish_date, duration_seconds, channel follower_count, and sound attribution (so top_sounds works on YouTube). shares and collects remain 0 — YouTube exposes no public counts. author is the channel (unique_id is the channel id) and region is null. A Short that can't be enriched degrades to views + title only.


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 (the type filter enum includes hashtag_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, or youtube) 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.50 standard, $1.00 with trend_analysis=true or depth=deep, $2.00 for depth=full, $2.50 for depth=full + trends.

  • Name
    404 Not Found
    Description

    Job ID not found or expired (24h status-cache TTL). Use the run_id to re-read for free.


Async workflow & timing

Status flow

processing → completed
           → failed

Timing expectations

ConfigurationTypical Duration
Default (max_videos=50)1-3 minutes
trend_analysis=true (~300)~8 minutes average, plan for up to 20

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 %23cleantok are 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: same sort, trends already analyzed if you ask for trend_analysis=true, and a stored depth equal to or deeper than the one you request. A request the cache can't satisfy (different sort, a trend upgrade, or a deeper depth) re-scrapes immediately and bills normally.
  • sort only changes the order of videos[] (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; stats are order-independent.
  • sample_quality.truncated_by_cap tells you the fetch hit its page cap; pages_fetched and note (insufficient_corpusdeep_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 + momentum triple, which tells you when each pattern fired and whether it came back stronger.

Was this page helpful?