# Creator lookup add-ons

> Two add-ons for a creator lookup. Trend analysis finds the formats and hooks a creator keeps repeating, with dates and momentum. Data Intelligence has AI break down every video.

Source: https://dev.virlo.ai/docs/satellite/creators
Markdown: https://dev.virlo.ai/docs/satellite/creators.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.

---

A [creator lookup](https://dev.virlo.ai/docs/satellite) gets a creator's profile, stats and videos for $0.50. Two add-ons go deeper: trend analysis finds the formats they keep coming back to, and Data Intelligence has AI break down every video.

**At a glance**

- **What it does:** Trend analysis maps a creator's patterns over time. Data Intelligence explains why each video works.
- **You send:** A normal creator lookup, `GET /v1/satellite/creator/:platform/:username`, with `trend_analysis=true`, `data_intelligence=true` or both.
- **You get back:** Patterns with dates, example videos and momentum. With Data Intelligence, an AI read of every video plus a "what's working" summary.
- **Cost:** $0.50. Trends add $0.50, Data Intelligence $0.25, audience data $0.50. All on: up to $1.75.
- **How long:** Trends: 30 seconds to a few minutes. Data Intelligence: a few more minutes, up to about 10 for big lookups.

Lookups run in the background. Check the status of the returned `job_id` every 10 seconds (free) until it isn't `processing`. Data Intelligence lands later.

---

## When to use them

| Turn on | When you want |
| - | - |
| Trend analysis | A creator's playbook: the formats and hooks they repeat, drop and bring back, with real videos as proof. |
| Data Intelligence | Why single videos work: each one's hook, format, visual style and brands. |
| Neither | Only stats, hashtags or outliers (videos that beat the creator's usual views): the base lookup covers those. Audience data is its own [add-on](https://dev.virlo.ai/docs/satellite). |

---

## What it costs

You pay when the lookup starts. Checking its status is free.

| What you turn on | Price |
| - | - |
| The creator lookup itself | $0.50 |
| Trend analysis (`trend_analysis=true`) | +$0.50 |
| Data Intelligence (`data_intelligence=true`) | +$0.25 |
| Audience data (`audience_demographics`, `audience_geography` or both) | +$0.50, free if Virlo measured it in the last 30 days ([details](https://dev.virlo.ai/docs/satellite#pricing)) |
| Everything on | $1.75 |
| The same creator within 6 hours | $0.00 if the earlier lookup had everything you ask for ([Repeat lookups](https://dev.virlo.ai/docs/satellite/creators#repeat-lookups)) |

- **Your real charge** is in the start call's `X-Cost` (dollars) or `X-Credits-Used` (credits, 1 credit = $0.01) header. `credits_charged` counts only the base and trend fees: a $0.75 Data Intelligence lookup shows `50`.
- **Failed lookups are refunded in full**, such as a handle that doesn't exist, is private or has no posts, or a lookup that stalls and is closed out as failed.
- **The trend fee is refunded when no trends come out**, the audience fee when no new snapshot was needed, and the Data Intelligence fee when there were no posts to enrich or its analysis failed. The result shows it as `credits_refunded`, in credits.

---

## Before you start

- **Single lookups only.** The [batch lookup](https://dev.virlo.ai/docs/satellite#batch-creator-lookup) rejects `trend_analysis` and `data_intelligence` with a `400` error.
- **Write the flag as lowercase `true`.** `True`, `1` or `yes` count as off, with no error, and you pay for a plain lookup. In Python, send the string `"true"`.
- **Up to 5 lookups per minute.** Then you get a `429` error. See [Rate limits](https://dev.virlo.ai/docs/rate-limits).

---

## Turn on trend analysis

Add `trend_analysis=true` to a creator lookup. Three things change:

- **It reads up to 300 of the creator's latest videos** and ignores `max_videos`.
- **The video list is always included**, so you can see the videos behind each pattern.
- **On YouTube, only Shorts count.** Fewer than 10 Shorts gives no trends (trend fee refunded), and a channel with no Shorts fails (fully refunded).

**cURL request:**

```bash
# 1. Start the lookup. It returns a job ID right away.
curl -G https://api.virlo.ai/v1/satellite/creator/youtube/mkbhd \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d trend_analysis=true

# 2. Check the status. Repeat every 10 seconds until it isn't "processing".
curl https://api.virlo.ai/v1/satellite/creator/status/{job_id} \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**JavaScript request:**

```js
const base = 'https://api.virlo.ai/v1/satellite'
const headers = { Authorization: 'Bearer YOUR_API_KEY' }

// 1. Start the lookup. It returns a job ID right away.
const start = await fetch(
  `${base}/creator/youtube/mkbhd?trend_analysis=true`,
  { headers },
).then((r) => r.json())
const jobId = start.data.job_id

// 2. Check the status every 10 seconds until it's done.
let poll
do {
  await new Promise((r) => setTimeout(r, 10_000))
  poll = await fetch(`${base}/creator/status/${jobId}`, { headers }).then(
    (r) => r.json(),
  )
} while (poll.data.status === 'processing')

// 'completed': the trends are in poll.data.result.trends
// 'failed': the reason is in poll.data.error
```

**Python request:**

```python
import time
import requests

base = "https://api.virlo.ai/v1/satellite"
headers = {"Authorization": f"Bearer YOUR_API_KEY"}

# 1. Start the lookup. Send the flag as the string "true", not True.
start = requests.get(
    f"{base}/creator/youtube/mkbhd",
    headers=headers,
    params={"trend_analysis": "true"},
).json()
job_id = start["data"]["job_id"]

# 2. Check the status every 10 seconds until it's done.
while True:
    time.sleep(10)
    poll = requests.get(f"{base}/creator/status/{job_id}", headers=headers).json()
    if poll["data"]["status"] != "processing":
        break

# "completed": the trends are in poll["data"]["result"]["trends"]
# "failed": the reason is in poll["data"]["error"]
```

**Response Started:**

```json
{
  "data": {
    "job_id": "3f9c2a71-5d4e-4b8a-9c1f-2e7d6a0b8c45",
    "status": "processing"
  }
}
```

**Response Repeat (free):**

```json
{
  "data": {
    "job_id": "3f9c2a71-5d4e-4b8a-9c1f-2e7d6a0b8c45",
    "status": "completed",
    "cached": true
  }
}
```

`cached: true` means you looked this creator up in the last 6 hours with at least these options, so you get that result free.

---

## What trends look like

The finished result has a `trends` block only if you asked for it. It holds a `summary` and a list of patterns at `result.trends.trends`. Each pattern has:

- `name`, `description` and a few `tactics`.
- `confidence`, from 0 to 1. Use it to rank patterns.
- `evidence_video_ids`: example videos from the result's `videos` list.
- `time_windows`: the stretches of time the creator used it, with real dates and view totals.
- `resurged`: `true` when there's more than one stretch (dropped, then brought back).
- `momentum`: `stronger`, `weaker` or `similar`, comparing average views in the latest stretch with the one before. `null` with one stretch.

**JSON:**

```json
{
  "data": {
    "status": "completed",
    "result": {
      "username": "mkbhd",
      "platform": "youtube",
      "profile": { /* ... */ },
      "stats": { /* ... */ },
      "hashtags": [ /* ... */ ],
      "content_tags": [ /* ... */ ],
      "videos": [
        {
          "id": "abc123",
          "title": "Pixel vs iPhone: blind camera test",
          "views": 4210000,
          "likes": 151000,
          "comments": 3900,
          "publish_date": "2025-11-03T17:00:00.000Z",
          "url": "https://www.youtube.com/shorts/abc123"
          /* ...more fields, and more videos */
        }
      ],
      "trends": {
        "analyzed": true,
        "summary": "MKBHD's Shorts center on quick first looks at new phones and gadgets, with recurring side-by-side camera tests.",
        "trends": [
          {
            "name": "Blind phone camera tests",
            "description": "Side-by-side clips that compare two flagship phones on the same shot and ask viewers to pick a winner.",
            "tactics": [
              "Split screen from the first frame",
              "Asks viewers to vote before the reveal"
            ],
            "confidence": 0.91,
            "video_count": 6,
            "evidence_video_ids": ["abc123", "def456", "ghi789", "jkl012", "mno345", "pqr678"],
            "time_windows": [
              {
                "start_date": "2025-11-03",
                "end_date": "2026-01-20",
                "video_count": 3,
                "total_views": 12400000,
                "total_likes": 410000,
                "total_comments": 28000,
                "total_shares": 0,
                "avg_views": 4133333,
                "description": "Comparisons around the fall flagship launches"
              },
              {
                "start_date": "2026-06-02",
                "end_date": "2026-08-25",
                "video_count": 3,
                "total_views": 19800000,
                "total_likes": 620000,
                "total_comments": 41000,
                "total_shares": 0,
                "avg_views": 6600000,
                "description": "Came back with the summer phone launches"
              }
            ],
            "resurged": true,
            "momentum": "stronger"
          }
        ],
        "status": "ok"
      },
      "run_id": "3f9c2a71-5d4e-4b8a-9c1f-2e7d6a0b8c45",
      "credits_charged": 100
    },
    "pending_jobs": [],
    "finalized": true
  }
}
```

**Full field reference**

- `trends.status` (string, optional): `ok` or `insufficient_corpus` (fewer than 10 videos). `ok` with an empty
  `trends` list means no clear pattern, or, with an empty `summary` and
  `credits_refunded` on the result, that the AI step failed and the trend
  fee was refunded.
- `resurged` (boolean, optional): If every video was posted within 14 days, each pattern gets one stretch,
  so this is `false`.
- `momentum` (string | null, optional): `stronger` or `weaker` means average views changed by 15% or more.
- `time_windows[]` (object[], optional): Oldest first. `total_shares` is `0` on YouTube and Instagram, which don't
  return share counts here.
- `run_id` (string, optional): The saved run's ID, the same value as the job ID.
- `pending_jobs` (array, optional): Audience snapshots still building. Never Data Intelligence.
- `finalized` (boolean, optional): `true` once the lookup and any audience snapshot are done. It doesn't
  wait for Data Intelligence.

Other result fields are on [Creator lookup](https://dev.virlo.ai/docs/satellite).

### Not enough videos

Trends need at least 10 videos. With fewer, the trend fee is refunded (`credits_refunded: 50`) and you get this:

**JSON:**

```json
{
  "trends": {
    "analyzed": true,
    "summary": "",
    "trends": [],
    "status": "insufficient_corpus"
  }
}
```

Check their video count on their profile first.

---

## See the videos behind each trend

This prints each pattern with its stretches and example videos.

**JavaScript:**

```js
// `poll` is the finished status response from "Turn on trend analysis" above.
const result = poll.data.result ?? {}
// Empty when the lookup failed or the result has no trends.
const trends = result.trends?.trends ?? []
const videos = result.videos ?? []

for (const trend of trends) {
  const evidenceVideos = trend.evidence_video_ids
    .map((id) => videos.find((v) => v.id === id))
    .filter(Boolean)

  console.log(`${trend.name}: ${trend.resurged ? 'came back after a gap' : 'one continuous run'}`)
  console.log(`Momentum: ${trend.momentum ?? 'n/a, only one time window'}`)
  for (const w of trend.time_windows) {
    console.log(
      `  ${w.start_date} to ${w.end_date}: ${w.video_count} videos, ${w.avg_views.toLocaleString()} avg views. ${w.description}`,
    )
  }
  for (const v of evidenceVideos) {
    console.log(
      `  - ${(v.publish_date ?? '').slice(0, 10)} | ${(v.views ?? 0).toLocaleString()} views | ${v.url}`,
    )
  }
}
```

**Output:**

```text
Blind phone camera tests: came back after a gap
Momentum: stronger
  2025-11-03 to 2026-01-20: 3 videos, 4,133,333 avg views. Comparisons around the fall flagship launches
  2026-06-02 to 2026-08-25: 3 videos, 6,600,000 avg views. Came back with the summer phone launches
  - 2025-11-03 | 4,210,000 views | https://www.youtube.com/shorts/abc123
```

---

## Data Intelligence

Add `data_intelligence=true` and AI reads every video: its hook, format, visual style, brands and more. You also get a "what's working" summary. It gives 43 of the [Data Intelligence](https://dev.virlo.ai/docs/intelligence) fields Content Research Agents get per video. The video list is always included.

- **It finishes after the lookup**, usually a few minutes later and up to about 10 for big lookups. The status check says `completed` and `finalized: true` before then. That's normal.
- **Read it from the saved run**, `GET /v1/satellite/runs/:run_id` (the run ID is your job ID). Check until `status` is `completed` and `intelligence_status` is no longer `pending`. If it says `ready`, the summary is in `analysis` and each video has `intelligence`.
- `context` (string, optional): Optional, free. A goal of up to 600 characters that the summary answers.
- `slideshow_sort` (string, optional): Optional, free. AI reads the slide text of up to 100 slideshows. If
  there are more, `popular` (default) picks the most liked and commented,
  `recent` the newest.

**cURL request:**

```bash
# 1. Start the lookup with data intelligence on.
curl -G https://api.virlo.ai/v1/satellite/creator/tiktok/mkbhd \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d data_intelligence=true \
  --data-urlencode "context=which hooks drive their most-viewed videos"

# 2. Read the saved run (its ID is the job ID). Repeat every 30 seconds until
#    "status" is "completed" and "intelligence_status" is no longer "pending".
#    Give up after about 12 minutes.
curl https://api.virlo.ai/v1/satellite/runs/{job_id} \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**JavaScript request:**

```js
const base = 'https://api.virlo.ai/v1/satellite'
const headers = { Authorization: 'Bearer YOUR_API_KEY' }

// 1. Start the lookup with data intelligence on.
const params = new URLSearchParams({
  data_intelligence: 'true',
  context: 'which hooks drive their most-viewed videos',
})
const start = await fetch(`${base}/creator/tiktok/mkbhd?${params}`, {
  headers,
}).then((r) => r.json())
const runId = start.data.job_id // the job ID is also the run ID

// 2. Check the saved run every 30 seconds. Stop once the lookup is done and
//    the AI breakdown is no longer pending. Give up after about 12 minutes.
let run
for (let i = 0; i < 24; i++) {
  await new Promise((r) => setTimeout(r, 30_000))
  const res = await fetch(`${base}/runs/${runId}`, { headers })
  run = (await res.json()).data
  if (run.status === 'failed') break
  if (run.status === 'completed' && run.intelligence_status !== 'pending') break
}

// 'ready': the summary is in run.analysis, and each video in
// run.result.videos has an `intelligence` object.
```

**Python request:**

```python
import time
import requests

base = "https://api.virlo.ai/v1/satellite"
headers = {"Authorization": f"Bearer YOUR_API_KEY"}

# 1. Start the lookup. Send the flag as the string "true", not True.
start = requests.get(
    f"{base}/creator/tiktok/mkbhd",
    headers=headers,
    params={
        "data_intelligence": "true",
        "context": "which hooks drive their most-viewed videos",
    },
).json()
run_id = start["data"]["job_id"]  # the job ID is also the run ID

# 2. Check the saved run every 30 seconds. Stop once the lookup is done and
#    the AI breakdown is no longer pending. Give up after about 12 minutes.
for _ in range(24):
    time.sleep(30)
    run = requests.get(f"{base}/runs/{run_id}", headers=headers).json()["data"]
    if run["status"] == "failed":
        break
    if run["status"] == "completed" and run["intelligence_status"] != "pending":
        break

# "ready": the summary is in run["analysis"], and each video in
# run["result"]["videos"] has an "intelligence" object.
```

**Response Ready:**

```json
{
  "data": {
    "run_id": "7c1e9b24-0a3f-4d6e-b5c8-91f2e4a7d630",
    "type": "creator_lookup",
    "platform": "tiktok",
    "status": "completed",
    "subject": "mkbhd",
    /* created_at, completed_at, credits_used, request */
    "result": {
      /* profile, stats, hashtags, and more */
      "videos": [
        {
          "id": "7481234567890123456",
          "title": "This $200 phone embarrassed my flagship",
          "views": 2400000,
          /* ...more video fields */
          "intelligence_status": "ready",
          "intelligence": {
            "hook_text": "This phone costs two hundred dollars and it beat mine.",
            "hook_type": "bold_claim",
            "visual_hook_type": "extreme_closeup",
            "content_format": "review",
            "visual_format": "talking_head",
            "is_sponsored": false,
            "brands_mentioned": ["Samsung"],
            "sentiment": "positive",
            "brand_safety_tier": "safe",
            "language_detected": "en"
            /* 43 fields in total */
          }
        }
      ]
    },
    "analysis": {
      "key_insight": "Bold price claims on budget phones drive his biggest videos, at about 6x his median views.",
      "what_works": [
        {
          "insight": "Bold-claim hooks on cheap phones average 6x the median views.",
          "evidence_video_ids": ["7481234567890123456"]
        }
      ],
      "best_formats": "Talking-head reviews beat demos, with a median of 1.2M views across 18 posts.",
      "winning_hooks": ["Price shock in the first line", "Head-to-head verdict teased up front"],
      "top_subjects": ["Budget phones", "Camera comparisons"],
      "relevance": "For hooks: open on a surprising price or a verdict.",
      "degraded": false
    },
    "intelligence_status": "ready",
    "error": null
  }
}
```

Using Virlo inside an AI assistant ([MCP](https://dev.virlo.ai/docs/mcp))? Ask it to look up the creator with Data Intelligence, then read the saved run a few minutes later.

**Full field reference**

**On the run**

- `intelligence_status` (string | null, optional): `pending`, `ready`, `failed`, or `skipped` (no posts to analyze). `null`
  on runs without Data Intelligence.
- `analysis` (object | null, optional): `key_insight`, `what_works` (3 to 5, each an `insight` with
  `evidence_video_ids`), `best_formats`, `winning_hooks` (2 to 4) and
  `top_subjects`. `relevance` appears only if you sent `context`.
  `degraded` is `true` when fewer than 60% of videos had their AI read
  ready, so treat the summary as rough.

**On each video** (in `result.videos`, `result.outliers.outlier_videos` and `/runs/:run_id/videos`)

- `intelligence` (object | null, optional): 43 fields for a video, 35 for a photo carousel, named as in the agent
  [field reference](https://dev.virlo.ai/docs/intelligence#field-reference) and [slideshow
  fields](https://dev.virlo.ai/docs/intelligence#slideshow-intelligence). On-screen people and
  AI-use fields aren't included.
- `intelligence_status` (string, optional): `ready`, `pending`, `failed` (the run's AI step failed), `skipped` (this
  post couldn't be read) or `disabled` (no Data Intelligence on this run).
  A post Virlo already analyzed elsewhere can still show `ready`.
- `slides` (object[], optional): Photo carousels only: `position` and `image_url` per slide. `image_url`
  is a full link, usually under
  `https://auth.virlo.ai/storage/v1/object/public/slideshow-images/`. Many
  slides are HEIC (`.heic`), which most browsers other than Safari can't
  show, so convert them before display. The slide words are in
  `intelligence.panel_texts`.

---

## Repeat lookups

Each paid lookup is saved as its own run. Looking a creator up again on the same platform (handle case doesn't matter):

- **Within 6 hours: free, if the earlier lookup covers this one.** It needs every add-on you ask for, at least as many videos (`max_videos`), and the same `outlier_threshold`. You get the earlier result with `cached: true`. Asking for more, like trends after a plain lookup, runs a new paid lookup.
- **After 6 hours: a new paid lookup with a new run ID.** Earlier runs stay as they were, trends and Data Intelligence included, even if the new lookup fails.

Turning on every add-on the first time saves a second paid lookup.

---

## Re-read past runs

Reading a saved run is free. The run ID is the job ID.

- `GET /v1/satellite/runs/:run_id`: the full saved result.
- `GET /v1/satellite/runs?type=creator_lookup`: your creator lookups, newest first, one row per paid lookup. Takes `limit` (default 25, max 100), `offset` and `platform`.
- `GET /v1/satellite/runs/:run_id/videos`: videos a page at a time, for runs with hundreds. Takes `limit` (default 25, max 200), `offset`, `sort` (`recent` or `popular`) and `content_type` (`all`, `video` or `slideshow`).

On TikTok, `video` also returns photo carousels, and `slideshow` finds them only on runs with Data Intelligence.

The status endpoint stops answering about 24 hours after a lookup finishes; the saved run keeps working. Full reference: [Re-read past runs](https://dev.virlo.ai/docs/satellite#durable-runs).

---

More in Deep-dive lookups:

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