# Hashtag lookup

> Look up one hashtag on TikTok, Instagram, or YouTube Shorts and get a sample of the videos posted under it, with stats on views, creators, related hashtags, top sounds, and the weeks those videos were posted. Add optional trend analysis: the creative patterns in those videos.

Source: https://dev.virlo.ai/docs/satellite/hashtags
Markdown: https://dev.virlo.ai/docs/satellite/hashtags.md
Section: Deep-dive lookups

## About the Virlo API (applies to every page)

- Base URL: `https://api.virlo.ai/v1`. Every request needs the header `Authorization: Bearer YOUR_API_KEY` (keys start with `virlo_tkn_`).
- Responses are JSON inside a `data` field, except the `/v1/webhooks` endpoints, which return the object or array directly. Field names are snake_case.
- Prices are in US dollars from a prepaid balance. 1 credit = $0.01. The `X-Cost` response header on each successful response is the exact charge. Errors are free.
- Slow jobs return an ID. Check its status every 15 seconds (or whatever `retry_after_seconds` says) until `finalized` is `true`.
- All docs pages: https://dev.virlo.ai/llms.txt. Every page in one file: https://dev.virlo.ai/llms-full.txt. MCP server for AI assistants: https://dev.virlo.ai/api/mcp/mcp.

---

Size up one hashtag on TikTok, Instagram, or YouTube Shorts before you build a campaign or brief creators around it.

**At a glance**

- **What it does:** Collects a sample of videos from one hashtag and sums up their views, creators, related tags, sounds, and posting dates. Optional trend analysis finds their creative patterns.
- **You send:** The platform (`tiktok`, `instagram`, or `youtube`) and one hashtag, without the `#`.
- **You get back:** A `job_id` right away. Check it every 10 to 15 seconds (this is called polling) until it says `completed`, then read the results.
- **Cost:** $0.50, or up to $2.50 with more videos and trend analysis. Status checks, saved results, and the same lookup repeated within 6 hours are free.
- **How long:** About 15 seconds on TikTok and 1 minute on YouTube or Instagram. Trend analysis on TikTok takes about 2 minutes. Bigger YouTube lookups take a few minutes.

Every number comes from a sample: the videos the platform lists first, not every video under the hashtag. On TikTok the sample can span years. For quick numbers on many hashtags, use [Hashtags](https://dev.virlo.ai/docs/hashtags) ($0.05 a request).

---

## What it costs

You pay when the lookup starts. 1 credit is $0.01.

| You ask for | Videos collected, at most | Price |
| - | - | - |
| Standard (the default) | 50, or up to 100 with `max_videos` | $0.50 (50 credits) |
| `trend_analysis=true`, `depth=deep`, or both | About 300 | $1.00 (100 credits) |
| `depth=full` | About 500 | $2.00 (200 credits) |
| `depth=full` with trend analysis | About 500 | $2.50 (250 credits) |

- **A failed lookup is refunded in full**, and the trend fee when no trends come out. Finding fewer videos than you paid for isn't refunded: `depth=deep` on YouTube #cleantok found 103 videos and still cost $1.00. For a small hashtag, try `max_videos=100` first.
- **Repeats within 6 hours are free** (`cached: true`) when `sort` matches and the saved lookup already has the trends and `depth` you ask for (or deeper). A different `max_videos` still gets the saved result as it was. Anything else is a new, billed lookup.
- **Your balance must cover the full price**, even for a free repeat. To skip that check, [open the saved run](https://dev.virlo.ai/docs/satellite/hashtags#durable-runs).
- **The exact charge** is in the `X-Cost` (dollars) and `X-Credits-Used` (credits) response headers.

---

## Start a hashtag lookup

**Endpoint:** `GET https://api.virlo.ai/v1/satellite/hashtags/:platform/:hashtag`

Starts a lookup in the background and returns its ID at `data.job_id`. Each new lookup is billed (unusual for a `GET`), so don't call it in a loop. A free repeat returns `status: "completed"` and `cached: true` instead.

Base price, before options: $0.50

Up to $2.50 with options. See [What it costs](https://dev.virlo.ai/docs/satellite/hashtags#pricing).

### Path parameters

- `platform` (string, required): `tiktok`, `instagram`, or `youtube`, in lowercase. YouTube collects Shorts only.
- `hashtag` (string, required): One tag, up to 100 characters, any case. Leave off the `#` (or send it as `%23`). Spaces and other `%` signs are rejected. Comma lists like `cleantok,organizing` are not: they run as one tag, and you're billed.

### Query parameters

- `trend_analysis` (boolean, optional): `true` adds [trend analysis](https://dev.virlo.ai/docs/satellite/hashtags#trends-block) (+$0.50) and collects about 300 videos (500 with `depth=full`). Only `true` or `1` work. `True` or `yes` count as `false`, with no error. Default `false`.
- `max_videos` (integer, optional): How many videos to collect, 1 to 100. Default `50`. Standard lookups only.
- `sort` (string, optional): `top` (most views first, the default) or `recent` (newest first). It only reorders the videos, but changing it bills a new lookup.
- `depth` (string, optional): `standard` (the default, uses `max_videos`), `deep` (up to about 300 videos, +$0.50, no extra with trend analysis), or `full` (up to about 500, +$1.50). The charge stays the same if fewer turn up. Not available on Instagram.

**cURL TikTok request:**

```bash
curl -G https://api.virlo.ai/v1/satellite/hashtags/tiktok/cleaninghacks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d trend_analysis=true
```

**cURL YouTube request:**

```bash
# The '#' is optional. If you include it, write it as %23.
curl -G https://api.virlo.ai/v1/satellite/hashtags/youtube/%23cleantok \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d sort=recent \
  -d max_videos=100
```

**JavaScript request:**

```js
const hashtag = 'cleaninghacks'
const params = new URLSearchParams({ trend_analysis: 'true' })

const response = await fetch(
  `https://api.virlo.ai/v1/satellite/hashtags/tiktok/${hashtag}?${params}`,
  { headers: { Authorization: 'Bearer YOUR_API_KEY' } },
)
const body = await response.json()
const jobId = body.data.job_id // use this to check the status
```

**Python request:**

```python
import requests

hashtag = 'cleaninghacks'

response = requests.get(
    f'https://api.virlo.ai/v1/satellite/hashtags/tiktok/{hashtag}',
    headers={'Authorization': 'Bearer YOUR_API_KEY'},
    # Send the string 'true'. Python's True is sent as "True", which is ignored.
    params={'trend_analysis': 'true'},
)
job_id = response.json()['data']['job_id']  # use this to check the status
```

**Response 200 Started:**

```json
{
  "data": {
    "job_id": "0bebf4eb-01a0-4b3c-b880-3c36fe32c4ea",
    "status": "processing"
  }
}
```

**Response 200 Cached:**

```json
{
  "data": {
    "job_id": "0bebf4eb-01a0-4b3c-b880-3c36fe32c4ea",
    "status": "completed",
    "cached": true
  }
}
```

**Response 400:**

```json
{
  "message": "hashtag must be a single tag without spaces (e.g. \"cleantok\")",
  "error": "Bad Request",
  "statusCode": 400,
  "code": "validation_error"
}
```

**Response 402 With trends:**

```json
{
  "statusCode": 402,
  "message": "Insufficient balance. $1.00 required, $0.80 remaining. Add funds at https://dev.virlo.ai/dashboard/billing",
  "error": "Payment Required",
  "required_credits": 100,
  "remaining_credits": 80,
  "code": "insufficient_credits"
}
```

---

## Check lookup status

**Endpoint:** `GET https://api.virlo.ai/v1/satellite/hashtags/status/:job_id`

Free to call. `status` is `processing`, then `completed` (with `result`) or `failed` (with `error`, refunded in full). Keep checking until you see one of those.

This check works for 24 hours, then returns `404`. The same ID still opens the [saved run](https://dev.virlo.ai/docs/satellite/hashtags#durable-runs).

**Rather not poll?** The `satellite.lookup.completed` [webhook](https://dev.virlo.ai/docs/webhooks#lookup-payload), with `type: "hashtag_lookup"`, fires on success or failure. A free repeat sends no webhook, so read its result right away.

### What's in the result

- `run_id` (string, optional): Same value as your `job_id`. Reopens this result for free.
- `stats` (object, optional): See [Stats](https://dev.virlo.ai/docs/satellite/hashtags#stats-block).
- `trends` (object, optional): See [Trend analysis](https://dev.virlo.ai/docs/satellite/hashtags#trends-block). `analyzed: false` if you didn't ask for it.
- `videos` (array, optional): The collected videos, in your `sort` order.
- `request` (object, optional): Your settings as applied. `max_videos` shows the real target: `300` or `500` on bigger lookups.
- `sample_quality` (object, optional): `note` rates the sample: `insufficient_corpus` (under 10 videos), `small_corpus_workable` (10 to 49), `healthy_corpus` (50 to 199), or `deep_corpus` (200 or more). A hashtag with no videos completes with 0 videos rather than failing, so only a trend fee is refunded. `truncated_by_cap` is `true` when more videos existed.
- `credits_charged` (number, optional): A free repeat still shows the original amount. `X-Credits-Used` shows what you paid.
- `credits_refunded` (integer, optional): Only when the trend fee bought nothing: `50` credits paid back. A `failed` status also has it, for the whole charge.
- `hashtag / data_captured_at` (object / string, optional): The tag's `name`, `platform`, and `page_url`, and when collection finished (UTC).

**cURL request:**

```bash
curl https://api.virlo.ai/v1/satellite/hashtags/status/0bebf4eb-01a0-4b3c-b880-3c36fe32c4ea \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**JavaScript request:**

```js
async function waitForHashtag(jobId) {
  while (true) {
    const res = await fetch(
      `https://api.virlo.ai/v1/satellite/hashtags/status/${jobId}`,
      { headers: { Authorization: 'Bearer YOUR_API_KEY' } },
    )
    if (!res.ok) throw new Error(`Status check failed: ${res.status}`)
    const { data } = await res.json()
    if (data.status === 'completed') return data.result
    if (data.status === 'failed') throw new Error(data.error)
    await new Promise((r) => setTimeout(r, 15000)) // check again in 15 seconds
  }
}
```

**Python request:**

```python
import time
import requests

def wait_for_hashtag(job_id):
    url = f'https://api.virlo.ai/v1/satellite/hashtags/status/{job_id}'
    headers = {'Authorization': 'Bearer YOUR_API_KEY'}
    while True:
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        data = res.json()['data']
        if data['status'] == 'completed':
            return data['result']
        if data['status'] == 'failed':
            raise RuntimeError(data['error'])
        time.sleep(15)  # check again in 15 seconds
```

**Response Completed:**

```json
{
  "data": {
    "status": "completed",
    "result": {
      "run_id": "0bebf4eb-01a0-4b3c-b880-3c36fe32c4ea",
      "request": {
        "hashtag": "cleaninghacks",
        "platform": "tiktok",
        "trend_analysis": true,
        "max_videos": 300,
        "sort": "top",
        "depth": "standard"
      },
      "hashtag": {
        "name": "cleaninghacks",
        "platform": "tiktok",
        "page_url": "https://www.tiktok.com/tag/cleaninghacks"
      },
      "data_captured_at": "2026-09-24T17:48:57.782Z",
      "credits_charged": 100,
      "stats": {
        "videos_analyzed": 300,
        "views": { "total": 1529643491, "avg": 5098812, "median": 2628772, "max": 36788449, "min": 1011 },
        "engagement": {
          "total_likes": 93695696, "total_comments": 440623, "total_shares": 4075438, "total_collects": 10542388,
          "avg_likes": 312319, "avg_comments": 1469, "avg_shares": 13585, "avg_collects": 35141,
          "engagement_rate": 0.0711
        },
        "velocity": {
          "videos_per_week": [{ "iso_week": "2026-W38", "count": 8 }, { "iso_week": "2026-W39", "count": 14 }],
          "last_4w_avg_videos_per_week": 7.5,
          "prior_4w_avg_videos_per_week": 4.5,
          "is_accelerating": true
        },
        "verified_creator_count": 36,
        "verified_creator_pct": 0.12,
        "duration_distribution": { "under_15s": 33, "between_15_30s": 64, "between_30_60s": 47, "over_60s": 110 },
        "top_creators": [
          { "unique_id": "creator_a", "nickname": "Creator A", "is_verified": true, "follower_count": null, "video_count": 10, "total_views": 95651889, "avg_views": 9565189 }
        ],
        "related_hashtags": [
          { "hashtag": "cleantok", "used_count": 172, "total_views": 820116970, "avg_views": 4768122 }
        ],
        "top_sounds": [
          { "external_id": "7468711641658525713", "title": "SHAKE IT TO THE MAX (FLY) [REMIX]", "artist": "MOLIY & Shenseea & Skillibeng & Silent Addy", "is_original": false, "used_count": 2, "total_views": 38805562, "avg_views": 19402781 }
        ],
        "top_video": { "id": "7171372479372365099", "...": "same fields as a videos[] item" }
      },
      "sample_quality": { "truncated_by_cap": true, "pages_fetched": 17, "note": "deep_corpus" },
      "trends": {
        "analyzed": true,
        "status": "ok",
        "summary": "The #cleaninghacks corpus reveals a strong and continuous interest in practical, satisfying, and motivational cleaning content.",
        "trends": [
          {
            "name": "Quick Cleaning Hacks & Everyday Tips",
            "description": "Short, practical videos demonstrating clever and easy cleaning solutions or organizational tips for daily household problems.",
            "tactics": ["Present a common problem and a quick solution", "Target everyday household items or situations"],
            "confidence": 0.92,
            "video_count": 82,
            "evidence_video_ids": ["6984134268918271237", "7099896027146177797", "..."],
            "time_windows": [
              { "start_date": "2020-05-26", "end_date": "2026-09-04", "video_count": 82, "total_views": 494830450, "total_likes": 30599073, "total_comments": 130883, "total_shares": 1640360, "avg_views": 6034518, "description": "A continuous run of quick cleaning tips and life hacks from 2020 to 2026." }
            ],
            "resurged": false,
            "momentum": null
          }
        ],
        "model_used": "gemini-2.5-flash",
        "cost_usd": 0.0551801,
        "tokens_used": 83335
      },
      "videos": [
        {
          "id": "7171372479372365099",
          "url": "https://www.tiktok.com/@creator_b/video/7171372479372365099",
          "description": "Car retreading #fyp #carbeauty #cleaninghacks",
          "publish_date": "2022-11-29T09:48:06.000Z",
          "duration_seconds": 15,
          "views": 36788449, "likes": 630082, "comments": 4351, "shares": 30904, "collects": 69181,
          "hashtags": ["fyp", "carbeauty", "cleaninghacks"],
          "is_duet": false,
          "is_stitch": false,
          "region": "US",
          "author": { "unique_id": "creator_b", "nickname": "Creator B", "is_verified": false, "follower_count": null }
        }
      ]
    }
  }
}
```

**Response Processing:**

```json
{
  "data": { "status": "processing" }
}
```

**Response Failed:**

```json
{
  "data": {
    "status": "failed",
    "error": "Request failed with status code 404"
  }
}
```

**Response 404:**

```json
{
  "message": "Hashtag lookup job not found or expired",
  "error": "Not Found",
  "statusCode": 404,
  "code": "not_found"
}
```

---

## What each platform returns

Each platform shares different data, so compare hashtags within one platform.

| | TikTok | YouTube | Instagram |
| - | - | - | - |
| Where videos come from | The hashtag page, including photo slideshows (`url` contains `/photo/`) | The hashtag page, Shorts only | Public reels found through Google search, 100 to 150 at most, even with trend analysis |
| `depth` options | All three | All three | `standard` only |
| `views` | Yes | Yes | `0` when Virlo couldn't look up that reel. Treat it as unknown. |
| `shares`, `collects` (saves) | Yes | Always `0` | Always `0` |
| `duration_seconds` | `null` for slideshows | Yes | Often `null` |
| `author.unique_id` | Handle | Channel ID (starts with `UC`) | Handle |
| `author.follower_count` | `null` (use a [Creator lookup](https://dev.virlo.ai/docs/satellite)) | Channel subscribers | `null` |
| `top_sounds` | Yes | Yes | Usually empty |
| `is_duet`, `is_stitch`, `region` | Yes | `false`, `null` | `false`, `null` |

---

## Stats

Plain math over your sample, with no AI.

- `views` (object, optional): `total`, `avg`, `median`, `max`, and `min`.
- `engagement` (object, optional): Totals and averages of likes, comments, shares, and collects (saves). `engagement_rate` is their sum divided by total views (`0.0711` means 7.11%). YouTube and Instagram have no shares or saves, so don't compare it across platforms.
- `velocity` (object, optional): How many of the sampled videos were posted each week. It reflects the sample, not every post under the hashtag.
  - `videos_per_week`: counts by calendar week, for up to the 26 most recent weeks that have videos. Empty weeks are left out.
  - `last_4w_avg_videos_per_week`, `prior_4w_avg_videos_per_week`: the average of the latest 4 of those weeks (or fewer, if that's all), and of up to 4 weeks before them (`0` if none).
  - `is_accelerating`: `true` when the last-4-week average beats the prior one by 15% or more. Also `true` with 4 or fewer weeks of videos: ignore it then.
- `top_creators` (array, optional): Up to 10 creators with the most views in the sample.
- `related_hashtags` (array, optional): Up to 20 other hashtags on the same videos, ranked by total views, leaving out the one you looked up. `used_count` is how many videos use it.
- `top_sounds` (array, optional): Up to 10 sounds with the most views. `is_original` means the creator's own audio.
- `verified_creator_count / verified_creator_pct` (number, optional): How many videos come from verified accounts, and that share. It counts videos, not separate creators.
- `duration_distribution` (object, optional): Videos per length band. Videos of unknown length aren't counted, so the bands can add up to less than `videos_analyzed`.

---

## Trend analysis

Filled in only with `trend_analysis=true`. An AI model reads each video's caption, hashtags, and numbers (it doesn't watch the videos). It groups them into named creative patterns, with tactics a creator can copy. It reads up to 240 videos, most viewed first, so `depth=full` adds videos to the stats but not to the trends.

Each trend's `time_windows` are the date ranges when its videos were posted. `resurged` means the pattern faded and came back, and `momentum` compares views in the last two windows. A pattern that never paused has one window, `resurged: false`, and `momentum: null`.

`status` is `ok`, `skipped` (you didn't ask), or `insufficient_corpus` (under 10 videos, $0.50 refunded). If the AI step errors, you still get `ok` with an empty `trends` list, and the $0.50 is refunded too.

**Full field reference**

- `confidence` (number, optional): The AI's own score, from 0 to 1, for how clear the pattern is.
- `evidence_video_ids` (string[], optional): The videos that show the pattern. All of them are in `videos[]`.
- `time_windows` (array, optional): A sample covering under 14 days gets one window per trend.
- `momentum` (string | null, optional): `stronger` (average views 15% or more higher than the window before), `weaker` (15% or more lower), or `similar`.
- `model_used / cost_usd / tokens_used` (string / number, optional): What the AI step cost Virlo. You pay the flat $0.50.

A trend that came back after a gap:

**A trend that resurged:**

```json
{
  "name": "Before/after speed-cleans",
  "description": "Sub-30s transformations that hard-cut from mess to spotless on the beat.",
  "tactics": ["open on the worst angle of the mess", "single hard cut to the reveal", "text overlay with the time it took"],
  "confidence": 0.84,
  "video_count": 78,
  "evidence_video_ids": ["7412345678901234567", "7419876543210987654", "..."],
  "time_windows": [
    { "start_date": "2026-06-14", "end_date": "2026-06-30", "video_count": 31, "total_views": 44000000, "total_likes": 3600000, "total_comments": 21000, "total_shares": 96000, "avg_views": 1419355, "description": "First wave, mostly small-account transformations." },
    { "start_date": "2026-07-18", "end_date": "2026-07-31", "video_count": 47, "total_views": 88000000, "total_likes": 7400000, "total_comments": 43000, "total_shares": 214000, "avg_views": 1872340, "description": "Came back with larger creators joining the format." }
  ],
  "resurged": true,
  "momentum": "stronger"
}
```

---

## Re-read past runs

Every paid lookup is saved as its own run that never expires. Reopening it is free:

- `GET /v1/satellite/runs/:run_id`: the full saved result, under `result`.
- `GET /v1/satellite/runs?type=hashtag_lookup&platform=tiktok`: your past hashtag lookups.
- `GET /v1/satellite/runs/:run_id/videos?limit=50&offset=0`: the videos, a page at a time (`limit` up to 200, default 25).

**A newer lookup of the same hashtag gets a new `run_id`.** That happens after 6 hours, or sooner if you change `sort`, add trends, or ask for a bigger `depth`. The older run stays as it was, even if the new lookup fails.

---

## Errors

These happen before a lookup starts, so they are never charged.

- `400 Bad Request`: Unknown platform. Empty hashtag, over 100 characters, or with a space or a `%` other than `%23`. `depth=deep` or `full` on Instagram. Unknown `depth` or `sort` value, `max_videos` outside 1 to 100, or an unknown query parameter.
- `401 Unauthorized`: API key missing (`missing_api_key`), or wrong or revoked (`invalid_api_key`). See [Authentication](https://dev.virlo.ai/docs/authentication).
- `402 Payment Required`: Balance below the full price, even for a repeat that would be free. Under $0.50, the error always says $0.50.
- `404 Not Found`: Status check only: wrong ID, another lookup type's ID, or over 24 hours old.
- `429 Too Many Requests`: Your key's daily request limit is used up. Lookups have no per-minute or hourly limit. See [Rate limits](https://dev.virlo.ai/docs/rate-limits).

---

More in Deep-dive lookups:

- [Creator lookup](https://dev.virlo.ai/docs/satellite.md)
- [Creator lookup add-ons](https://dev.virlo.ai/docs/satellite/creators.md)
- [Video outlier check](https://dev.virlo.ai/docs/satellite/video-outlier.md)
- [Sound lookup](https://dev.virlo.ai/docs/satellite/sounds.md)
