# Virlo API documentation (full text) > Virlo is a short-form video intelligence API for TikTok, YouTube Shorts, and Instagram Reels: research a niche with Content Research Agents, look up creators, sounds, and hashtags, track creators and videos over time, and read daily trends, sounds, hooks, hashtags, and top videos. This file contains every page of https://dev.virlo.ai/docs as Markdown, in sidebar order. Each request is shown once, as cURL; each page's own Markdown copy (add .md to its URL, for example https://dev.virlo.ai/docs/quickstart.md) also has the JavaScript and Python versions. The page index is https://dev.virlo.ai/llms.txt. ## 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. 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`. - MCP server for AI assistants: https://dev.virlo.ai/api/mcp/mcp. OpenAPI spec: https://api.virlo.ai/openapi.json. ## Contents - [API Documentation](https://dev.virlo.ai/docs.md) - [Quickstart](https://dev.virlo.ai/docs/quickstart.md) - [Authentication](https://dev.virlo.ai/docs/authentication.md) - [Billing and pricing](https://dev.virlo.ai/docs/credits.md) - [Glossary](https://dev.virlo.ai/docs/glossary.md) - [Content Research Agents](https://dev.virlo.ai/docs/agents.md) - [Autopilot](https://dev.virlo.ai/docs/agents/autopilot.md) - [Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook.md) - [Data Intelligence](https://dev.virlo.ai/docs/intelligence.md) - [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) - [Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags.md) - [Track creators and videos](https://dev.virlo.ai/docs/tracking.md) - [Trends](https://dev.virlo.ai/docs/trends.md) - [Sounds](https://dev.virlo.ai/docs/sounds.md) - [Hooks](https://dev.virlo.ai/docs/hooks.md) - [Hashtags](https://dev.virlo.ai/docs/hashtags.md) - [Videos](https://dev.virlo.ai/docs/videos.md) - [YouTube hashtags](https://dev.virlo.ai/docs/youtube-hashtags.md) - [YouTube videos](https://dev.virlo.ai/docs/youtube-videos.md) - [TikTok hashtags](https://dev.virlo.ai/docs/tiktok-hashtags.md) - [TikTok videos](https://dev.virlo.ai/docs/tiktok-videos.md) - [Instagram hashtags](https://dev.virlo.ai/docs/instagram-hashtags.md) - [Instagram videos](https://dev.virlo.ai/docs/instagram-videos.md) - [How results load](https://dev.virlo.ai/docs/async-data.md) - [Pagination](https://dev.virlo.ai/docs/pagination.md) - [Rate limits](https://dev.virlo.ai/docs/rate-limits.md) - [Webhooks](https://dev.virlo.ai/docs/webhooks.md) - [Errors](https://dev.virlo.ai/docs/errors.md) - [Workflow recipes](https://dev.virlo.ai/docs/recipes.md) - [API playground](https://dev.virlo.ai/docs/playground.md) - [MCP server](https://dev.virlo.ai/docs/mcp.md) - [Connect an AI assistant](https://dev.virlo.ai/docs/ai-agents.md) - [Research playbook](https://dev.virlo.ai/docs/agent-playbook.md) - [Orbit (deprecated)](https://dev.virlo.ai/docs/orbit.md) - [Comet (deprecated)](https://dev.virlo.ai/docs/comet.md) --- # API Documentation > Research niches, look up creators, sounds, and hashtags, and follow trends on TikTok, YouTube Shorts, and Instagram Reels through the Virlo API. Source: https://dev.virlo.ai/docs See what's working on TikTok, YouTube Shorts, and Instagram Reels from your software or AI assistant. New to APIs? Read the [Glossary](https://dev.virlo.ai/docs/glossary). With the Virlo API you can research a niche, look up any creator, sound, or hashtag, and see what's trending. You send each request with your API key (it works like a password), and you get back data as JSON, not a finished report, ready for your code or AI assistant to use. There's no subscription. You add funds to a prepaid balance, and most requests cost $0.50 or less. Errors are free, and failed creator, sound, and hashtag lookups are refunded. A one-time agent whose run fails is still charged, as [Billing and pricing](https://dev.virlo.ai/docs/credits#how-it-works) explains. Most requests answer in seconds, and lookups take up to a few minutes. Agents take longer: 9 in 10 finish collecting in under 20 minutes, and the AI report follows a few minutes after that. [Quickstart](https://dev.virlo.ai/docs/quickstart) [Content Research Agents](https://dev.virlo.ai/docs/agents) [API playground](https://dev.virlo.ai/docs/playground) ## Before you start 1. [Sign up](https://dev.virlo.ai/signup) and [add funds](https://dev.virlo.ai/dashboard/billing). Keys need funds. 2. [Generate an API key](https://dev.virlo.ai/dashboard/api-keys) and copy it: it's shown once. 3. Follow the [Quickstart](https://dev.virlo.ai/docs/quickstart). ## Find what you need | You want to | Go to | | - | - | | Analyze a niche's videos, once or on a schedule | [Content Research Agents](https://dev.virlo.ai/docs/agents) | | Profile a creator and their go-to formats | [Creator lookup](https://dev.virlo.ai/docs/satellite), [add-ons](https://dev.virlo.ai/docs/satellite/creators) | | Check if a video beat its creator's usual views | [Video outlier check](https://dev.virlo.ai/docs/satellite/video-outlier) | | Follow creators or videos over time | [Tracking](https://dev.virlo.ai/docs/tracking) | | See today's trends | [Trends](https://dev.virlo.ai/docs/trends) | | Find trending sounds, or dig into one | [Sounds](https://dev.virlo.ai/docs/sounds), [Sound lookup](https://dev.virlo.ai/docs/satellite/sounds) | | Rank hashtags, or dig into one | [Hashtags](https://dev.virlo.ai/docs/hashtags), [Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags) | | Find opening lines (hooks) that work | [Hooks](https://dev.virlo.ai/docs/hooks) | | Get the last 48 hours' top videos | [Videos](https://dev.virlo.ai/docs/videos), [YouTube](https://dev.virlo.ai/docs/youtube-videos), [TikTok](https://dev.virlo.ai/docs/tiktok-videos), [Instagram](https://dev.virlo.ai/docs/instagram-videos) | | Get alerts when runs, lookups, or trends are ready, or tracked items change | [Webhooks](https://dev.virlo.ai/docs/webhooks) | | Use Virlo from Claude, Cursor, or another AI assistant | [MCP server](https://dev.virlo.ai/docs/mcp) | ## Good to know - Requests go to `https://api.virlo.ai/v1` with your key in the [`Authorization` header](https://dev.virlo.ai/docs/authentication). - Slow jobs return a job ID to [check](https://dev.virlo.ai/docs/async-data) every 15 seconds, or whatever `retry_after_seconds` says. - The `X-Cost` header shows each call's [price](https://dev.virlo.ai/docs/credits) in dollars. Some fields count credits: 1 credit = $0.01. **Details for developers** Results sit in `data`, except on `/v1/webhooks`. Fields are snake_case, except `statusCode` in errors. JavaScript examples use top-level `await`, so save them as `.mjs` files. Still on `/v1/orbit` or `/v1/comet`? They are deprecated. [Switch to agents](https://dev.virlo.ai/docs/orbit). --- # Quickstart > Get an API key, check it works with a free request, then pull today's trends for $0.25. Source: https://dev.virlo.ai/docs/quickstart Check your API key with a free request, then pull today's top trends. **At a glance** - **What it does:** Gets you from sign-up to your first results. - **You send:** Two requests with your API key. - **You get back:** Your balance, then today's top trends. - **Cost:** $0.25 total. The balance check is free. ## Before you start Someone gave you a key? Skip steps 1 to 3. 1. **Create an account** at [dev.virlo.ai/signup](https://dev.virlo.ai/signup). 2. **Add funds** on the [Billing page](https://dev.virlo.ai/dashboard/billing). You need a balance to generate a key. 3. Click **Generate Key** on the [API keys](https://dev.virlo.ai/dashboard/api-keys) page and copy the key. It is shown only once. Paste your whole key over `YOUR_API_KEY`; it already starts with `virlo_tkn_`. Mac: cURL tab in Terminal. Windows: the **Windows** tab here, in PowerShell. For cURL examples on other pages, use Git Bash. Rather not use a terminal? Once you have a key, pick **Trends Digest** in the [dashboard playground](https://dev.virlo.ai/dashboard/playground). Same request, same $0.25. ## Check your balance This free call tests your key. If you see your balance, it works. **cURL request:** ```bash curl https://api.virlo.ai/v1/account/balance \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 It worked:** ```json { "data": { "balance": "$25.00", "credits_remaining": 2500, "status": "active" } } ``` **Response 401 Bad key:** ```json { "message": "Invalid or inactive API key", "error": "Unauthorized", "statusCode": 401, "code": "invalid_api_key" } ``` `balance` is in dollars; `credits_remaining` is the same in credits (1 credit = $0.01). A `401` means the key is missing, mistyped, or revoked: see [Authentication](https://dev.virlo.ai/docs/authentication#when-a-key-is-rejected). ## Get today's trends Returns today's top worldwide trends, ranked, in about a second. **It costs $0.25 each time.** Under $0.25 left? You get an `insufficient_credits` error. **cURL request:** ```bash curl https://api.virlo.ai/v1/trends/digest \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 It worked:** ```json { "data": [ { "id": "0a3dd8f0-7bb2-4de0-a7e2-74eb82d851ae", "title": "Trends for Sep 24", "region": "global", "local_date": "2026-09-24", "trends": [ { "ranking": 1, "trend": { "id": "503b791b-663a-4ae0-bccc-6d3329707f1e", "name": "Meta's new VR glasses lineup (Ray-Ban Audio + no camera + 5K micro OLED visuals) & Meta Connect 2026 reveals", "description": "On September 23, 2026, Meta unveiled its latest VR wearables at the Meta Connect conference, including the Ray-Ban Meta Audio glasses, which lack a camera and focus on audio capabilities, and the advanced Meta VR Glasses featuring 5K micro-OLED visuals." }, "momentum": { "status": "new", "views_per_hour": 131006 }, "top_exemplars": [ { "url": "https://www.youtube.com/shorts/p4BUXLcRvKk", "platform": "youtube", "views": 3660687, "author": { "username": "@actuallycarterpcs" } } ] } ] } ] } ``` - `data`: one daily list of 15 to 20 topics, up to 5 example videos each. - A new worldwide list starts at 7:00 New York time; trends are added at 13:00 and 19:00. Before 7:00 you get yesterday's, even if `title` says today; check `local_date`. - `momentum.status` is `new`, `rising`, `steady`, or `fading` (`momentum` is `null` until measured). - `top_exemplars` are sorted by all-time views, so older videos can appear. Add `?region=us` for one country (or `gb`, `au`, `sg`), or `?top_exemplars=0` for a shorter reply. See [Trends](https://dev.virlo.ai/docs/trends). ### See what you paid To see the charge, add `-i` after `curl`. Look for these (curl may print them in lowercase): **Response headers:** ```http X-Cost: 0.25 X-Credits-Used: 25 X-Balance-Remaining: 24.75 X-Credits-Remaining: 2475 ``` - `X-Cost` is the charge in dollars, with no `$`. Free calls show `0.00`. - `X-Balance-Remaining` is your balance afterward; the `Credits` headers repeat both in credits. - `Remaining` headers can appear on free calls too (never on the balance check), so only `X-Cost` shows what you paid. - Failed requests are never charged and carry none of these headers. ## What's next? - [Content Research Agents](https://dev.virlo.ai/docs/agents): research a niche once or on a schedule. - [Billing and pricing](https://dev.virlo.ai/docs/credits): what each request costs. - [Errors](https://dev.virlo.ai/docs/errors): what each error `code` means. - [MCP server](https://dev.virlo.ai/docs/mcp): use Virlo from Claude, Cursor, or VS Code. - [Research playbook](https://dev.virlo.ai/docs/agent-playbook): which workflow fits which question. --- # Authentication > How to get a Virlo API key, send it with each request, and fix a rejected key. Source: https://dev.virlo.ai/docs/authentication Your API key tells Virlo who is calling and which balance to charge. **At a glance** - **What it does:** Proves each request comes from your team. - **You send:** `Authorization: Bearer YOUR_API_KEY` on every request. - **You get back:** Your data, or `401` (key rejected). - **Cost:** Free. Rejected requests are never charged. ## Get an API key [Add funds](https://dev.virlo.ai/dashboard/billing), then click **Generate Key** on the [API keys](https://dev.virlo.ai/dashboard/api-keys) page. Copy the key now: it's shown only once. - Keys start with `virlo_tkn_`. Each team shares up to 10 active keys. - In the dashboard, only team owners and admins can generate or revoke keys. ## Who can see what - **The whole team:** the balance, [lookups](https://dev.virlo.ai/docs/satellite), [tracked](https://dev.virlo.ai/docs/tracking) creators and videos, and [webhooks](https://dev.virlo.ai/docs/webhooks). - **One person:** [Content Research Agents](https://dev.virlo.ai/docs/agents) are private to the person whose key created them. Teammates' keys get `404` (not found). ## Send your key Run this free check in a terminal, replacing `YOUR_API_KEY` with your whole key: **cURL request:** ```bash curl https://api.virlo.ai/v1/account/balance \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "balance": "$25.00", "credits_remaining": 2500, "status": "active" } } ``` **Response 401:** ```json { "message": "Invalid or inactive API key", "error": "Unauthorized", "statusCode": 401, "code": "invalid_api_key" } ``` ## When a key is rejected The `401` message tells you why: - `API key is required` (`missing_api_key`): no key in the `Authorization` header. `x-api-key` and `?api_key=` aren't accepted. - `Invalid API key format` (`invalid_api_key`): the key doesn't look like `virlo_tkn_...`. Usually `bearer` instead of `Bearer`, an extra space, or an empty key. - `Invalid or inactive API key` (`invalid_api_key`): the key is mistyped, revoked, or has `virlo_tkn_` pasted twice. Rapid retries from one computer with a missing or malformed key get briefly blocked (`429`, `too_many_requests`). Fix the key first. ## Keep your key safe - Anyone with your key can spend your balance. Keep it out of chats, shared docs, web page code, and Git. - If a key leaks, revoke it on the [API keys](https://dev.virlo.ai/dashboard/api-keys) page and generate a new one. Revoking cuts off everyone on your team using that key. ## Using the MCP server AI assistants like Claude can use Virlo, paid from your team's balance. - **Claude Desktop, claude.ai, Claude Code:** anyone who signs in gets a new team key (needs funds and under 10 active keys). - **Cursor, VS Code, Windsurf, Codex:** paste an API key. [Set up the MCP server](https://dev.virlo.ai/docs/mcp) --- # Billing and pricing > How Virlo API billing works: a prepaid dollar balance, pay per successful request, auto top-up, no subscriptions, and a free endpoint to check your balance. Source: https://dev.virlo.ai/docs/credits You pay from a prepaid dollar balance your team shares. No subscription, and the balance never expires. **At a glance** - **What it does:** Explains how you pay for the API. - **You send:** Nothing extra. Paid requests take their price from your balance. - **You get back:** Each successful reply shows its dollar price (`X-Cost` header). - **Cost:** Checking your balance and re-reading results you paid for are free. ## How it works 1. Add funds on the [Billing page](https://dev.virlo.ai/dashboard/billing), which shows the minimum. 2. You pay for each accepted request, even if it finds nothing. Rejected requests (errors) are free, and so is a request whose reply never reaches you (it timed out or your connection closed first). A one-time agent is charged even if its run fails. A creator, sound, or hashtag lookup that fails (for example, the handle doesn't exist) is refunded later, as is a post collection that finds nothing. 3. Optional: turn on **auto top-up** in [Settings](https://dev.virlo.ai/dashboard/settings) to refill from your card when you run low. ## Scheduled charges - **Recurring agents:** free to create, then $0.50 per run ($1.50 with [Data Intelligence](https://dev.virlo.ai/docs/credits#data-intelligence-add-ons)). The first run starts right away, so your balance must cover it. - **[Tracking](https://dev.virlo.ai/docs/tracking):** $0.25 to start (covers the first check), then $0.25 per check. Asking for a creator's older posts (`collection_depth`) adds a one-time $0.50, $1.00, or $2.00. Runs and later checks never show up in `X-Cost`. See each one on the [Usage](https://dev.virlo.ai/dashboard/usage) page, or [check your balance](https://dev.virlo.ai/docs/credits#check-your-balance) for free. If your balance runs out, these pause, and adding funds won't restart them. To resume, set `active: true` with [Update agent](https://dev.virlo.ai/docs/agents#update-agent), or `status: active` with [Update tracking](https://dev.virlo.ai/docs/tracking#update-tracked-creator). ## Check your balance **Endpoint:** `GET https://api.virlo.ai/v1/account/balance` Returns your team's balance. Free. **cURL request:** ```bash curl https://api.virlo.ai/v1/account/balance \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 OK:** ```json { "data": { "balance": "$259.25", "credits_remaining": 25925, "status": "active" } } ``` - `balance` (string, optional): Your balance in dollars. - `credits_remaining` (integer, optional): The same balance in credits (1 credit = $0.01). - `status` (string, optional): `active`, or `zero_balance` once it runs out. ## Prices | Task | Price | | - | - | | [Content Research Agent](https://dev.virlo.ai/docs/agents) run | $0.50, $1.50 with Data Intelligence | | [Creator lookup](https://dev.virlo.ai/docs/satellite) | $0.50, up to $1.75 with add-ons | | [Sound lookup](https://dev.virlo.ai/docs/satellite/sounds) | $0.50, up to $1.00 | | [Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags) | $0.50, up to $2.50 | | Trends, digests, trending sounds and hooks | $0.25 | | [Hashtag stats](https://dev.virlo.ai/docs/hashtags), [sound search](https://dev.virlo.ai/docs/sounds#search-sounds), [hook library](https://dev.virlo.ai/docs/hooks#hook-template-library) | $0.05 to $0.10 | Full list below. | Endpoint | Method | Price per request | What it does | | - | - | - | - | | `/v1/agents` | POST | $0.50 / run | Create a Content Research Agent. A one-time agent (is_recurring=false) is charged once, when you create it. A recurring agent (is_recurring=true) is free to create and charged after each run. Listing agents and reading results are free. | | `/v1/satellite/sounds/status/:job_id` | GET | Free | Check a sound lookup's status | | `/v1/satellite/runs/:run_id` | GET | Free | Re-read a saved lookup | | `/v1/satellite/runs` | GET | Free | List your saved lookups (runs) | | `/v1/tracking/creators/:id/posts/collect` | ALL | $0.50 to $2.00 | Collect a tracked creator's older posts (price depends on depth). Refunded if no posts are found. | | `/v1/tracking/creators/:id/posts/collect/:collection_id` | ALL | Free | Check a post collection's status | | `/v1/tracking/creators/:id/posting-cadence` | ALL | Free | How often a tracked creator posts | | `/v1/satellite/creators/batch` | ALL | $0.50 / creator | Batch creator lookup (up to 25 creators). A creator looked up in the last 6 hours with at least the same options comes back from cache, free. | | `/v1/satellite/creators/batch/:batch_id` | ALL | Free | Check a batch lookup's status | | `/v1/tracking/videos/:id/report` | ALL | Free | A tracked video's latest AI report | | `/v1/tracking/videos/:id` | ALL | Free | Get, update, or stop tracking a video | | `/v1/tracking/creators/:id/snapshots` | ALL | Free | A tracked creator's numbers at each check | | `/v1/tracking/creators/:id/report` | ALL | Free | A tracked creator's latest AI report | | `/v1/hooks/library/categories` | GET | Free | Hook template categories (free) | | `/v1/satellite/hashtags/status/:job_id` | GET | Free | Check a hashtag lookup's status | | `/v1/tracking/videos/:id/snapshots` | ALL | Free | A tracked video's numbers at each check | | `/v1/tracking/creators/:id/posts` | ALL | Free | A tracked creator's posts, with numbers for each | | `/v1/tracking/creators/:id/posts/:post_id` | ALL | Free | One post from a tracked creator | | `/v1/tracking/creators/:id` | ALL | Free | Get, update, or stop tracking a creator | | `/v1/hooks/types` | GET | Free | Which hook types win most often (free) | | `/v1/tracking/creators/:id/audience-demographics` | ALL | Free | A tracked creator's audience by age and gender | | `/v1/tracking/creators/:id/audience-geography` | ALL | Free | A tracked creator's audience by country | | `/v1/tracking/creators/:id/audience-refresh` | ALL | $0.50 | Make a new audience snapshot for a tracked creator. Free on a cache hit, or while a snapshot is already being built. | | `/v1/tracking/creators/:id/audience-refresh/:jobId` | ALL | Free | Check an audience snapshot's status | | `/v1/satellite/runs/:run_id/videos` | GET | Free | Page through a saved lookup's videos | | `/v1/instagram/hashtags` | ALL | $0.05 | Top Instagram Reels hashtags for a date range | | `/v1/sounds/:sound_id` | GET | $0.05 | One sound's details. Matching it to a song (resolve=true) adds $0.10 the first time. | | `/v1/sounds/:sound_id/usage-history` | GET | $0.05 | A sound's daily usage history | | `/v1/hashtags` | ALL | $0.05 | Top hashtags across all platforms for a date range | | `/v1/hashtags/:hashtag/performance` | ALL | $0.05 | One hashtag's video count and views | | `/v1/youtube/hashtags` | ALL | $0.05 | Top YouTube Shorts hashtags for a date range | | `/v1/tiktok/hashtags` | ALL | $0.05 | Top TikTok hashtags for a date range | | `/v1/sounds/search` | GET | $0.10 | Search sounds by title | | `/v1/hooks/library` | GET | $0.10 | Hook templates, with why each one works | | `/v1/tracking/videos` | POST | $0.25 | Start tracking a video. The price covers the first check, and each later check costs the same. | | `/v1/sounds/breakout` | GET | $0.25 | Sounds growing fastest from a small start | | `/v1/videos/digest` | ALL | $0.25 | Most-viewed videos from the last 48 hours, all platforms | | `/v1/sounds/by-creator/:platform/:handle` | GET | $0.25 | Sounds a creator owns | | `/v1/sounds/:sound_id/videos` | GET | $0.25 | Videos that use a sound | | `/v1/hooks/trending` | GET | $0.25 | Top hooks from recent posts | | `/v1/sounds/trending` | GET | $0.25 | Trending sounds | | `/v1/tracking/creators` | POST | $0.25 | Start tracking a creator. The price covers the first check, and each later check costs the same. | | `/v1/youtube/videos/digest` | ALL | $0.25 | Most-viewed YouTube Shorts from the last 48 hours | | `/v1/trends/emerging` | ALL | $0.25 | Trends that are new or rising right now | | `/v1/hooks/search` | GET | $0.25 | Search about 1.4 million hooks by phrase or type | | `/v1/agents/:id/hooks` | GET | $0.25 | Top hooks from an agent's videos. Free when the agent has Data Intelligence, or while it has no hooks yet. | | `/v1/tiktok/videos/digest` | ALL | $0.25 | Most-viewed TikToks from the last 48 hours | | `/v1/instagram/videos/digest` | ALL | $0.25 | Most-viewed Instagram Reels from the last 48 hours | | `/v1/trends` | ALL | $0.25 | Daily trend lists for a date range | | `/v1/trends/digest` | ALL | $0.25 | Today's trend list for a region | | `/v1/satellite/creator/:platform/:username` | ALL | $0.50 | Creator lookup (runs in the background). Refunded in full if the lookup fails. | | `/v1/satellite/video-outlier` | POST | $0.50 | Video outlier check (runs in the background) | | `/v1/satellite/hashtags/:platform/:hashtag` | GET | $0.50 | Hashtag lookup: stats, trends, and videos for one hashtag. Refunded in full if the lookup fails. | | `/v1/satellite/sounds/:platform/:music_id` | GET | $0.50 | Sound lookup (runs in the background). Refunded in full if the lookup fails. | **Optional add-ons (charged on top of the base price)** | Add-on | Cost | Applies to | | - | - | - | | Data Intelligence for agents | +$1.00 per run | data_intelligence_enabled=true on a Content Research Agent. An AI breakdown of every video (79 fields; slideshows get 70). One-time and recurring agents cost the same, and a recurring agent is charged once per run. It cannot be added to a run that already happened. | | Trend analysis | +$0.50 per lookup | trend_analysis=true on a creator, sound, or hashtag lookup. Reads up to about 300 videos and returns the patterns it finds in a trends block. Refunded when no trends come out (fewer than 10 videos, or the analysis step failed). | | Data Intelligence for creator lookups | +$0.25 per lookup | data_intelligence=true on a creator lookup. Adds each video's hook, content format, and visual style (43 fields; 35 for a photo carousel), plus a "what's working" summary. Read it on GET /v1/satellite/runs/:run_id. Refunded when there are no posts to enrich, the analysis fails, or the whole lookup fails. | | Hashtag depth: deep | +$0.50 per lookup | depth=deep (about 300 videos) on a hashtag lookup. Not charged when trend_analysis=true, which already collects that many. | | Hashtag depth: full | +$1.50 per lookup | depth=full (about 500 videos) on a hashtag lookup. | | Audience snapshot | +$0.50 per creator | audience_demographics or audience_geography on a creator lookup or batch lookup, or POST /v1/tracking/creators/:id/audience-refresh. Free when a snapshot from the last 30 days already exists or one is already being built. Refunded when the snapshot fails. | ## Data Intelligence add-ons An AI breakdown of each video: - **Agents:** `data_intelligence_enabled: true` adds $1.00 per run ([details](https://dev.virlo.ai/docs/intelligence)). On a recurring agent, [turning it on or off later](https://dev.virlo.ai/docs/agents#update-agent) affects only future runs. - **Creator lookups:** `data_intelligence=true` adds $0.25, so a lookup costs $0.75. You get each video's hook, content format, and visual style, plus a summary of what's working. ## Response headers Only successful responses carry these. Amounts have no `$` sign. - `X-Cost`: the exact charge in dollars (`0.00` if free). `X-Credits-Used` gives it in credits. - `X-Balance-Remaining`: your balance afterward, on endpoints that can charge, even when this call was free. Only `X-Cost` shows your charge. `X-Credits-Remaining` gives the balance in credits. ## Insufficient balance If your balance can't cover a request, you get a `402` and aren't charged. Messages vary, so read `code`, `required_credits` (price), and `remaining_credits` (balance), both in cents. **Most endpoints:** ```json { "statusCode": 402, "message": "Insufficient balance. $0.25 required, $0.12 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 25, "remaining_credits": 12, "required_amount": "$0.25", "remaining_balance": "$0.12", "code": "insufficient_credits" } ``` **Creating an agent:** ```json { "statusCode": 402, "code": "insufficient_credits", "message": "Insufficient credits", "error": "Payment Required", "required_credits": 50, "remaining_credits": 12 } ``` On auto top-up? The card charge may have failed, so check [Billing](https://dev.virlo.ai/dashboard/billing). A `429` means too many requests, not low funds ([Rate limits](https://dev.virlo.ai/docs/rate-limits)). --- # Glossary > Plain definitions of the words used in the Virlo API docs, including three that mean different things on different pages: outlier, weighted_score, and run. Source: https://dev.virlo.ai/docs/glossary Virlo terms in plain words. [Three](https://dev.virlo.ai/docs/glossary#words-that-change-meaning) change meaning between pages. ## Research - **[Content Research Agent](https://dev.virlo.ai/docs/agents)**: collects short-form videos on your topic from TikTok, YouTube, and Instagram, then reports what's working. - **[One-time or recurring](https://dev.virlo.ai/docs/agents#create-agent)** (`is_recurring`): a one-time agent runs once, for $0.50 at creation. A recurring agent costs $0.50 per run. - **Cadence**: how often a recurring agent runs: `daily`, `weekly`, `monthly`, or a cron schedule up to once a day. Tracking uses `scrape_cadence`. - **[Data Intelligence](https://dev.virlo.ai/docs/intelligence)**: an AI breakdown of the videos worth studying, like their hook, format, and tone. Off-topic, year-old and underperforming videos are skipped. It adds $1.00 per agent run, or $0.25 per creator lookup. ## Lookups - **[Lookup](https://dev.virlo.ai/docs/satellite)**: a one-off deep dive on a creator, sound, hashtag, or video, for $0.50 to $2.50. Paths say `satellite`, the API's name for lookups. ## Tracking - **[Check](https://dev.virlo.ai/docs/tracking#how-it-works)** (also _cycle_ or _scrape_): one $0.25 round where Virlo fetches a tracked creator's or video's latest numbers and writes an AI report. - **[Snapshot](https://dev.virlo.ai/docs/tracking#get-creator-snapshots)**: the numbers saved at one check. An _audience snapshot_ is different: a $0.50 profile of a creator's commenters. ## Billing - **[Credit](https://dev.virlo.ai/docs/credits#response-headers)**: 1 credit = $0.01. Some headers and fields count in credits: `X-Credits-Used: 50` equals `X-Cost: 0.50`. ## Technical - **[Job ID](https://dev.virlo.ai/docs/async-data#per-resource)**: the ID you get right away when you start background work (a lookup's `job_id`, an agent's `id`), used to check on it. - **[Polling](https://dev.virlo.ai/docs/async-data#polling-pattern)**: checking every 15 seconds, or whatever `retry_after_seconds` says, until the job is done. It's free. - **[Finalized](https://dev.virlo.ai/docs/async-data#finalized)**: `finalized: true` means the results are ready, including follow-ups like an agent's AI report. `status: "completed"` alone isn't enough. - **[Webhook](https://dev.virlo.ai/docs/webhooks)**: a message Virlo sends to your web address when something happens, like an agent run finishing, so you don't have to poll. - **[Response envelope](https://dev.virlo.ai/docs/errors)**: successful responses wrap results in `{"data": ...}`. Errors, and calls under `/v1/webhooks`, aren't wrapped. ## Words that change meaning **Outlier**: far more views than expected, and "expected" differs by page. The _median_ is a creator's typical views. - [Content Research Agents](https://dev.virlo.ai/docs/agents#get-agent-outliers) and [hooks](https://dev.virlo.ai/docs/hooks): `outlier_ratio` is views ÷ followers. Hooks use one video's views, creator outliers a creator's average. - [Creator lookup](https://dev.virlo.ai/docs/satellite#start-creator-lookup): a video over `outlier_threshold` times the median (default `2`). - [Video outlier check](https://dev.virlo.ai/docs/satellite/video-outlier): `outlier_score` is views ÷ median. `3` or more is `viral`, `10` or more `mega_viral`. - [Tracking](https://dev.virlo.ai/docs/tracking#get-creator-signals) and [webhooks](https://dev.virlo.ai/docs/webhooks#tracking-payload): a video over 3 times the median (a breakout video). **`weighted_score`**: two different scores share this name, so never compare them. - On Content Research Agents and hooks, it's the **Virality Score**: how far views beat followers, with extra credit for big accounts. 18 or more is strong, 35 or more exceptional. - On lookups, tracking, and webhooks, it ranks outlier videos, giving big creators more credit. **For developers: the formulas** Virality Score: ln(views ÷ followers) × ln(followers). Lookups and tracking: ln(views ÷ median) × ln(median). **Run**: on [Content Research Agents](https://dev.virlo.ai/docs/agents#list-agent-runs), one round of collecting videos. On [lookups](https://dev.virlo.ai/docs/satellite#durable-runs), a saved result, free to re-read any time. Each paid lookup makes a new run. Its `run_id` is the `job_id`. --- # Content Research Agents > Research a niche once or on a schedule. One call creates the agent. After it runs, you read its videos, creators, trends, and analysis, almost all of it for free. Source: https://dev.virlo.ai/docs/agents Want to know what is working in a niche on TikTok, YouTube, and Instagram? A **Content Research Agent** collects the videos and reports the trends, standout creators, sounds, and hashtags, once or on a schedule. **At a glance** - **What it does:** Searches the platforms for your topic, keeps the videos that fit, and reports what is working and why. - **You send:** Your API key ([Quickstart](https://dev.virlo.ai/docs/quickstart)), a one-sentence `intent`, some keywords, and whether it runs once or on a schedule. - **You get back:** An agent `id` right away. Once the run is done, you read its videos, creators, trends, and report, all free except [hooks](https://dev.virlo.ai/docs/agents#get-agent-hooks). - **Cost:** $0.50 per run, $1.50 with [Data Intelligence](https://dev.virlo.ai/docs/intelligence) (AI video breakdowns). One-time agents pay at creation, recurring ones after each run. - **How long:** Half of runs finish collecting in under 8 minutes, and 9 in 10 in under 20. The AI report follows within a few minutes. --- ## How an agent works 1. **Get keywords (free).** Send your `intent`, one sentence about what you want, to [`POST /v1/agents/suggest-keywords`](https://dev.virlo.ai/docs/agents#suggest-keywords). 2. **Create the agent.** Send that `intent`, the `keywords`, and any `exclude_keywords` you agree with to [`POST /v1/agents`](https://dev.virlo.ai/docs/agents#create-agent). You get its `id` at once. 3. **Wait** until [`GET /v1/agents/:id`](https://dev.virlo.ai/docs/agents#get-agent) shows `finalized: true`. Check every 15 seconds, or whatever `retry_after_seconds` says. This polling is free. Or use the [`content_research_agent.run.completed`](https://dev.virlo.ai/docs/webhooks#supported-events) webhook. 4. **Read the results.** Start with the [summary](https://dev.virlo.ai/docs/agents#get-agent-summary), then [videos](https://dev.virlo.ai/docs/agents#get-agent-videos), [creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers) (ready about a minute after `finalized`), and [trends](https://dev.virlo.ai/docs/agents#get-agent-trends). **Virality Score** (`weighted_score`, on creator and hook rows) shows how far views outran follower count, with extra credit for bigger accounts ([formula](https://dev.virlo.ai/docs/agent-playbook#spotting-the-most-viral)). 35 and up is exceptional, 25 to 35 very strong, 18 to 25 strong, 10 to 18 promising. --- ## Which call answers my question? | Your question | Call | | - | - | | What's working? | [Get summary](https://dev.virlo.ai/docs/agents#get-agent-summary) | | Which videos did best? | [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos) with `order_by=views` | | Why is it working? | [Get latest trends](https://dev.virlo.ai/docs/agents#get-agent-trends), [Get latest analysis](https://dev.virlo.ai/docs/agents#get-agent-analysis) | | Which creators should we work with? | [Get creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers), then [similar creators](https://dev.virlo.ai/docs/agents#get-agent-similar-creators) and [benchmarks](https://dev.virlo.ai/docs/agents#get-agent-benchmarks) | | Which sounds, hashtags, and hooks? | [Get sounds](https://dev.virlo.ai/docs/agents#get-agent-sounds), [Get hashtags](https://dev.virlo.ai/docs/agents#get-agent-hashtags), [Get hooks](https://dev.virlo.ai/docs/agents#get-agent-hooks) | | Why so few videos? | [List runs](https://dev.virlo.ai/docs/agents#list-agent-runs) | | What did autopilot change? | [Get activity](https://dev.virlo.ai/docs/agents/autopilot#get-agent-activity) | --- ## Writing a good intent Keywords decide where the agent looks. The `intent` decides which videos it keeps. Write **one concrete sentence**, about 40 to 250 characters (500 at most): ``` [Find/Monitor] [content type] about [niche] for [use case], [not / exclude X]. ``` Good: `"Find beginner skincare routines that name drugstore products, not dermatologist lectures."` Weak: bare topics (`"skincare, beauty"`) or vague wishes (`"I want to find viral video"`). More real examples: [Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook). --- ## Suggest keywords **Endpoint:** `POST https://api.virlo.ai/v1/agents/suggest-keywords` Turns your `intent` into 7 to 12 graded keywords plus words to exclude, ready for [Create agent](https://dev.virlo.ai/docs/agents#create-agent). Free, and it creates nothing. - `intent` (string, required): One sentence, up to 500 characters. Reuse it for Create agent. - `topic_hint` (string, optional): A short topic name to steer the result. - `platforms` (string[], optional): `youtube`, `tiktok`, `instagram`. Not `meta_ads`. - `mode` (string, optional): `create` (default) builds a fresh list, `refresh` replaces keywords that stopped finding much, `opportunity` suggests new angles. Apply the result with [Update agent](https://dev.virlo.ai/docs/agents#update-agent). - `existing_keywords` (string[], optional): Current keywords, for `refresh` or `opportunity`. - `desired_count` (integer, optional): Accepts 1 to 50, but you always get 7 to 12. - `use_web_grounding` (boolean, optional): Check the web for current phrasing, for news-driven topics. `quality.passes: true` means a `quality.score` of 65 or more and no `critical` issue. It grades the keyword list, not your intent: a vague intent like `"coffee"` can still score 100. **Full field reference** `quality.issues` items have `code`, `severity` (`critical`, `warning`, or `info`), `message`, and often `offenders`. Codes: `too_few_keywords`, `too_many_keywords`, `single_word_keywords`, `overly_long_keywords`, `duplicate_keywords`, `low_cluster_cohesion`, `empty_set`. A low `quality.stats.core_token_coverage` means a scattered list and more off-topic videos. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/agents/suggest-keywords \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "intent": "Track viral protein-recipe content for a fitness brand", "topic_hint": "Protein Recipes", "platforms": ["tiktok", "instagram"], "desired_count": 7 }' ``` **Response 200:** ```json { "data": { "keywords": [ "protein recipes", "high protein meals", "healthy protein recipes", "easy protein recipes", "protein snack ideas", "fitness protein recipes", "muscle building recipes" ], "exclude_keywords": ["powder", "shake", "supplement", "bar", "diet", "vegan", "vegetarian"], "reasoning": "This set focuses on 'protein recipes' as the core, with variations covering different meal types and fitness goals.", "quality": { "score": 100, "passes": true, "issues": [], "stats": { "count": 7, "avg_words_per_keyword": 2.86, "single_word_count": 0, "long_keyword_count": 0, "duplicate_count": 0, "core_token_coverage": 0.86, "core_token": "protein" } }, "timely_context_used": false } } ``` --- ## Create agent **Endpoint:** `POST https://api.virlo.ai/v1/agents` Creates an agent and starts its first run right away. **Cost:** $0.50 per run, or $1.50 with Data Intelligence. - **One-time** (the first example): charged at creation, so `X-Cost` shows `0.50` (or `1.50`). No refund if the run fails. - **Recurring:** free to create (the `X-Cost` response header says `0.00`). Each run, the first starting right away, is charged when it finishes. Charges show on your [Usage page](https://dev.virlo.ai/dashboard/usage), not in a header. Failed runs are free; `partial_failure` runs (one keyword or platform failed) cost full price. Runs repeat until you pause ([Update agent](https://dev.virlo.ai/docs/agents#update-agent), `active: false`) or [delete](https://dev.virlo.ai/docs/agents#delete-agent) the agent. - Your [balance](https://dev.virlo.ai/dashboard/billing) must cover the first run, or you get `402` (`insufficient_credits`). - `is_recurring` (boolean, required): `false` runs once, `true` repeats on `cadence`. Can't be changed later. The string `"false"` is rejected. - `intent` (string, required): Up to 500 characters. See [Writing a good intent](https://dev.virlo.ai/docs/agents#writing-a-good-intent). - `keywords` (string[], required): 1 to 50 search phrases; 7 to 12 multi-word phrases work best. A leading `#` is dropped (`#latteart` searches `latteart`), so write `latte art` for the phrase. The agent may search refined versions, shown in `intent_keywords`. - `cadence` (string, optional): Required when recurring: `"daily"`, `"weekly"`, `"monthly"`, or a cron expression that runs at most once a day. On a one-time agent it is ignored and comes back `null`. - `platforms` (string[], optional): `youtube`, `tiktok`, `instagram`. Defaults to all three. - `name` (string, optional): Your label, or `null` if you leave it out. - `exclude_keywords` (string[], optional): Up to 100 single words, saved in lowercase, for the _other_ meaning of your topic (for Apple the company: `fruit`, `recipe`). A video is dropped when one appears as a whole word in its caption or hashtags. Send none and the first run picks and saves some from your `intent`. - `exclude_keywords_strict` (boolean, optional): Also check excludes against the spoken transcript. - `meta_ads_enabled` (boolean, optional): Also collect matching Meta Ad Library ads, at no extra cost. - `data_intelligence_enabled` (boolean, optional): AI breakdowns of the videos, $1.00 more per run. Covers only runs after you turn it on, and only the videos worth studying: off-topic, year-old and underperforming ones are skipped. Recurring agents can switch it later. [Details](https://dev.virlo.ai/docs/intelligence). - `english_only` (boolean, optional): Default `true`. For another language, set `false` and write the `intent` and `keywords` in that language. - `autopilot` (boolean, optional): Default `true`. After each run, a recurring agent adds and rewords keywords to keep finding new videos. It never removes yours and never adds charges. `false` keeps the setup exactly as you send it. It has no effect on one-time agents. [Details](https://dev.virlo.ai/docs/agents/autopilot). > **Note:** Filter by views or date when you [read the videos](https://dev.virlo.ai/docs/agents#get-agent-videos), not here: `min_views`, `time_range`, or any unlisted field returns a free [`400` error](https://dev.virlo.ai/docs/errors). **Details for developers** **Schedules** run in UTC. `daily` is `0 0 * * *`, `weekly` is `0 0 * * 0`, and `monthly` is `0 0 1 * *`, the cron form the response shows. These three, as shortcuts or written out, give each agent its own fixed start time spread across the period: the same time every day for `daily`, the same day and time every week for `weekly`, and a fixed time in the first 7 days of the month for `monthly`. `next_run_at` shows when. Any other cron runs at the time you set. A run starts within about 30 minutes of `next_run_at`. **Errors** come back before anything is charged. Each `400` has `code: "validation_error"`. Its `message` is usually a list, but some checks, such as `intent` length, return a string. **One-time request:** ```bash # $0.50 charged now (X-Cost: 0.50). Runs once. # Shortened to 3 keywords. Pass the 7 to 12 from suggest-keywords. curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": false, "intent": "Track viral protein-recipe content for a fitness brand", "keywords": ["high protein recipe", "protein meal prep", "protein snack ideas"], "platforms": ["youtube", "tiktok", "instagram"], "name": "Protein Recipes" }' ``` **Response 201 One-time:** ```json { "data": { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "name": "Protein Recipes", "is_recurring": false, "intent_keywords": null, "cadence": null, "next_run_at": null, "last_run_at": null, "job_id": "c29bfbf3-4fa6-470b-80d3-53364f916ea8", "latest_run": { "id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90", "status": "pending" } }, "message": "Agent created" } ``` **Response 201 Recurring:** ```json { "data": { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "name": "Protein Recipes", "is_recurring": true, "intent_keywords": null, "cadence": "0 0 * * 0", "next_run_at": "2026-09-27T02:14:08.000Z", "last_run_at": null, "job_id": "c29bfbf3-4fa6-470b-80d3-53364f916ea8", "latest_run": { "id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90", "status": "pending" } }, "message": "Agent created" } ``` **Response 400 No cadence:** ```json { "message": [ "cadence must be \"daily\", \"weekly\", \"monthly\", or a valid cron expression that runs at most once per day", "cadence must be a string" ], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 400 Long intent:** ```json { "message": "intent must be at most 500 characters", "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` Save `data.id`: paste it over `{agent_id}` in every later call. Ignore `job_id`. --- ## Get agent **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id` Settings, latest run, and live progress: the call you repeat while you wait. Free. - `finalized` (boolean, optional): `true` once collecting and the AI report are done. Wait for this, not `latest_run.status`. - `pending_jobs` (object[], optional): AI work still running. Wait each job's `retry_after_seconds` (currently 15) before checking again. - `latest_run` (object, optional): Shaped like [Get run](https://dev.virlo.ai/docs/agents#get-agent-run). `status` goes `pending`, `processing`, then `completed`, `partial_failure` (one platform or keyword failed, results still usable), or `failed`. - `intent_keywords` (string[], optional): The phrases actually searched, built from `intent` and `keywords`. Set soon after a run starts; editing either clears it until the next run. - `last_run_at` (string, optional): When the last run fully wrapped up, creator outliers included. `null` on a new agent until then. - `is_processing` (boolean, optional): Always `false` on one-time agents, even mid-run. Not a done signal. - `autopilot` (boolean, optional): `true` when [autopilot](https://dev.virlo.ai/docs/agents/autopilot) is on, the default. `autonomy_level` and `autopilot_unlocked` are older fields; read this one instead. - `pinned_keywords` (string[], optional): The keywords you set. Autopilot keeps all of them and only adds around them, so `keywords` can hold more. `null` for agents made in the Virlo app. For a progress bar, use `stage`, `progress_pct`, and `eta_seconds` ([values](https://dev.virlo.ai/docs/async-data#progress)). **When can I read my results?** | You see | Safe to read | | - | - | | `latest_run.status: "completed"` | Nothing yet. Only collecting is done. | | `finalized: true` | Summary, videos, slideshows, ads, sounds, hashtags, analysis, trends | | `last_run_at` later than `latest_run.started_at` (on a new agent: not `null`) | Creator outliers | | A video's `intelligence_status: "ready"` | That video's [Data Intelligence](https://dev.virlo.ai/docs/intelligence) | `finalized` does not wait for Data Intelligence. Videos Virlo will not analyze show `intelligence_status: "skipped"`, often more than half of an agent's videos. A video still `pending` a day later will probably never be analyzed. **cURL request:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id} \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 Done:** ```json { "data": { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "name": "Protein Recipes", "is_recurring": true, "active": true, "intent": "Track viral protein-recipe content for a fitness brand", "intent_keywords": ["high protein recipe ideas", "protein meal prep for the week", "easy protein snacks"], "cadence": "0 0 * * 0", "next_run_at": "2026-09-27T02:14:08.000Z", "last_run_at": "2026-09-24T17:31:12.491Z", "is_processing": false, "autopilot": true, "autonomy_level": "autopilot", "autopilot_unlocked": true, "cognition_enabled": true, "pinned_keywords": ["high protein recipe", "protein meal prep", "protein snack ideas"], "analysis": "No-cook overnight protein recipes are driving the most outsized reach this week...", "analysis_data": { "key_highlight": "No-cook overnight protein recipes are driving the most outsized reach this week...", "themes": [] }, "analysis_batch_start": "2026-09-24T17:23:43.301+00:00", "analysis_batch_end": "2026-09-24T17:29:14.320+00:00", "pending_jobs": [], "finalized": true, "progress_pct": 100, "stage": "completed", "eta_seconds": 0, "latest_run": { "id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90", "status": "completed", "videos_linked": 283, "outliers_identified": 12, "started_at": "2026-09-24T17:23:43.423Z", "completed_at": "2026-09-24T17:31:11.085Z" } } } ``` **Response 200 Running:** ```json { "data": { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "last_run_at": null, "pending_jobs": [ { "type": "viral_analysis", "status": "processing", "poll_url": "/v1/agents/a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "result_path": "data", "webhook_event": "content_research_agent.run.completed", "retry_after_seconds": 15 } ], "finalized": false, "progress_pct": 75, "stage": "analyzing", "eta_seconds": 893, "latest_run": { "id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90", "status": "completed", "videos_linked": 283 } } } ``` --- ## Get summary **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/summary` A one-call digest of the latest run: status, counts, the top 5 creators and trends, and the main takeaway. The best first read once `finalized` is `true`. Free. - `counts` are the run's own tallies. For how many videos you can read, use `total` from [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos). - `counts.sounds` is always `null` for now. `counts.creators` counts outlier creators only, and can read `0` until they're ready. - `run.platform_counts` are counted before filters, so they can add up to more than `videos_linked`. - Some YouTube creators show `username: "channel"`. Get their link from [creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers). **cURL request:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id}/summary \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "agent_name": "Protein Recipes", "is_recurring": true, "finalized": true, "progress_pct": 100, "stage": "completed", "eta_seconds": 0, "run": { "status": "completed", "started_at": "2026-09-24T17:23:43.423Z", "completed_at": "2026-09-24T17:31:11.085Z", "total_videos": 283, "videos_linked": 283, "platform_counts": { "youtube": 104, "tiktok": 171, "instagram": 33 }, "outliers_identified": 12 }, "counts": { "videos": 283, "slideshows": 41, "sounds": null, "creators": 12 }, "top_creators": [ { "username": "highproteinhannah", "platform": "tiktok", "followers": 84000, "weighted_score": 31.4 } ], "top_trends": [ { "name": "No-cook overnight protein", "stable_key": "no-cook-overnight-protein", "status": "new" } ], "analysis_summary": "No-cook overnight protein recipes are driving the most outsized reach this week...", "generated_at": "2026-09-24T17:40:04.276Z" } } ``` --- ## Get videos **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/videos` The videos the agent collected. **This is where you filter**, by views, date, platform, or country, as often as you like, free, without re-running the agent. Newest first, 50 per page. - `min_views` (integer, optional): Only videos with at least this many views. - `platforms` (string[], optional): `youtube`, `tiktok`, `instagram`, as `tiktok,youtube` or repeated. Plural: `platform` returns `400`. - `start_date / end_date` (string, optional): Publish date window, such as `2026-09-01` or a full timestamp. - `order_by / sort` (string, optional): `publish_date` (default), `views`, or `created_at` (when Virlo added it). `desc` (default) or `asc`. - `region` (string, optional): **Beta.** Uploader's country as a two-letter code, such as `US`. Videos with no known region are left out, and an unknown code returns none. - `intent_match` (boolean, optional): Data Intelligence agents only. `true` keeps videos that fit your intent, but pages can then come back empty before the end, and `total` counts only that page. To find every match, page without it and check `intent_match.matches`. - `include_transcript` (boolean, optional): `true` adds each video's `transcript`: the full text, plus timestamps where the transcript has them. Free. Transcripts make pages several times bigger, so use a smaller `limit`. See [Transcripts](https://dev.virlo.ai/docs/agents#agent-video-transcripts) below. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`, larger values cut to 100). `offset` or any unlisted parameter returns `400`. ### Transcripts With `include_transcript=true`, every video gets a `transcript` object: - `text`: the full transcript. - `segments`: `[{ "start": 0.64, "end": 3.52, "text": "..." }]`, in seconds from the start of the video, or `null` when there are no timestamps. - `source`: `transcribed` when Virlo turned the audio into text (always timed), or `platform` when it's the transcript TikTok or YouTube published (usually text only, with `segments` `null`). When both exist you get `transcribed`. `transcript` is `null` when the video has no speech, such as music-only videos, or hasn't been transcribed yet. **Where timestamps come from.** Most TikTok and YouTube videos have a `platform` transcript on any agent, usually as text without timestamps. A small share of `platform` transcripts do carry timestamps, so check `segments` itself instead of reading it off `source`. Virlo transcribes the audio itself, with timestamps, only when the platform didn't publish a transcript, and only on agents with [Data Intelligence](https://dev.virlo.ai/docs/intelligence). That makes **Instagram Reels** Data Intelligence only: Instagram publishes no transcripts, so a Reel's transcript always comes from Virlo, always with timestamps. While a video's `intelligence_status` is `pending`, its transcript may still be on the way. > **Note:** **Paging.** A page can hold fewer than `limit` videos while more exist, and `total` can shift a little between pages. Page until a page is empty, and remove duplicates by `id`. To rank by Virality Score, compute it from `views` and `author.followers`, as the [Research playbook](https://dev.virlo.ai/docs/agent-playbook#spotting-the-most-viral) does. [Creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers) can sort by it directly. **Full field reference** - `publish_date` is UTC with no time zone suffix, such as `2026-09-23T14:55:48`. - `duration` is the video's length in seconds, or `null` when the platform didn't report it. `sound.duration` is the length of the audio track, which can differ. - YouTube usernames start with `@`; TikTok and Instagram ones don't. `author.country` is the creator's country, often `null`, not the video's upload region. - `sound.cover_url`, `thumbnail_url`, and `author.avatar_url` are full links, or `null`. Some files are HEIC (`.heic`), which most browsers other than Safari can't show, so convert them before display. - `intelligence_status` is `ready`, `pending`, `skipped`, or `disabled`. `skipped` means Virlo chose not to analyze the video, and `intelligence_skip_reason` says why: `intent_mismatch`, `too_old`, or `under_followers` ([details](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing)). It is `null` on every other status. Agents with [Data Intelligence](https://dev.virlo.ai/docs/intelligence) off can still show `ready` on videos analyzed elsewhere, at no charge. - `upload_region_source`: `tiktok_region`, `youtube_channel_country`, and `instagram_location_tag` are exact; `inferred_normalize` is a guess. About 3 in 10 videos have no region, and Instagram videos rarely do. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -d min_views=100000 \ -d start_date=2026-09-01 \ -d region=US \ -d order_by=views \ -d limit=50 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "agent_name": "Protein Recipes", "total": 283, "limit": 50, "offset": 0, "videos": [ { "id": "e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01", "url": "https://www.tiktok.com/@fitcoachjen/video/7412345678901234567", "description": "3-ingredient protein brownies that actually taste good", "platform": "tiktok", "views": 2140000, "likes": 312000, "shares": 41200, "comments": 8900, "bookmarks": 128000, "publish_date": "2026-09-21T18:22:00", "duration": 34, "author": { "country": "US", "username": "fitcoachjen", "verified": false, "followers": 48200, "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/5e1c9a.jpg" }, "hashtags": ["proteinrecipe", "highprotein", "healthydessert"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/9b0f2d.jpg", "keyword_found_by": "high protein recipe", "is_duet": false, "is_stitch": false, "upload_region": "US", "upload_region_source": "tiktok_region", "intelligence": { "primary_topic": "high-protein dessert recipe", "content_format": "cooking_recipe", "hook_type": "tutorial_promise" }, "intent_match": { "matches": true, "reasoning": "The caption and hashtags describe a high-protein dessert recipe, which fits the intent." }, "sound": { "id": "8f6e5c50-1451-4007-a120-92744e632dad", "title": "original sound - fitcoachjen", "duration": 58, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/5e1c9a.jpg", "owner_handle": "fitcoachjen", "owner_nickname": "Jen", "is_original": true, "is_commerce_music": true, "usage_count": 1, "platform": "tiktok" }, "intelligence_status": "ready", "intelligence_skip_reason": null } ] } } ``` --- ## Get creator outliers **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/creators/outliers` Creators whose videos get far more views than their follower count predicts: small accounts punching above their weight. Free. Ready about a minute after `finalized`. For the fairest ranking across account sizes, use `order_by=weighted_score`. Pass a row's `author_id` to [similar creators](https://dev.virlo.ai/docs/agents#get-agent-similar-creators) to find more like them. - `order_by` (string, optional): `outlier_ratio` (default, average views per follower), `weighted_score`, `avg_views`, `follower_count`, or `rising` (growth since the previous run). With one run, or with `category`, `rising` falls back to Virality Score order (`ranking: "outlier_fallback"`). - `sort` (string, optional): `desc` (default) or `asc`. - `platform` (string, optional): `youtube`, `tiktok`, or `instagram`. Singular here: `platforms` returns `400`. - `follower_tier` (string, optional): `nano` (under 10,000 followers), `micro` (10,000 to 100,000), `mid` (100,000 to 1 million), or `macro` (over 1 million). - `category` (string, optional): Keep creators whose topics contain this text. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). > **Note:** With `follower_tier` or `category`, a page can come back empty before the end, and `total` ignores the filter. Use `limit=100` and page while `has_more` is `true`. **Details for developers** In `videos`, `id` is the platform's own ID, `type` is the platform, and the date is camelCase `publishDate`. `hasMore` is an older duplicate of `has_more`. Once the agent has two runs, `rising` returns `ranking: "velocity"` and a different shape: rows add `growth_followers`, `growth_views`, `growth_video_count`, and `local_video_count`, but lack `weighted_score` and `videos`, and most outlier stats are `null`. There is no `has_more`, and `total` counts only this page, so page until a page has fewer than `limit` rows. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/creators/outliers \ -H "Authorization: Bearer YOUR_API_KEY" \ -d order_by=weighted_score \ -d limit=25 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "agent_name": "Protein Recipes", "total": 12, "limit": 25, "offset": 0, "has_more": false, "hasMore": false, "outliers": [ { "author_id": "c3d4e5f6-a7b8-4901-9c2d-3e4f5a6b7c8d", "creator_url": "https://www.tiktok.com/@fitcoachjen", "creator_avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/5e1c9a.jpg", "follower_count": 48200, "avg_views": 512000, "outlier_ratio": 10.62, "weighted_score": 25.48, "videos_analyzed": 14, "creator_topics": ["fitness", "recipes", "meal prep"], "matching_topics": ["recipes", "meal prep"], "platform": "tiktok", "identified_at": "2026-09-24T17:31:02.579Z", "median_views": 431000, "top_video_views": 2140000, "breakout_video_count": 4, "avg_engagement_rate": 0.081, "posts_per_week": 5.2, "content_angle": "Three-ingredient high-protein desserts filmed in one unbroken take.", "videos": [ { "id": "7412345678901234567", "title": "", "description": "3-ingredient protein brownies that actually taste good", "views": 2140000, "likes": 312000, "comments": 8900, "publishDate": "2026-09-21T18:22:00.000Z", "url": "https://www.tiktok.com/@fitcoachjen/video/7412345678901234567", "hashtags": ["proteinrecipe"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/9b0f2d.jpg", "type": "tiktok" } ] } ] } } ``` --- ## Get latest trends **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/trends/latest` The trends the AI found in the latest run, ranked, with evidence videos and view and engagement totals: the themes from the [analysis](https://dev.virlo.ai/docs/agents#get-agent-analysis). Free. `status` is each trend's move since the previous run: `new`, `rising`, `steady`, or `fading`. A trend going from `new` to `rising` with a growing `video_count` is your strongest "post about this now" signal. `stable_key` follows a trend across runs in [trends history](https://dev.virlo.ai/docs/agents#get-agent-trends-history). **Full field reference** `avg_virality_score` is an AI rating from 0 to 1, not the Virality Score (`weighted_score`), so don't compare them. `peak_hour_utc` is the hour (UTC) when the trend's videos do best. The `prev_` fields are `null` on a first run. `insight_type` holds an [older label](https://dev.virlo.ai/docs/agents#migrating-from-orbit-comet), here and in the analysis. **cURL request:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id}/trends/latest \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "reference_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "insight_type": "custom_niche", "viral_insight_id": "d5e6f7a8-b9c0-4d12-8e34-5f6a7b8c9d01", "batch_start": "2026-09-24T17:23:43.301+00:00", "batch_end": "2026-09-24T17:29:14.320+00:00", "total": 5, "trends": [ { "id": "aa11bb22-cc33-4d44-8e55-6f778899aa00", "viral_insight_id": "d5e6f7a8-b9c0-4d12-8e34-5f6a7b8c9d01", "insight_type": "custom_niche", "reference_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "batch_start": "2026-09-24T17:23:43.301+00:00", "batch_end": "2026-09-24T17:29:14.320+00:00", "rank": 1, "stable_key": "no-cook-overnight-protein", "name": "No-cook overnight protein", "why_it_works": "Zero-effort framing and a high protein payoff in one scroll.", "tactics": ["show the jar first", "put the protein grams on screen"], "confidence": 0.86, "evidence_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"], "evidence_videos": [ { "id": "e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01", "url": "https://www.tiktok.com/@fitcoachjen/video/7412345678901234567", "platform": "tiktok" } ], "video_count": 17, "total_views": 18400000, "total_likes": 2100000, "total_comments": 54000, "total_shares": 210000, "avg_virality_score": 0.87, "platform_breakdown": { "tiktok": 11, "instagram": 4, "youtube": 2 }, "top_creators": [ { "username": "fitcoachjen", "followers": 48200, "total_views": 2140000, "video_count": 2 } ], "peak_hour_utc": 18, "status": "new", "first_seen_at": "2026-09-24T17:29:14.320+00:00", "prev_video_count": null, "prev_total_views": null, "created_at": "2026-09-24T17:30:17.390296+00:00" } ] } } ``` --- ## Get latest analysis **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/analysis/latest` The AI report on the latest run: the main themes and why they work, tactics to copy, timing, and the top videos. Free. `analysis` is the main takeaway, a short paragraph you can quote to a client; `analysis_data` holds the rest. The AI reads a sample of up to 240 videos, so it covers the strongest patterns, not every video. Before the first analysis, you get `{ "data": null }`. **Full field reference** `overview.avg_virality` runs from 0 to 1 and is not the Virality Score. `excluded_videos` lists videos the AI set aside as off-topic. `video_count` counts the sample, not the whole collection. **cURL request:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id}/analysis/latest \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "id": "d5e6f7a8-b9c0-4d12-8e34-5f6a7b8c9d01", "insight_type": "custom_niche", "reference_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "batch_start": "2026-09-24T17:23:43.301+00:00", "batch_end": "2026-09-24T17:29:14.320+00:00", "analysis": "No-cook overnight protein recipes are driving the most outsized reach this week...", "analysis_data": { "themes": [ { "name": "No-cook overnight protein", "tactics": ["show the jar in the first frame", "call out the protein grams in text"], "confidence": 0.82, "stable_key": "no-cook-overnight-protein", "video_count": 17, "why_it_works": "Zero-effort framing lowers the barrier to trying the recipe.", "evidence_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"] } ], "overview": { "avg_virality": 0.55, "total_videos": 240 }, "key_highlight": "No-cook overnight protein recipes are driving the most outsized reach this week...", "viral_tactics": ["Put the protein grams on screen in the first second."], "excluded_videos": [ { "reason": "A supplement ad, not a recipe.", "video_id": "6e736606-4eb4-434c-9070-4877af24cf56" } ], "timing_analysis": { "pattern": "Content performs best in the evening.", "peak_hours": [19, 20, 21] }, "whats_happening": [], "top_10_breakdown": { "videos": [ { "video_id": "e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01", "description": "Three-ingredient brownies with the macros on screen." } ], "intro_header": "Protein Recipes: What's Working Now", "intro_subheader": "Simple, no-cook recipes with visible macros are winning." }, "connecting_thread": "Every winning theme makes high protein feel effortless.", "fresh_insights_headline": "Effortless protein is the breakout format" }, "video_count": 240, "analyzed_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"], "model_used": "gemini-2.5-flash", "created_at": "2026-09-24T17:29:16.018692+00:00" } } ``` --- ## Get sounds **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/sounds` The sounds in the agent's videos, ranked by how many of this agent's videos use each one. Free. - `sort` (string, optional): Picks the ranking, not a direction: `video_count` (default), `usage_count` (uses across the whole platform, often `null`), or `rising` (alias `growth_7d`, the biggest growth since the previous run). Unknown values fall back to `video_count`. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `20`). `lifecycle` compares with the previous run: `new` (not in the previous run, so every sound after a first run), `rising` or `fading` (views across this agent's videos using it moved 25% or more), or `steady`. The `growth_` fields stay `null` until there are two runs. `cover_url` is a full link, or `null`. For one sound's history, pass its `id` (not `external_id`) to [usage history](https://dev.virlo.ai/docs/sounds#usage-history), $0.05 per request. > **Note:** Here `total` and `total_pages` are estimates. Page while `has_next_page` is `true`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/sounds \ -H "Authorization: Bearer YOUR_API_KEY" \ -d sort=rising \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "8f6e5c50-1451-4007-a120-92744e632dad", "external_id": "7412345678901234001", "title": "Saxophones getting louder", "platform": "tiktok", "duration": 30, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/93b232a4aa7f.jpg", "owner_handle": "saxsounds", "owner_nickname": "Sax Sounds", "is_original": false, "is_commerce_music": true, "usage_count": 138106, "video_count": 42, "avg_views": 612000, "growth_video_count": 18, "growth_views": 240000, "lifecycle": "rising" } ], "pagination": { "page": 1, "limit": 20, "total": 21, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` --- ## Get hashtags **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/hashtags` Hashtag stats across the agent's videos: video count, views, engagement, growth between runs, and top creators. Free. - `sort` (string, optional): Picks the ranking, not a direction: `volume` (default), `growth` (biggest jump since the last run), or `avg_views`. After one run, `growth` gives the same order as `volume`. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). `order_by` and `platform` return `400` here. `lifecycle` works as for [sounds](https://dev.virlo.ai/docs/agents#get-agent-sounds). `total` and `total_pages` are estimates, so page while `has_next_page` is `true`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/hashtags \ -H "Authorization: Bearer YOUR_API_KEY" \ -d sort=growth \ -d limit=25 ``` **Response 200:** ```json { "data": [ { "hashtag": "proteinrecipe", "video_count": 96, "total_views": 41200000, "avg_views": 429166, "avg_engagement": 0.081, "growth_video_count": 34, "lifecycle": "rising", "top_creators": [ { "username": "fitcoachjen", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/5e1c9a.jpg", "video_count": 8 } ] } ], "pagination": { "page": 1, "limit": 25, "total": 26, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` --- ## Get hooks **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/hooks` The strongest opening lines (hooks) from the agent's videos, ranked by Virality Score. Parameters and fields: [Agent hooks](https://dev.virlo.ai/docs/hooks#agent-hooks). **Cost:** $0.25 per request. Free while the agent has no hooks yet (`coverage.videos_with_hooks` is `0`), and always free when it has Data Intelligence on. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/hooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 ``` --- ## Get slideshows **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/slideshows` TikTok photo carousels the agent collected with the videos. Free. Almost every slideshow has a `region`, so region filters work best here. Same filters as [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos), but `platforms` and `intent_match` do nothing. **Fields that differ from videos** Slideshows have `region` instead of `upload_region`, an `images` list, and an `is_eligible_for_commission` flag. They have no `sound`, `intent_match`, `is_duet`, `is_stitch`, or `author.country`. `publish_date` ends in `+00:00`. `intelligence` uses the [slideshow fields](https://dev.virlo.ai/docs/intelligence#slideshow-intelligence). **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/slideshows \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=US \ -d limit=50 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "agent_name": "Protein Recipes", "total": 41, "limit": 50, "offset": 0, "slideshows": [ { "id": "b7da2c2c-5ff5-4fe3-8e63-dafe51f35314", "url": "https://www.tiktok.com/@mealprepmaya/photo/7677872045765430561", "description": "5 high protein breakfasts under 10 minutes", "platform": "tiktok", "views": 98900, "likes": 5190, "shares": 630, "comments": 120, "bookmarks": 3040, "publish_date": "2026-09-20T07:44:56+00:00", "author": { "username": "mealprepmaya", "verified": false, "followers": 8979, "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/8ead10.jpg" }, "hashtags": ["mealprep", "highprotein"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/cea5f4.jpg", "images": [ { "image_url": "https://auth.virlo.ai/storage/v1/object/public/slideshow-images/669187.jpg", "position": 0 } ], "keyword_found_by": "protein meal prep", "is_eligible_for_commission": false, "region": "US", "intelligence": { "content_format": "listicle", "narrative_arc": "listicle", "text_density": "balanced" }, "intelligence_status": "ready", "intelligence_skip_reason": null } ] } } ``` --- ## Get ads **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/ads` Ads from Meta's Ad Library that the agent collected, when `meta_ads_enabled` is on. Free. An agent without ads returns an empty list. - `order_by` (string, optional): `created_at` (default, when Virlo collected the ad) or `page_like_count` (the advertiser page's likes). - `sort` (string, optional): `desc` (default) or `asc`. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/ads \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "agent_name": "Protein Recipes", "total": 37, "limit": 50, "offset": 0, "ads": [ { "id": "56c50beb-9518-463a-9d9b-d8509b94fef6", "ad_archive_id": "1789791185482522", "page_id": "120945717945722", "page_profile_url": "https://www.facebook.com/exampleproteinco/", "page_profile_picture_url": "https://scontent.xx.fbcdn.net/v/example.jpg", "is_active": true, "start_date": "2026-09-15", "end_date": "2026-09-20", "url": "https://www.facebook.com/ads/library/?id=1789791185482522", "caption": "exampleproteinco.com", "body": "20g of protein in every bar. Try the new flavors.", "cta_type": "SHOP_NOW", "page_like_count": 218491, "title": "New protein bar flavors", "video_url": null, "created_at": "2026-09-21T16:23:41.881496+00:00", "keyword_found_by": "protein snack ideas" } ] } } ``` --- ## List agents **Endpoint:** `GET https://api.virlo.ai/v1/agents` Your agents, newest first, including ones made in the Virlo app, with their full settings. Free. It is per person, not per team: only agents owned by whoever created your API key. A teammate's agents aren't listed, and reading one by `id` returns `404`. - `is_recurring` (boolean, optional): `true` for recurring agents only, `false` for one-time only. - `include_inactive` (boolean, optional): Also list paused agents. Deleted agents never appear. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). There is no total. Page until a page has fewer than `limit` agents. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -d is_recurring=true \ -d limit=50 ``` **Response 200:** ```json { "data": { "limit": 50, "page": 1, "count": 1, "agents": [ { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "name": "Protein Recipes", "is_recurring": true, "active": true, "team_id": "9d4c2b10-8e6f-4a23-b1c7-0a5e3f9d2b18", "source": "api", "keywords": ["high protein recipe", "protein meal prep", "protein snack ideas"], "platforms": ["youtube", "tiktok", "instagram"], "exclude_keywords": ["powder", "supplement"], "exclude_keywords_strict": false, "meta_ads_enabled": true, "data_intelligence_enabled": false, "english_only": true, "intent": "Track viral protein-recipe content for a fitness brand", "intent_keywords": ["high protein recipe ideas", "protein meal prep for the week", "easy protein snacks"], "autopilot": true, "autonomy_level": "autopilot", "autopilot_unlocked": true, "cognition_enabled": true, "pinned_keywords": ["high protein recipe", "protein meal prep", "protein snack ideas"], "cadence": "0 0 * * 0", "next_run_at": "2026-09-27T02:14:08.000Z", "last_run_at": "2026-09-24T17:31:12.491Z", "is_processing": false, "created_at": "2026-09-24T17:23:43.255Z", "updated_at": "2026-09-24T17:31:12.525Z" } ] } } ``` --- ## Update agent **Endpoint:** `PUT https://api.virlo.ai/v1/agents/:id` Changes settings for future runs. Send only what you want to change. Free. Videos already collected are not re-filtered. There is no "run now" call: to research again, create a new one-time agent. [Autopilot](https://dev.virlo.ai/docs/agents/autopilot#autonomy) never adds a billed run either. An agent can't switch type: sending `is_recurring` returns `400` (`property is_recurring should not exist`). - `active` (boolean, optional): `false` pauses a recurring agent: no runs, no charges. `true` resumes it. - `name / intent / platforms` (mixed, optional): Replace the current values, with the same rules as [Create agent](https://dev.virlo.ai/docs/agents#create-agent). - `keywords` (string[], optional): Replaces the list, with the same rules as [Create agent](https://dev.virlo.ai/docs/agents#create-agent). On an agent made through the API, your new list becomes the [pinned](https://dev.virlo.ai/docs/agents/autopilot#pinned-keywords) set that autopilot always keeps. - `cadence` (string, optional): Recurring agents only. On a one-time agent it returns `400` (`cadence can only be set on a recurring agent`). - `exclude_keywords` (string[], optional): Replaces the list. On an agent made through the API, autopilot keeps every word you send. Clear it (`[]`) and the next run picks new words from your `intent`. - `exclude_keywords_strict / meta_ads_enabled / english_only` (boolean, optional): Turn these on or off. - `data_intelligence_enabled` (boolean, optional): Turning it on makes each later run $1.50. - `autopilot` (boolean, optional): `true` turns [autopilot](https://dev.virlo.ai/docs/agents/autopilot) on. `false` turns it off and keeps the setup exactly as you set it. **cURL request:** ```bash curl -X PUT https://api.virlo.ai/v1/agents/{agent_id} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "keywords": ["high protein recipe", "protein meal prep", "protein desserts"] }' ``` **Response 200:** ```json { "data": { "id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "name": "Protein Recipes", "is_recurring": true, "active": true, "keywords": ["high protein recipe", "protein meal prep", "protein desserts"], "intent_keywords": null, "autopilot": true, "pinned_keywords": ["high protein recipe", "protein meal prep", "protein desserts"], "cadence": "0 0 * * 0", "next_run_at": "2026-09-27T02:14:08.000Z", "updated_at": "2026-09-24T18:02:11.684Z" }, "message": "Agent updated" } ``` **Response 400 Empty body:** ```json { "message": "At least one field must be provided", "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` --- ## Delete agent **Endpoint:** `DELETE https://api.virlo.ai/v1/agents/:id` Deletes the agent, so it never runs or charges again. Returns `204`, also on a repeat call. **This can't be undone.** To stop it for now, pause it with [Update agent](https://dev.virlo.ai/docs/agents#update-agent). A deleted agent disappears from [List agents](https://dev.virlo.ai/docs/agents#list-agents), even with `include_inactive=true`. Reading it by `id` may work for a while, showing `active: false`, but save what you need first. **cURL request:** ```bash curl -X DELETE https://api.virlo.ai/v1/agents/{agent_id} \ -H "Authorization: Bearer YOUR_API_KEY" ``` --- ## More reads Less common questions, all free. ### Get benchmarks `GET /v1/agents/:id/benchmarks`: what's normal for creators in this niche by account size, so you can see whether a creator beats others their size. One row per follower tier, largest first. Empty tiers are left out. **Fields and example** - `follower_tier` (string, optional): The tiers from [creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers), or `unknown` (no follower count, so `median_followers` and `median_posting_frequency_days` are `null`). - `median_engagement_rate` (number, optional): A decimal (`0.071` means 7.1%). Compare creators only within a tier. A small `creator_count` makes it noisy. - `median_videos_in_niche` (integer, optional): This agent's videos per creator. Often `1`. - `median_posting_frequency_days` (number, optional): Days between videos in this agent's collection, counting only creators with at least two. Not their overall posting rate, and `null` when no creator in the tier has two. **cURL:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id}/benchmarks \ -H "Authorization: Bearer YOUR_API_KEY" ``` **JSON:** ```json { "data": [ { "follower_tier": "macro", "creator_count": 21, "median_engagement_rate": 0.0294, "median_followers": 2130000, "median_videos_in_niche": 1, "median_posting_frequency_days": 242.43 }, { "follower_tier": "micro", "creator_count": 48, "median_engagement_rate": 0.071, "median_followers": 42000, "median_videos_in_niche": 1, "median_posting_frequency_days": 1.4 } ] } ``` ### Get affinity `GET /v1/agents/:id/affinity`: the topics this niche's creators cover most, and the top sounds and hashtags in the agent's videos, up to 20 of each. **Beta:** a rough guide. For rankings and growth, use [sounds](https://dev.virlo.ai/docs/agents#get-agent-sounds) and [hashtags](https://dev.virlo.ai/docs/agents#get-agent-hashtags). **Example** **cURL:** ```bash curl https://api.virlo.ai/v1/agents/{agent_id}/affinity \ -H "Authorization: Bearer YOUR_API_KEY" ``` **JSON:** ```json { "data": { "creator_topics": [ { "topic": "meal prep", "creator_count": 31 } ], "related_sounds": [ { "title": "Saxophones getting louder", "artist": "saxsounds", "sound_id": "8f6e5c50-1451-4007-a120-92744e632dad", "video_count": 42 } ], "related_hashtags": [ { "hashtag": "proteinrecipe", "video_count": 96 } ] } } ``` ### Get similar creators `GET /v1/agents/:id/creators/:creator_id/similar`: other creators in this agent's videos who share the most hashtags and sounds with one creator. **Beta:** a rough guide. For `creator_id`, use an `author_id` from [creator outliers](https://dev.virlo.ai/docs/agents#get-agent-outliers) or from this endpoint's rows (video rows have none). A `creator_id` not in this agent returns an empty list, not an error. **Parameters and example** - `limit` (integer, optional): 1 to 100. Default `20`. There is no paging: `page` returns `400`. **cURL:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/creators/{creator_id}/similar \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 ``` **JSON:** ```json { "data": [ { "author_id": "40dc6179-5bea-4797-8c15-5c78ee0b3d1a", "username": "proteinpantry", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/2a7f11.jpg", "url": "https://www.tiktok.com/@proteinpantry", "verified": false, "followers": 5510, "shared_hashtag_count": 3, "shared_sound_count": 1, "similarity_score": 4 } ] } ``` ### Get trends history `GET /v1/agents/:id/trends`: every trend this agent has produced, newest first, shaped like [latest trends](https://dev.virlo.ai/docs/agents#get-agent-trends). Filter by `stable_key` to follow one trend across runs. **Parameters and example** - `stable_key` (string, optional): Show only one trend's history. - `start_date / end_date` (string, optional): Only trends in this window. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). The `pagination` object's `total` is a real count. **cURL:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/trends \ -H "Authorization: Bearer YOUR_API_KEY" \ -d stable_key=no-cook-overnight-protein \ -d limit=50 ``` ### Get analysis history `GET /v1/agents/:id/analysis`: every analysis this agent has produced, newest first, shaped like [latest analysis](https://dev.virlo.ai/docs/agents#get-agent-analysis). **Parameters and example** - `start_date / end_date` (string, optional): Only analyses in this window. - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). The `pagination` object's `total` is a real count. **cURL:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/analysis \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 ``` ### List runs `GET /v1/agents/:id/runs`: the agent's runs, newest first. Each is a **collection report**: how many videos came in, how many were dropped, and why. When an agent returns less than you expected, start here. **Parameters, fields, and example** - `page / limit` (integer, optional): From 1, and 1 to 100 per page (default `50`). Other parameters, such as `status`, are ignored. There is no total, and the response shows `offset`, not `page`. Page until a page has fewer than `limit` runs. On a `partial_failure` run the counts can read `0` even though videos came in, so check `total` on [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos). - `keyword_breakdown`: results per phrase actually searched, the best field for tuning keywords. - `intent_filtered`, `language_filtered_count`, `exclude_keywords_filtered`: videos dropped by your `intent`, by `english_only`, and by your excludes. A high `intent_filtered` means your keywords pull in off-topic videos. - `youtube_count`, `tiktok_count`, `instagram_count`: what each platform returned, before filters, so they can add up to more than `videos_linked`. - `total_videos_inserted` and `total_videos_updated`: videos new to Virlo, and videos Virlo already had, now refreshed. - `duplicates_dropped` and `trends_detected` are currently always `0` and `null`. **cURL:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/runs \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **JSON:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "limit": 50, "offset": 0, "count": 1, "runs": [ { "id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90", "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "status": "completed", "total_videos_inserted": 122, "total_videos_updated": 161, "total_videos_failed": 0, "videos_linked": 283, "meta_ads_linked": 37, "slideshows_linked": 41, "youtube_count": 104, "tiktok_count": 171, "instagram_count": 33, "exclude_keywords_filtered": 14, "intent_filtered": 38, "language_filtered_count": 24, "duplicates_dropped": 0, "outliers_identified": 12, "trends_detected": null, "execution_time_ms": 447662, "created_at": "2026-09-24T17:23:43.349Z", "started_at": "2026-09-24T17:23:43.423Z", "completed_at": "2026-09-24T17:31:11.085Z", "keyword_breakdown": [ { "keyword": "high protein recipe ideas", "tiktok_count": 76, "videos_linked": 132, "youtube_count": 54, "videos_updated": 94, "instagram_count": 13, "videos_inserted": 38 } ] } ] } } ``` ### Get run `GET /v1/agents/:id/runs/:run_id`: one run, the same object as in [List runs](https://dev.virlo.ai/docs/agents#list-agent-runs). A run from a different agent returns `404` (`Run not found`). --- ## Coming from Orbit or Comet? Orbit and Comet were the old names for one-time and recurring agents. Their URLs still work but are deprecated, so move now. Agent IDs carry over, so it's mostly a URL change: [Orbit](https://dev.virlo.ai/docs/orbit) · [Comet](https://dev.virlo.ai/docs/comet). In `insight_type`, `orbit` still means a one-time agent and `custom_niche` a recurring one. --- # Autopilot > After each run, a recurring Content Research Agent tunes its own keywords so it keeps finding new videos. Autopilot is on by default, never removes the keywords you set, and never adds charges. Source: https://dev.virlo.ai/docs/agents/autopilot After each run, a recurring [Content Research Agent](https://dev.virlo.ai/docs/agents) tunes its own keywords so it keeps finding new videos. Autopilot is on for every new agent, and it never removes the keywords you set. **At a glance** - **What it does:** Rewords the agent's searches, adds keywords, follows breaking stories in its niche, and collects more widely after a thin run. One-time agents do none of this. - **You send:** Nothing. It is on by default. To turn it off, send `autopilot: false`. - **You get back:** `autopilot` and `pinned_keywords` on every agent, plus an [activity log](https://dev.virlo.ai/docs/agents/autopilot#get-agent-activity) of what changed and why. - **Cost:** Free. Autopilot never adds charges. The price per run stays the same, and it never starts an extra billed run. --- ## How autopilot works Autopilot is on by default for every new recurring agent (`is_recurring: true`), whether you make it through the API, over [MCP](https://dev.virlo.ai/docs/mcp), or in the Virlo app. There is nothing to unlock. After each run, autopilot can: - Reword the agent's searches. - Add keywords, up to 15 in all. If you set 15 or more yourself, it keeps them all and adds none. - Add short-lived **timely keywords** for a breaking story in the niche ([events](https://dev.virlo.ai/docs/agents/autopilot#get-agent-events)). They expire on their own. - Collect more widely when a run comes back thin. Every keyword change has to pass a quality check first. The [activity log](https://dev.virlo.ai/docs/agents/autopilot#get-agent-activity) records each change and the reason for it. Autopilot never: - Removes a keyword or exclude term you set on an agent made through the API or MCP (see [Your keywords stay](https://dev.virlo.ai/docs/agents/autopilot#pinned-keywords)). - Adds charges. The price per run stays the same, and it never starts an extra billed run. - Changes the schedule (`cadence`) or pauses the agent. To turn it off, send `autopilot: false` when you [create](https://dev.virlo.ai/docs/agents#create-agent) or [update](https://dev.virlo.ai/docs/agents#update-agent) the agent, or use [Turn autopilot on or off](https://dev.virlo.ai/docs/agents/autopilot#set-autonomy). The agent then keeps its setup exactly as you set it, and it still runs on schedule. Agent responses show the setting as `autopilot`, `true` when it is on. `autonomy_level` and `autopilot_unlocked` are older fields that still come back. Read `autopilot` instead. ### Your keywords stay The keywords you send are pinned. The agent returns them as `pinned_keywords`, and autopilot only adds around them, so `keywords` holds yours plus any autopilot added. Send new `keywords` with [Update agent](https://dev.virlo.ai/docs/agents#update-agent) and your list replaces the pinned one. Your edits win. Exclude terms work the same way: autopilot never removes one you set in `exclude_keywords`. Agents made in the Virlo app have no pinned keywords (`pinned_keywords: null`), even after you edit them through the API, so autopilot manages their whole list. --- ## Turn autopilot on or off **Endpoint:** `PUT https://api.virlo.ai/v1/agents/:id/autonomy` Turns autopilot on or off for one agent. It does the same as `autopilot` on [Update agent](https://dev.virlo.ai/docs/agents#update-agent). Free. Send at least one field. - `autopilot` (boolean, optional): `true` turns autopilot on, the default. `false` keeps the agent's setup exactly as you set it. - `cognition_enabled` (boolean, optional): `false` stops the agent reviewing its own runs, so autopilot changes nothing. The agent keeps collecting. `true` turns reviews back on. - `autonomy_level` (string, optional): The older way to set the same switch, still accepted. `"autopilot"` is on and `"suggest"` is off. Send this or `autopilot`, not both. An empty body returns `400` (`No autonomy fields provided`). So does sending `autopilot` and `autonomy_level` with different answers (`autopilot and autonomy_level disagree; send one of them`). **Autopilot off request:** ```bash curl -X PUT https://api.virlo.ai/v1/agents/{agent_id}/autonomy \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "autopilot": false }' ``` --- ## Get activity **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/activity` The agent's change log: what it noticed, what autopilot changed, and why. Newest first. Free. Empty for one-time agents, and possibly right after a recurring agent's first run. Each entry has `content`, `created_at`, and a `category`: usually `observation`, `decision`, or `milestone`, sometimes `user_pref` (a note about your preferences). - `limit` (integer, optional): 1 to 100. Default `20`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/activity \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "count": 1, "activity": [ { "content": "This run collected 283 videos, and 71% were on target. No-cook recipes drove most of the reach.", "category": "observation", "created_at": "2026-10-04T02:31:04.325401+00:00" } ] } } ``` --- ## Get events **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/events` Breaking stories a recurring agent spotted in its niche, such as a launch or a controversy. Free. Active events come first, most important first. The [`content_research_agent.event.detected`](https://dev.virlo.ai/docs/webhooks#supported-events) webhook fires as soon as one is confirmed. - `limit` (integer, optional): 1 to 100. Default `50`. **Full field reference** - `source`: `corpus_burst` (spotted in your agent's videos) or `news_scan` (from the news, then confirmed on the platforms). - `salience`: importance from 0 to 10. - `status`: `candidate`, `confirmed` (active), `dismissed`, or `expired`. - `evidence`: `url` and `views` are `null` when the evidence is your agent's own videos. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/events \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "count": 1, "events": [ { "id": "c12fc742-7c5d-44fd-9b9c-765710147980", "title": "New protein cereal launch", "summary": "A major cereal brand launched a 20g-protein cereal, and taste tests are spreading.", "source": "news_scan", "salience": 8, "status": "confirmed", "keywords": ["protein cereal launch"], "evidence": [ { "url": "https://example.com/protein-cereal-launch", "views": null, "description": "Cereal brand announces a 20g-protein cereal" } ], "detected_at": "2026-10-01T23:52:14.965821+00:00", "confirmed_at": "2026-10-01T23:52:15.102+00:00", "expires_at": "2026-10-08T23:52:21.405+00:00" } ] } } ``` --- ## List proposals (deprecated) **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/proposals` > **Note:** Deprecated. Autopilot applies changes itself, and [Get activity](https://dev.virlo.ai/docs/agents/autopilot#get-agent-activity) shows what changed and why. The proposal endpoints still work for now and send a `Deprecation: true` header. There is no removal date yet. The changes this agent has proposed, newest first. Free. Changes autopilot made show as `auto_applied`. With autopilot off, changes wait here as `pending`. - `status` (string, optional): `pending`, `applied`, `auto_applied`, `dismissed`, or `reverted`. An unknown value returns an empty list, not an error. - `limit` (integer, optional): 1 to 100. Default `50`. | Proposal `type` | What it proposes | | - | - | | `keyword_refresh` | Swap stale keywords | | `filter_change` | Collect more broadly | | `timely_keywords` | Add short-lived keywords for a breaking story | | `cadence_change` | Run daily (costs more) | | `pause` | Pause the agent | | `early_run` | An extra run now, billed like any other | | `event_detected` | An alert, not a change. Dismiss it once read. | **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/{agent_id}/proposals \ -H "Authorization: Bearer YOUR_API_KEY" \ -d status=pending ``` **Response 200:** ```json { "data": { "agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "count": 1, "proposals": [ { "id": "b94357b3-0c3b-4289-87c7-648beb1fe31f", "content_research_agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44", "type": "timely_keywords", "source": "event", "rationale": "Detected \"New protein cereal launch\" in your niche. Adding 1 timely keyword.", "diff": { "before": { "timely_keywords": [] }, "after": { "event_id": "c12fc742-7c5d-44fd-9b9c-765710147980", "timely_additions": [ { "keyword": "protein cereal launch", "expires_at": "2026-10-08T23:52:21.405+00:00" } ] }, "event": { "title": "New protein cereal launch", "salience": 8 } }, "status": "pending", "created_at": "2026-10-01T23:52:25.448948+00:00", "decided_at": null, "updated_at": "2026-10-01T23:52:25.448948+00:00" } ] } } ``` --- ## Apply, dismiss, or revert a proposal (deprecated) Deprecated, like [List proposals](https://dev.virlo.ai/docs/agents/autopilot#list-agent-proposals). Three free calls, under `/v1/agents/:id/proposals/:proposal_id/`. Applying an `early_run` starts an extra run, billed like any other. - `POST .../apply` approves a pending proposal and makes the change. On an agent made through the API, the keywords and exclude terms you approve become your [pinned](https://dev.virlo.ai/docs/agents/autopilot#pinned-keywords) lists. - `POST .../dismiss` rejects a pending proposal. - `POST .../revert` undoes an applied or auto-applied change. Only `pending` proposals can be applied or dismissed, and only applied or auto-applied ones reverted. Otherwise you get `400`, such as `Proposal ... is already applied`. `event_detected` has nothing to apply, so dismiss it. Applying `early_run` returns `400` if the agent ran in the last 24 hours or your balance can't cover the run. To stop autopilot changing an agent, turn it off with `autopilot: false` ([Turn autopilot on or off](https://dev.virlo.ai/docs/agents/autopilot#set-autonomy)) instead of reverting each change. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/agents/{agent_id}/proposals/{proposal_id}/apply \ -H "Authorization: Bearer YOUR_API_KEY" ``` --- # Intent cookbook > How to write the one-sentence intent that tells a Content Research Agent which videos to keep, with good and bad examples from real agents. Source: https://dev.virlo.ai/docs/intent-cookbook A Content Research Agent collects short-form videos on a topic. Keywords decide where it searches. Your `intent`, one plain sentence, decides **which videos it keeps**. **At a glance** - **What it does:** Tells the agent what counts as on-topic, so it sharpens your keywords and drops videos that don't fit. - **You send:** One sentence. Aim for 40 to 250 characters (500 max; at least 20 through an AI assistant). - **You get back:** More on-topic videos and less junk. - **Cost:** Drafting keywords is free. Each run costs $0.50, or $1.50 with [Data Intelligence](https://dev.virlo.ai/docs/intelligence). - **How long:** Keyword drafts take seconds. Half of runs collect in under 8 minutes, 9 in 10 in under 20; the AI report adds a few minutes. ## What intent does - **Sharpens your keywords.** If your keywords are too broad, the agent rewrites each one into a more specific phrase before the first run (`sourdough scoring pattern` became `sourdough scoring patterns home bakers`). See them in `intent_keywords` on [Get agent](https://dev.virlo.ai/docs/agents#get-agent). Changing `keywords` or `intent` clears that list until the next run. - **Writes exclude words.** If you send no `exclude_keywords`, the agent writes them from your intent when a run starts. They can include words from your own topic, so check `exclude_keywords` after the first run. - **Drops off-topic videos.** Every run, an AI reads each video's caption, hashtags, and transcript and drops the misses (`intent_filtered`). It skips this step when the intent is too vague to judge by. - **Marks videos yes or no.** With Data Intelligence on (`data_intelligence_enabled: true`), each kept video gets `intent_match`: `matches` true or false plus a short `reasoning`, judged the same way. Recurring agents may add news keywords, shown in the run's `keyword_breakdown` but not in `intent_keywords`. ## The formula Write **one concrete sentence** with four parts: 1. **Goal**: find videos, watch a niche over time, collect hooks, or spot breakout videos. 2. **Niche**: a specific topic, not one bare word. 3. **Content type**: tutorials, get-ready-with-me videos, UGC ads, slideshows. 4. **Boundary**: what to leave out, if it matters. ``` [Goal] [content type] about [niche], not [what to leave out]. ``` Then add **7 to 12 keywords**: multi-word phrases for the same topic, such as `home espresso setup` and `beginner espresso guide`. In about 11,000 runs (Aug to Sep 2026), intents of 40 to 250 characters and 7 to 12 keywords were least likely to have over 70% of videos dropped as off-topic. **Exclude words** drop any video whose caption, hashtags included, contains one as a whole word: `art` drops "latte art" and "#art", but not "article". Never use a word from your own topic. For Apple the company, exclude `recipe` and `orchard`, not `apple the fruit`, which almost never appears word for word in a caption. ## Good intents Tidied-up intents from real agents that kept hundreds of videos per run: - **Research a niche:** Find beginner morning skincare routines and honest drugstore product reviews, not dermatologist lectures or brand-only ads. - **Sell an app:** Find viral Gen Z videos that sell a photo or AI-image mobile app with demos or "this app is called" reveals, not desktop tutorials or generic photo tips. - **Watch over time:** Monitor UFC and MMA highlight content to track which fight-night hooks and formats keep winning week over week. ## Bad intents - **Keyword dump or list**, like `Keyword research: jeep, fitness, gym` or `skin / curly hair / good hooks`. It gives the agent nothing to work with. - **Too vague**, like `I want to find viral video`. The agent can't tell on-topic from off-topic, so junk gets through. - **Thin keywords**: a precise intent with 1 to 3 generic keywords. The intent drops most of what they find, and about 1 run in 5 ends with 20 videos or fewer. - **Wrong language**: turning off `english_only` but writing the intent and keywords in English. The agent searches in the language you write in, so you still get mostly English. Write in the language you want. ## Step by step Same steps via the API or an AI assistant ([MCP](https://dev.virlo.ai/docs/mcp) tool names in parentheses). 1. **[Draft keywords](https://dev.virlo.ai/docs/agents#suggest-keywords)** from your intent with `POST /v1/agents/suggest-keywords` (`suggest_keywords`). Free. 2. **[Create the agent](https://dev.virlo.ai/docs/agents#create-agent)** with the **same** intent: `POST /v1/agents` (`search_keywords` once, `create_niche_monitor` on repeat). $0.50 a run, $1.50 with Data Intelligence. 3. **Wait.** Check `GET /v1/agents/:id` (`get_keyword_search_results` or `get_niche_monitor_data`) every 30 seconds (polling, free) until `finalized` is `true`: the run and its AI report are done. **cURL:** ```bash curl -X POST https://api.virlo.ai/v1/agents/suggest-keywords \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "intent": "Find what is working in home espresso content for beginners, not barista competition clips", "platforms": ["tiktok", "youtube"] }' ``` **Response:** ```json { "data": { "keywords": [ "home espresso setup", "beginner espresso guide", "espresso machine review", "easy espresso recipe", "latte art for beginners", "budget espresso machine", "espresso tips home", "grinding coffee at home", "espresso puck prep", "home barista basics" ], "exclude_keywords": ["competition", "championship", "pro", "advanced", "expert", "latteart", "battle", "world"], "reasoning": "This set focuses on beginner-friendly home espresso content by including terms like 'setup', 'guide', and 'easy', while excluding professional competition and cafe-related terms to avoid off-topic results.", "quality": { "score": 100, "passes": true, "issues": [], "stats": { "count": 10, "avg_words_per_keyword": 3.2, "single_word_count": 0, "long_keyword_count": 0, "duplicate_count": 0, "core_token_coverage": 0.7, "core_token": "espresso" } }, "timely_context_used": false } } ``` **Check it first.** Excluding `latteart` drops the #latteart videos that `latte art for beginners` should find, so cut it. `quality` grades the keyword set, not your intent: a vague intent like `coffee` can still score 100. If `quality.passes` is `false`, read `quality.issues`, then sharpen the intent or fix the flagged keywords. Creating the agent starts its run. For `intent_match` on each video ($1.50 a run), add `"data_intelligence_enabled": true`. **cURL:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": false, "name": "Home Espresso for Beginners", "intent": "Find what is working in home espresso content for beginners, not barista competition clips", "keywords": ["home espresso setup", "beginner espresso guide", "budget espresso machine", "latte art for beginners", "espresso puck prep", "home barista basics", "espresso tips home"], "exclude_keywords": ["competition", "championship", "pro", "advanced", "expert", "battle", "world"], "platforms": ["tiktok", "youtube"] }' ``` ## How to tell it worked Read the run's numbers in `latest_run` on [Get agent](https://dev.virlo.ai/docs/agents#get-agent), or in [List runs](https://dev.virlo.ai/docs/agents#list-agent-runs). | Check | Healthy | Needs a fix | | - | - | - | | `videos_linked` (videos kept) | A few hundred (about 360; about 200 on later recurring runs) | 20 or fewer | | Off-topic share: `intent_filtered` ÷ (`videos_linked` + `intent_filtered`) | Some, not most (typically about 30%) | 0% (intent too vague) or over 70% (intent too tight, or keywords off-topic) | | `intent_match` on each video | A mix of true and false | All `null`: Data Intelligence is off | Low count? [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos) may list more than the run's count. The run's `keyword_breakdown` shows what each keyword found. On Get videos, the `intent_match=true` filter's `total` covers one page only. To count all matches, page through without the filter and count `intent_match.matches: true`. **If results look off:** 1. **Sharpen the intent**: add a content type and a boundary. 2. **Refresh keywords**: call suggest-keywords with `mode: "refresh"` and the current keywords in `existing_keywords`. 3. **Apply them**: use [Update agent](https://dev.virlo.ai/docs/agents#update-agent) on a recurring agent. A one-time agent never runs again, so create a new one (charged again). Single-word keywords pull in more off-topic videos. --- # Data Intelligence > AI labels the videos a Content Research Agent collects with 79 fields: topic, hook, format, tone, brand safety, who is on screen, AI use and more. Off-topic, year-old and underperforming videos are skipped on purpose. Slideshows get 70. Costs $1.00 extra per run. Source: https://dev.virlo.ai/docs/intelligence Data Intelligence is an add-on for [Content Research Agents](https://dev.virlo.ai/docs/agents). AI labels the videos worth studying with 79 fields, like hook, format, tone and brand safety. It skips the rest on purpose and tells you why. You can then compare videos by what is in them, not just by views. **At a glance** - **What it does:** Labels the videos an agent collects that are worth studying: hook, format, tone, brand safety, who is on screen and more. - **You send:** `data_intelligence_enabled: true` when you create an agent. A recurring agent can also turn it on later. - **You get back:** An `intelligence` block on each analyzed video and slideshow, and an `intelligence_status` on all of them. Videos also get `intent_match`: does this video fit your brief? - **Cost:** $1.00 extra per run, so a run costs $1.50 instead of $0.50. Reading the results is free. - **How long:** A run usually takes under 20 minutes. Fields keep arriving after it finishes. Videos marked `skipped` never get them. --- ## How it works 1. You create a [Content Research Agent](https://dev.virlo.ai/docs/agents) with `data_intelligence_enabled: true`. 2. The agent collects videos and slideshows for your keywords. Each round of collecting is a **run**. 3. AI reviews the ones worth studying ([which ones](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing)). It looks at still frames when it can, and reads the caption and what is said (for a slideshow, its images and their text). It writes what it finds into an `intelligence` block. 4. You read the results for free with [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos) and [Get slideshows](https://dev.virlo.ai/docs/agents#get-agent-slideshows). Each video and slideshow also has an `intelligence_status` that says whether its fields are in yet. See [When fields are missing](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing). --- ## What you get The 79 fields fall into 14 groups: | Group | What it tells you | Fields | | - | - | - | | Content | Topic, category and kind of content | 5 | | Visual | Format, camera, setting, lighting | 11 | | Hook and on-screen text | The opening line and the text on screen | 6 | | Captions and transcript | Burned-in captions, spoken words, language | 7 | | Tone | Mood, sentiment, speaking style | 3 | | Brand safety | Safety tier, sensitive topics, sponsorship, brands named | 5 | | Engagement tactics | Calls to action, social proof, trend references | 3 | | Summary | Short summary, educational or not, what the AI was unsure about | 3 | | People on screen | Who appears, apparent age and gender, faceless or on camera | 17 | | AI use | Whether the video itself was made with AI | 4 | | Brands and products on screen | Logos, products and characters in frame | 3 | | Animals | Which animals appear and whether they are the subject | 3 | | Screen and audio | Screen recordings, and whether anyone speaks | 2 | | Edit structure | Compilations, rankings, reposts, before-and-after, watermarks | 7 | You can't ask the API for only faceless videos, say. You sort the results yourself, in code or a spreadsheet ([Use cases](https://dev.virlo.ai/docs/intelligence#use-cases)). Only [`intent_match`](https://dev.virlo.ai/docs/intelligence#intent-matching) has a built-in filter. > **Note:** [Creator lookups](https://dev.virlo.ai/docs/satellite/creators#data-intelligence) with `data_intelligence` get only the first eight groups: 43 fields per video, 35 per slideshow. ### Filters to apply yourself | You want | Check that | | - | - | | Faceless videos | `presence_style` is `faceless_voiceover`, `faceless_silent` or `hands_or_pov` | | A person talking to the camera | `presence_style` is `on_camera_presenter` and `is_silent` is `false` | | No AI-made content | `ai_provenance` is `none` | | Original posts only | `is_repost` and `is_compilation` are `false` | | Sponsored posts | `is_sponsored` is `true` | | An animal is the star | `animal_presence` is `animal_featured`. For pets, also check `animals_visible` has `dog`, `cat` or `other_pet` | | No talking | `is_silent` is `true` | `on_camera_presenter` means someone faces the camera and addresses it at some point, speaking or not. For stricter talking-head videos, where that is the main shot throughout, use `visual_format` `talking_head`. --- ## Pricing Data Intelligence adds **$1.00** to each run, however many videos it collects. | Agent type | Without | With Data Intelligence | When you pay | | - | - | - | - | | Runs once | $0.50 | **$1.50** | When you create it, even if the run later fails | | Recurring (runs on a schedule) | $0.50 per run | **$1.50** per run | After each run finishes, starting with the first | - **Recurring agents are free to create,** but creating one with Data Intelligence needs $1.50 in your balance. A run where some keywords or platforms failed (`partial_failure`) is still charged. A recurring run that fails outright is not charged. - **Recurring charges are automatic.** Check them in your balance and usage history ([Billing and pricing](https://dev.virlo.ai/docs/credits)). No API response reports them (no `X-Cost` header). - **Reading is free.** With Data Intelligence on, the agent's [hook list](https://dev.virlo.ai/docs/hooks#agent-hooks) is free too. Without it, each hook-list request costs $0.25, or nothing while the agent has no hooks yet. --- ## Turn it on Add `data_intelligence_enabled: true` when you [create an agent](https://dev.virlo.ai/docs/agents#create-agent). The first run starts right away. In every example on this page, replace `YOUR_API_KEY` with your [API key](https://dev.virlo.ai/docs/authentication). **cURL:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Skincare Routine Research", "is_recurring": false, "intent": "Find beginner glass-skin routines that name drugstore products, not dermatologist lectures", "keywords": ["glass skin routine", "skincare routine for beginners", "drugstore skincare routine"], "platforms": ["tiktok", "youtube"], "data_intelligence_enabled": true }' ``` A one-time agent can't run again, so turn it on at creation. A recurring agent can turn it on or off at any time with [Update agent](https://dev.virlo.ai/docs/agents#update-agent). The change applies from the next run, sets the price of future runs, and does not go back over earlier videos. **cURL:** ```bash curl -X PUT https://api.virlo.ai/v1/agents/{agent_id} \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data_intelligence_enabled": true }' ``` --- ## When fields are missing `intelligence_status` on each video and slideshow tells you why `intelligence` might be `null`: | `intelligence_status` | Meaning | What to do | | - | - | - | | `ready` | The fields are in. | Use them. | | `pending` | Data Intelligence is on, but the fields aren't in yet. | Check again later. If it is still `pending` the next day, it will probably never get them. It never turns into "failed". | | `skipped` | Virlo chose not to analyze this one. `intelligence_skip_reason` says why. | Stop checking. The fields will not arrive. | | `disabled` | Data Intelligence is off for this agent. | Turn it on for future runs, or ignore the field. | **Why a video is skipped.** `intelligence_skip_reason` is `null` unless the status is `skipped`: | `intelligence_skip_reason` | Meaning | | - | - | | `intent_mismatch` | The video does not fit the agent's intent (`intent_match` is `false`). | | `too_old` | It was posted more than a year ago. | | `under_followers` | It is more than two days old and got no more views than its creator has followers. | Each run's top 25 videos by views and top 50 by Virality Score are analyzed whatever their age or follower count, as long as they fit the intent. A skipped video is checked again on every run that finds it, so one that takes off later gets analyzed then. Slideshows follow the same rules, without the intent check. What to plan for: 1. **Not every video gets fields.** Often more than half of an agent's videos are `skipped`. That is by design: they are off-topic, over a year old, or got fewer views than their creator has followers. 2. **`finalized: true` does not wait for the fields.** The agent can report its run as finished ([Get agent](https://dev.virlo.ai/docs/agents#get-agent)) while videos are still `pending`. Read again later to pick them up. 3. **YouTube videos often lack the picture fields.** When the AI can't get a video's frames, it works from the caption and speech only. Every field that needs the picture is then `null`: format, setting, camera, lighting, on-screen text, faceless or on camera, products, animals and watermarks. It can hit more than half of an agent's YouTube videos. You can spot them because `low_confidence_fields` includes `tier3_unavailable`. 4. **Turned it on later?** Videos from earlier runs show `pending` and usually stay that way. **Details for developers** - **Single-value fields can be `null` in a `ready` block** when the AI could not tell, including many true/false fields. For example, `hook_type` is `null` when the video has no opening line at all. A weak opening line gets `none`. - **List fields are `[]`, never `null`.** `is_nsfw`, `is_sponsored`, `is_educational` and `is_multilingual` are never `null` either: they read `false` when nothing was found, so `false` can also mean "couldn't tell". - **`low_confidence_fields` can hold names that aren't fields,** like `inferred_region`. Ignore names you don't recognize. - **Videos flagged by Virlo's minor-safety check are dropped** from every list, and no field marks them. That is one reason a page can hold fewer videos than you asked for. - **Fields without Data Intelligence.** A video Virlo already reviewed for another agent can come back `ready` at no charge, even one this agent would have skipped. Don't count on it. - **Older videos.** Videos collected before October 2, 2026 can show `pending` where they would now show `skipped`. --- ## Intent matching Every agent has an `intent`: one plain sentence saying what you want to find. With Data Intelligence on, each video also gets `intent_match`, a yes or no answer to "does this video fit my intent?" - AI decides from the caption, hashtags and spoken words when the video is collected. It never looks at the picture. So write your intent about the topic ("honest reviews of drugstore moisturizers"), not the look ("talking head"), and check the look with the [fields](https://dev.virlo.ai/docs/intelligence#what-you-get). The [Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook) has more on writing intents. - `intent_match` can arrive before `intelligence`. Videos that get `false` are not analyzed: their `intelligence_status` is `skipped`, with `intelligence_skip_reason: "intent_mismatch"`. - Videos only. Slideshows don't get it. - It sits next to `intelligence` on the video, not inside it. It is `null` when Data Intelligence was off when the video was collected, or the check failed. **JSON:** ```json { "id": "b3a1f892-7c4e-4d8a-9f12-6e8b4a2c1d05", "url": "https://www.tiktok.com/@creator/video/7392847561023456789", "views": 2847000, "intelligence": { ... }, "intent_match": { "matches": true, "reasoning": "The caption 'my honest review after 30 days' and the transcript describe the creator testing a moisturizer and giving their own opinion, which fits the intent." } } ``` - `matches` (boolean, optional): `true` if the video fits your intent. - `reasoning` (string, optional): Why, usually one sentence quoting the caption, hashtags or transcript. Sometimes it is a machine string instead, like `jev subject=0.93 stuffing=0.05`, so check before showing it to clients. Don't parse it. ### Filter videos by intent match Add `intent_match=true` (or `false`) to [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos): ``` GET /v1/agents/:id/videos?intent_match=true ``` > **Note:** Good for browsing, not counting. It filters one page at a time. A page can be short or empty while later pages still have matches, and `total` counts only that page (with `limit=1`, usually `0`). To count matches, read every page without the filter and count `intent_match.matches` yourself, as [Example 1](https://dev.virlo.ai/docs/intelligence#end-to-end-examples) does. Ignore the `intent_summary` (`{ matched, total_evaluated }`) that some run-finished [webhooks](https://dev.virlo.ai/docs/webhooks) carry. It counts every video checked for the agent so far, not just that run. --- ## Use cases The code below is for your developer. Each snippet takes `videos`, the list from [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos) (`response['data']['videos']`), and skips videos with no fields yet. ### Brand safety filtering Drop risky videos before you repost creator content or place ads next to it. **Python:** ```python def is_brand_safe(v): intel = v.get('intelligence') if not intel: return False # not reviewed yet, so not confirmed safe topics = set(intel.get('sensitive_topics') or []) return ( intel.get('brand_safety_tier') in ('safe', 'low_risk') and not intel.get('is_nsfw') and topics <= {'none'} # nothing sensitive: the list is empty or only 'none' ) safe_videos = [v for v in videos if is_brand_safe(v)] ``` ### Which hooks show up most Count the most common kinds of opening line in your niche. Common is not the same as successful: to see which hooks get the most views, group by `hook_type` the way the next snippet groups by format. [Hooks](https://dev.virlo.ai/docs/hooks#hook-types-explained) explains each hook type. **Python:** ```python from collections import Counter hook_counts = Counter( v['intelligence']['hook_type'] for v in videos if v.get('intelligence') and v['intelligence'].get('hook_type') ) print(hook_counts.most_common(5)) ``` ### Which formats get the most views Compare one platform at a time, and use the median, not the average, so one viral hit doesn't skew a format. **Python:** ```python from statistics import median # (platform, format) -> view counts views = {} for v in videos: fmt = (v.get('intelligence') or {}).get('visual_format') if fmt: views.setdefault((v['platform'], fmt), []).append(v['views'] or 0) median_views = { key: median(counts) for key, counts in views.items() if len(counts) >= 5 # skip formats with too few videos to judge } # each platform, best format first for (platform, fmt), mid in sorted(median_views.items(), key=lambda kv: (kv[0][0], -kv[1])): print(platform, fmt, int(mid)) ``` ### Competitor brand mentions `brands_mentioned` comes from what is said and written. `brands_visible` comes from logos and products on screen. This counts each brand once per video. **Python:** ```python from collections import Counter brand_counts = Counter() for v in videos: intel = v.get('intelligence') or {} brands = set(intel.get('brands_mentioned') or []) | set(intel.get('brands_visible') or []) brand_counts.update(brands) print(brand_counts.most_common(10)) ``` ### Find one exact format: text-story videos Some formats are easier to find by look than by keyword. Text-story videos show a person, nobody talks, and more than 50 words of on-screen text tell the story. Use the agent's [intent](https://dev.virlo.ai/docs/intelligence#intent-matching) for the topic and these fields for the look. **Python:** ```python def is_text_story(v): intel = v.get('intelligence') or {} overlay = intel.get('text_overlay_content') or '' return ( intel.get('has_face_visible') is True and intel.get('is_silent') is True and len(overlay.split()) > 50 ) text_stories = [v for v in videos if is_text_story(v)] ``` --- ## End-to-end examples These use Python and the `requests` library. Every response wraps its results in a `data` object, which is why the code reads `['data']`. ### Example 1: Find talking-head product reviews A brand team wants reviews where a creator talks to the camera, for a user-generated content (UGC) campaign. `intent_match` checks the topic. The fields check the look. **Step 1: Create the agent.** This costs $1.50. **Python:** ```python import time import requests API = 'https://api.virlo.ai/v1' HEADERS = {'Authorization': 'Bearer YOUR_API_KEY'} agent = requests.post(f'{API}/agents', headers=HEADERS, json={ 'name': 'UGC Talking Head Reviews', 'is_recurring': False, 'intent': ( 'Honest product reviews where a creator tries a product and gives ' 'their own opinion, not paid ads or sponsored posts' ), 'keywords': ['honest product review', 'trying this product', 'is it worth it review'], 'platforms': ['tiktok', 'instagram'], 'data_intelligence_enabled': True, }).json() agent_id = agent['data']['id'] ``` **Step 2: Wait for the run to finish.** Check the agent again every 30 seconds (this is called polling) until `finalized` is `true`. Give up after about 45 minutes, or if the latest run's `status` is `failed`. **Python:** ```python deadline = time.time() + 45 * 60 # give up after 45 minutes while True: info = requests.get(f'{API}/agents/{agent_id}', headers=HEADERS).json()['data'] if (info.get('latest_run') or {}).get('status') == 'failed': raise RuntimeError('The run failed. Check the agent before trying again.') if info.get('finalized') is True: break if time.time() > deadline: raise TimeoutError('The run is still going after 45 minutes.') time.sleep(30) ``` **Step 3: Read every video.** Ask for 100 per page until a page comes back empty. A page can hold fewer than 100 even when more remain. **Python:** ```python def all_videos(agent_id): """Yield every video the agent collected, 100 per page.""" seen = set() page = 1 while True: batch = requests.get( f'{API}/agents/{agent_id}/videos', headers=HEADERS, params={'limit': 100, 'page': page}, ).json()['data']['videos'] if not batch: return for video in batch: if video['id'] not in seen: # pages can overlap, so skip videos we have already seen seen.add(video['id']) yield video page += 1 videos = list(all_videos(agent_id)) ``` Some videos can still show `intelligence_status: pending` right after the run. Run Steps 3 and 4 again later to pick them up. **Step 4: Keep the videos that fit.** **Python:** ```python def is_talking_head_review(v): intel = v.get('intelligence') or {} fits_intent = (v.get('intent_match') or {}).get('matches') is True return ( fits_intent and intel.get('presence_style') == 'on_camera_presenter' and intel.get('is_silent') is False and intel.get('is_sponsored') is False ) picks = [v for v in videos if is_talking_head_review(v)] print(f'{len(picks)} of {len(videos)} videos fit') for v in picks[:10]: print(v['url'], '|', v['intent_match']['reasoning']) ``` ### Example 2: Daily brand-safety audit A media buyer checks a fitness niche every day for videos that would be unsafe next to supplement ads. `cadence: 'daily'` with Data Intelligence costs $1.50 a day. **Python:** ```python agent = requests.post(f'{API}/agents', headers=HEADERS, json={ 'name': 'Daily Brand Safety Audit - Fitness', 'is_recurring': True, 'cadence': 'daily', 'intent': ( 'Everyday creators sharing their own fitness transformation or ' 'weight loss journey, not gym ads or supplement promos' ), 'keywords': ['fitness transformation', 'weight loss journey', 'body transformation progress'], 'platforms': ['tiktok', 'youtube'], 'data_intelligence_enabled': True, }).json() agent_id = agent['data']['id'] ``` After each run (when `last_run_at` changes or a webhook arrives, see Example 3), split the videos into safe and flagged. This checks every video collected so far. To report only new ones, skip IDs you already checked. It reuses `is_brand_safe` ([Brand safety filtering](https://dev.virlo.ai/docs/intelligence#brand-safety-filtering)) and `all_videos` (Example 1). **Python:** ```python safe, flagged = [], [] for v in all_videos(agent_id): if not v.get('intelligence'): continue # no fields yet if is_brand_safe(v): safe.append(v) else: flagged.append(v) print(f'Brand-safe: {len(safe)} Flagged: {len(flagged)}') for v in flagged[:10]: intel = v['intelligence'] print(v['url'], intel['brand_safety_tier'], intel['sensitive_topics']) ``` ### Example 3: Start work from a webhook A [webhook](https://dev.virlo.ai/docs/webhooks) lets Virlo tell your server when a run finishes, so you don't have to poll. React to `content_research_agent.run.completed`: **Python:** ```python from flask import Flask, request app = Flask(__name__) @app.post('/webhooks/virlo') def handle_webhook(): payload = request.json if payload['event'] == 'content_research_agent.run.completed': # Reply fast. Do the slow work (reading videos) in a background job. schedule_intent_review(payload['data']) return '', 200 ``` `schedule_intent_review` is your own code that reads and filters videos as in Example 1. The agent's ID is not at the top of `data`, and where it sits depends on how the run ended. [Agent run finished](https://dev.virlo.ai/docs/webhooks#agent-run-payload) shows the one-line lookup. The webhook can arrive before every video's fields are in, so read again later if many are still `pending`. --- ## Field reference All 79 fields sit in the `intelligence` object on a video. `enum` means one value from a fixed list (see [Allowed values](https://dev.virlo.ai/docs/intelligence#enum-values)), `object` is a small record, and `[]` means a list. See [When fields are missing](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing) for when a field is `null`. **All 79 video fields** ### Content - `primary_topic` (string, optional): Main topic. - `secondary_topics` (string[], optional): Other topics it covers. - `keywords` (string[], optional): Keywords that describe the content. - `category` (string, optional): Broad subject area, like `beauty`. Usually one of the [28 listed values](https://dev.virlo.ai/docs/intelligence#enum-values). - `content_format` (string, optional): Kind of content, like `tutorial` or `review`. Usually one of the [listed values](https://dev.virlo.ai/docs/intelligence#enum-values). Virlo adds new formats over time. ### Visual - `visual_format` (enum, optional): Main visual style, like `talking_head` or `screen_recording`. - `visual_complexity` (enum, optional): How busy the picture is. - `camera_perspective` (enum, optional): Camera angle and position. - `setting` (enum, optional): Where it was filmed. - `lighting_quality` (enum, optional): Lighting quality. - `has_face_visible` (boolean, optional): A human face is visible. - `scene_changed` (boolean, optional): The scene changes during the video. - `background_type` (enum, optional): What the background is. - `background_reasoning` (string, optional): Why it chose that `background_type`. - `foreground_type` (enum, optional): What the main subject in front is. - `foreground_reasoning` (string, optional): Why it chose that `foreground_type`. ### Hook and on-screen text - `hook_text` (string, optional): The opening line, spoken or on screen. - `hook_type` (enum, optional): Kind of hook, like `question` or `bold_claim`. Virlo adds new hook types over time. [`/v1/hooks/types`](https://dev.virlo.ai/docs/hooks#hook-taxonomy-stats) lists every type that has videos. - `visual_hook_type` (enum, optional): What the opening shot shows. - `has_text_overlay` (boolean, optional): Text is shown on screen. - `text_overlay_purpose` (enum, optional): What the main on-screen text is for. - `text_overlay_content` (string, optional): The main on-screen text, word for word. ### Captions and transcript - `has_onscreen_captions` (boolean, optional): Burned-in captions are shown. - `caption_style` (enum, optional): Style of those captions. - `transcript_quality` (enum, optional): How clean the transcript of the spoken words is. - `transcript_word_count, transcript_character_count` (integer, optional): Size of the transcript. `0` when nobody speaks or no transcript was made. To read the transcript itself, add `include_transcript=true` to [Get videos](https://dev.virlo.ai/docs/agents#agent-video-transcripts). - `language_detected` (string, optional): Two-letter language code, like `en`. - `is_multilingual` (boolean, optional): More than one language is spoken. ### Tone - `emotional_tone` (enum, optional): Main mood. - `sentiment` (enum, optional): Positive, negative, neutral or mixed. - `speaking_style` (enum, optional): How the speaker talks. ### Brand safety - `brand_safety_tier` (enum, optional): How safe it is for brands, from `safe` to `unsafe`. - `is_nsfw` (boolean, optional): Not safe for work. - `sensitive_topics` (enum[], optional): Sensitive topics found. `["none"]` when there are none. - `is_sponsored` (boolean, optional): Sponsored content. - `brands_mentioned` (string[], optional): Brands named in what is said or written. ### Engagement tactics - `cta_usages` (object[], optional): Calls to action, each `{ type, text }`. `text` is the exact wording. - `social_proof_used` (enum[], optional): Social proof used, like testimonials or statistics. - `trend_references` (string[], optional): Trends or challenges it refers to. ### Summary - `summary` (string, optional): Short summary, usually 2 or 3 sentences. - `is_educational` (boolean, optional): The video mainly teaches something. - `low_confidence_fields` (string[], optional): Fields the AI was unsure about. `tier3_unavailable` here means the frames were not analyzed. ### People on screen Read from the picture. These describe how people **appear**, not who they are, and are empty or `null` when nobody is detected. - `people` (object[], optional): One record per person: `{ role, is_synthetic, apparent_gender, apparent_age_bracket, apparent_age_min, apparent_age_max, confidence }`. - `presence_style` (enum, optional): Faceless or on camera. Combines `on_screen_presence` with whether anyone speaks. Use this one for faceless filters. - `on_screen_presence` (enum, optional): What the camera shows of a person, from the picture only. - `people_source` (enum, optional): How `people` was worked out: from frames, from the words, or not at all. - `people_count_bucket` (enum, optional): Roughly how many people appear. - `primary_subject_gender` (enum, optional): Apparent gender of the main person. - `primary_subject_age_bracket` (enum, optional): Apparent age bracket of the main person. - `primary_subject_age_min, primary_subject_age_max` (integer, optional): Estimated age range of the main person. - `primary_subject_confidence` (enum, optional): How sure the AI is about the main person. - `genders_present` (enum[], optional): Every apparent gender on screen. - `age_brackets_present` (enum[], optional): Every apparent age bracket on screen. - `subject_gender_skew` (enum, optional): Overall gender mix. - `has_minor_visible` (boolean, optional): Someone who appears under 18 is on screen. - `has_real_person` (boolean, optional): A real, not AI-made, person appears. - `has_baby_visible` (boolean, optional): A baby (0 to 3) appears. - `has_senior_visible` (boolean, optional): Someone 65 or older appears. ### AI use Whether the video was **made** with AI. A tutorial about ChatGPT is not AI-made. - `ai_provenance` (enum, optional): How much of the video is AI-made, from `none` to `fully_ai`. - `ai_elements` (enum[], optional): Each AI-made part found. Videos can mix them, like a real creator with an AI voiceover. - `ai_confidence` (enum, optional): How sure the AI-use answer is. - `ai_reasoning` (string, optional): Why it chose that `ai_provenance`. ### Brands and products on screen From the **picture**, so they catch a logo nobody mentions. - `brands_visible` (string[], optional): Brand names and logos seen in frame. - `product_presence` (enum, optional): Whether a product is the subject, and whether it is branded. - `ip_characters` (string[], optional): Known characters, like franchise characters or mascots. ### Animals - `animals_visible` (enum[], optional): Kinds of animal on screen. - `animal_presence` (enum, optional): Whether an animal is the subject or just in the background. - `has_animal` (boolean, optional): Any animal appears. ### Screen and audio - `screen_content_type` (enum, optional): What a recorded screen shows. `null` unless a screen fills much of the picture. - `is_silent` (boolean, optional): Nobody speaks (text or music only). ### Edit structure - `is_compilation` (boolean, optional): Unrelated clips stitched together. - `is_ranking` (boolean, optional): A ranking or tier list, like "top 5". - `is_repost` (boolean, optional): Reuploaded content rather than original. - `is_before_after` (boolean, optional): A before-and-after transformation. - `is_layered_composition` (boolean, optional): Visual layers stacked, like a reaction over a clip. - `has_platform_watermark` (boolean, optional): A platform or editing-app watermark is visible. - `watermark_source` (string, optional): Where the watermark is from, like `tiktok` or `capcut`. `presence_style`, `subject_gender_skew`, `ai_provenance` and the `has_*` people and animal fields are calculated from the other fields, so they always agree with them. Filter on these instead of digging through `people`. --- ## Slideshow intelligence Slideshows are photo carousels, mostly from TikTok. Each gets its own `intelligence` block with **70 fields** and the same `intelligence_status` values as videos. They drop 16 video fields about motion, camera work and speech, and add 7 about the slides, which Virlo calls panels. The other 63 fields work as on videos. - `narrative_arc` (enum, optional): How the panels tell the story, like `listicle`, `tutorial_steps` or `before_after`. - `text_density` (enum, optional): Whether text or images carry the post: `text_dominant`, `balanced` or `image_dominant`. - `image_count` (integer, optional): Panels analyzed, up to 10. The slideshow's `images` list can hold more. - `panel_texts` (string[], optional): The text on each panel, in order. - `panel_text_full` (string, optional): All panel text in one string, labeled `Panel 1: ...`, `Panel 2: ...`. Can be `null`. - `panel_text_word_count, panel_text_character_count` (integer, optional): Words and characters in `panel_text_full`. Video fields slideshows don't have: `visual_format`, `camera_perspective`, `lighting_quality`, `visual_complexity`, `scene_changed`, `visual_hook_type`, `has_text_overlay`, `text_overlay_content`, `text_overlay_purpose`, `has_onscreen_captions`, `caption_style`, `transcript_quality`, `transcript_word_count`, `transcript_character_count`, `speaking_style`, `is_layered_composition`. Slideshows don't get `intent_match`, so the `intent_match` filter does nothing on [Get slideshows](https://dev.virlo.ai/docs/agents#get-agent-slideshows). **Example slideshow intelligence (shortened)** This leaves out the people, AI use, brands on screen, animals, screen and edit structure fields. Real slideshows include them. **JSON:** ```json { "intelligence_status": "ready", "intelligence_skip_reason": null, "intelligence": { "primary_topic": "Minimal morning skincare routine for oily skin", "secondary_topics": ["niacinamide benefits", "SPF layering"], "keywords": ["skincare", "oily skin", "morning routine"], "category": "beauty", "content_format": "tutorial", "narrative_arc": "tutorial_steps", "text_density": "balanced", "image_count": 5, "panel_texts": [ "Stop wasting money on a 12-step routine", "Step 1: gentle cleanser", "Step 2: niacinamide serum", "Step 3: lightweight moisturizer", "Step 4: SPF 50" ], "panel_text_full": "Panel 1: Stop wasting money on a 12-step routine\n\nPanel 2: Step 1: gentle cleanser\n\nPanel 3: Step 2: niacinamide serum\n\nPanel 4: Step 3: lightweight moisturizer\n\nPanel 5: Step 4: SPF 50", "panel_text_word_count": 33, "panel_text_character_count": 185, "language_detected": "en", "is_multilingual": false, "emotional_tone": "inspiring", "sentiment": "positive", "has_face_visible": false, "background_type": "real_world_photo", "background_reasoning": "Product photos on a bathroom counter fill every panel.", "foreground_type": "real_photo_subject", "foreground_reasoning": "Each panel centers one skincare product with a text caption.", "setting": "indoor_bathroom", "hook_text": "Stop wasting money on a 12-step routine", "hook_type": "bold_claim", "brand_safety_tier": "safe", "is_nsfw": false, "sensitive_topics": ["none"], "is_educational": true, "is_sponsored": false, "brands_mentioned": ["CeraVe", "La Roche-Posay"], "cta_usages": [ { "type": "link_in_bio", "text": "products linked below" } ], "trend_references": ["skin minimalism"], "social_proof_used": [], "summary": "Four-step morning routine for oily skin, presented as a numbered carousel with affordable product picks.", "low_confidence_fields": [] } } ``` --- ## Allowed values A field typed `enum` takes one value from these lists. New values may be added, so handle ones you don't recognize. New `hook_type` and `content_format` values can appear at any time. `category` and `content_format` are looser: the AI usually picks from their lists, but other values can appear. **All allowed values** ### category `art_design`, `automotive`, `beauty`, `business_career`, `crafts_diy`, `education`, `entertainment`, `fashion`, `finance`, `fitness`, `food_beverage`, `gaming`, `health_wellness`, `home_garden`, `kids_content`, `lifestyle`, `music`, `news_politics`, `parenting_family`, `pets`, `real_estate`, `relationships_dating`, `religion_spirituality`, `science_nature`, `sports`, `tech`, `travel`, `other` ### content_format `tutorial`, `storytime`, `rant`, `review`, `comedy_bit`, `motivational`, `news_commentary`, `news_report`, `reaction`, `listicle`, `challenge`, `day_in_life`, `q_and_a`, `explainer`, `educational_breakdown`, `hot_take`, `unboxing`, `transformation`, `commentary_voiceover`, `grwm_routine`, `haul_restock`, `cooking_recipe`, `workout_demo`, `meditation`, `asmr`, `time_lapse`, `skit_sketch`, `lip_sync_dance`, `trend_performance`, `prank`, `experiment`, `street_interview`, `reddit_reading_storytime`, `silent_aesthetic`, `location_showcase`, `travel_guide`, `gameplay_commentary`, `gameplay_lets_play`, `stream_highlight`, `podcast_clip`, `interview_clip`, `ranking`, `compilation`, `tier_list`, `documentary_short`, `fancam_edit`, `testimonial`, `product_demo`, `ad_creative`, `pov_scenario`, `quiz_or_test`, `other` ### background_type `solid_color`, `illustrated_scene`, `real_world_photo`, `digital_screen_capture` A phone filmed on a desk is `real_world_photo`. Only direct screenshots are `digital_screen_capture`. ### foreground_type `text_only`, `real_photo_subject`, `illustrated_subject`, `meme`, `chart_or_data_ui`, `none_or_minimal` ### hook_type `question`, `bold_claim`, `shock_statement`, `story_tease`, `tutorial_promise`, `controversy`, `before_after`, `pov_setup`, `statistic`, `direct_address`, `trend_reference`, `cliffhanger`, `negation`, `relatable_scenario`, `comparison`, `mystery_setup`, `none` ### speaking_style `conversational`, `formal`, `hype_energy`, `whisper_asmr`, `voiceover_narration`, `comedic`, `storytelling`, `instructional`, `monotone`, `aggressive`, `deadpan`, `shouting`, `flirty` ### emotional_tone `funny`, `inspiring`, `shocking`, `educational`, `controversial`, `heartwarming`, `wholesome`, `angry`, `sad`, `hype`, `calm`, `sarcastic`, `nostalgic`, `cringe`, `dark_humor`, `urgent`, `mysterious`, `relatable`, `playful`, `neutral` ### sentiment `positive`, `negative`, `neutral`, `mixed` ### cta_usages type `follow`, `subscribe`, `like_video`, `comment`, `share`, `save_post`, `tag_friend`, `link_in_bio`, `visit_website`, `dm_message`, `buy`, `pre_order`, `download`, `sign_up`, `use_code`, `enter_giveaway`, `vote`, `book_appointment` ### social_proof_used `testimonial`, `before_after_results`, `statistic_cited`, `celebrity_mention`, `expert_endorsement`, `popularity_claim`, `user_count`, `award_mention` ### sensitive_topics `mild_profanity`, `strong_profanity`, `violence_described`, `drug_reference`, `alcohol`, `tobacco_vaping`, `gambling`, `controversial_politics`, `religious_discussion`, `mental_health`, `eating_disorders`, `self_harm_reference`, `sexual_content`, `hate_speech`, `medical_claims`, `financial_advice`, `weapons`, `none` `["none"]` means nothing sensitive was found. `none` can also appear next to other values, so check for anything other than `none`. ### brand_safety_tier `safe`, `low_risk`, `medium_risk`, `high_risk`, `unsafe` ### transcript_quality `clean`, `partial`, `garbled` ### visual_format `talking_head`, `pov_footage`, `interview`, `street_interview`, `screen_recording`, `text_messaging_thread`, `slideshow_text`, `animation_motion_graphics`, `b_roll_montage`, `whiteboard_presentation`, `vlog_handheld`, `activity_demonstration`, `product_closeup`, `green_screen_commentary`, `split_screen_duet`, `gameplay_background`, `native_gameplay`, `stream_overlay`, `pip_stream_layout`, `food_overhead`, `dance_full_body`, `outfit_showcase`, `clip_compilation`, `live_performance`, `event_footage`, `music_video`, `other` ### visual_complexity `minimal_clean`, `moderate`, `busy` ### caption_style `standard_subtitles`, `animated_word_by_word`, `large_bold_centered`, `karaoke_highlight`, `meme_top_bottom` ### camera_perspective `selfie_front`, `rear_camera`, `tripod_static`, `handheld_moving`, `overhead_topdown`, `drone_aerial` ### setting `indoor_home_general`, `indoor_bedroom`, `indoor_bathroom`, `indoor_kitchen`, `indoor_living_room`, `indoor_studio`, `indoor_gym`, `indoor_office`, `indoor_classroom`, `outdoor_urban`, `outdoor_nature`, `outdoor_beach`, `outdoor_pool`, `car`, `vehicle_other`, `restaurant_cafe`, `store_retail`, `event_venue`, `studio_set`, `indoor_salon_spa`, `indoor_medical_clinic`, `indoor_workshop_garage`, `outdoor_rural_farm`, `sports_venue`, `stage_performance`, `construction_site`, `place_of_worship`, `generic` ### lighting_quality `professional`, `cinematic`, `natural_good`, `golden_hour`, `natural_dim`, `ring_light`, `harsh_artificial`, `mixed`, `low_quality` ### visual_hook_type `text_hook`, `extreme_closeup`, `before_state`, `shocking_image`, `aesthetic_setup`, `person_speaking_to_camera`, `motion_action`, `crowded_scene`, `mystery_object`, `dramatic_zoom`, `animal_pet`, `product_reveal`, `none` ### text_overlay_purpose `title`, `list_item`, `statistic`, `quote`, `dialogue_label`, `chapter_marker`, `branding`, `cta`, `meme_caption`, `lyric`, `none` ### narrative_arc (slideshows) `listicle`, `story_progression`, `before_after`, `comparison`, `escalating_reveal`, `single_idea_expanded`, `q_and_a`, `meme_setup_punchline`, `tutorial_steps`, `screenshot_dump`, `none` ### text_density (slideshows) `text_dominant`, `balanced`, `image_dominant` ### people_source `visual` (from frames), `text` (guessed from the words when no frames were usable), `none` ### people_count_bucket `none`, `one`, `two`, `small_group_3_5`, `large_group_6_plus`, `crowd` ### on_screen_presence `face_presenting`, `face_not_presenting`, `body_no_face`, `hands_or_pov_only`, `animated_or_avatar_character`, `no_person` ### presence_style `on_camera_presenter`, `on_camera_subject`, `faceless_voiceover`, `faceless_silent`, `hands_or_pov`, `avatar_or_animated`, `no_person` ### apparent_gender Used by `people[].apparent_gender`, `primary_subject_gender` and `genders_present`: `female`, `male`, `ambiguous`. `ambiguous` means the AI looked and could not tell. ### subject_gender_skew `all_female`, `all_male`, `mixed`, `ambiguous`, `none` ### apparent_age_bracket Used by `people[].apparent_age_bracket`, `primary_subject_age_bracket` and `age_brackets_present`: `infant_0_3`, `child_4_12`, `teen_13_17`, `adult_18_24`, `adult_25_34`, `adult_35_49`, `adult_50_64`, `senior_65_plus`, `unknown` ### person role and confidence `people[].role`: `primary`, `secondary`, `background` `people[].confidence`, `primary_subject_confidence` and `ai_confidence`: `high`, `medium`, `low` ### ai_provenance `none`, `ai_assisted`, `ai_voice`, `ai_visuals`, `ai_presenter`, `fully_ai`, `unknown` From least to most AI-made. `ai_assisted`: AI only helped edit or caption. `ai_voice`: real footage, AI voice. `ai_visuals`: AI-made images or video, no AI presenter. `ai_presenter`: an AI avatar presents. `fully_ai`: footage, voice and presenter are all AI. ### ai_elements `synthetic_presenter`, `generated_imagery`, `generated_video`, `face_swap_or_clone`, `synthetic_voice`, `ai_captions_or_edit` ### product_presence `branded_product_featured`, `unbranded_product_featured`, `product_incidental`, `no_product` To find no-name products, use `unbranded_product_featured`. Empty brand lists can't tell you that. ### animals_visible `dog`, `cat`, `other_pet`, `bird`, `horse`, `livestock`, `wildlife`, `aquatic`, `other` ### animal_presence `animal_featured` (the video is about an animal), `animal_incidental`, `none` ### screen_content_type `gameplay`, `text_conversation`, `notes_or_document`, `social_feed`, `app_ui`, `website`, `video_playback`, `other` --- ## Full example A complete video from [Get videos](https://dev.virlo.ai/docs/agents#get-agent-videos) on an agent with Data Intelligence on. `publish_date` has no time zone marker. Read it as UTC. **Show the full video (JSON)** **Video with intelligence:** ```json { "id": "b3a1f892-7c4e-4d8a-9f12-6e8b4a2c1d05", "url": "https://www.tiktok.com/@glowbysara/video/7392847561023456789", "description": "the glass skin routine that changed my life, step by step for beginners #skincare #glassskin #routine", "platform": "tiktok", "views": 2847000, "likes": 341200, "shares": 89400, "comments": 12800, "bookmarks": 267000, "publish_date": "2026-04-22T14:30:00", "author": { "country": "US", "username": "glowbysara", "verified": true, "followers": 890000, "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/example.jpg" }, "hashtags": ["skincare", "glassskin", "routine"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/example.webp", "keyword_found_by": "glass skin routine", "is_duet": false, "is_stitch": false, "upload_region": "US", "upload_region_source": "tiktok_region", "intelligence": { "primary_topic": "beginner glass skin routine", "secondary_topics": ["glass skin", "drugstore skincare", "beginner skincare"], "keywords": ["glass skin", "double cleanse", "hyaluronic acid", "niacinamide", "SPF"], "category": "beauty", "content_format": "tutorial", "visual_format": "product_closeup", "visual_complexity": "moderate", "camera_perspective": "tripod_static", "setting": "indoor_bathroom", "lighting_quality": "ring_light", "has_face_visible": true, "scene_changed": true, "background_type": "real_world_photo", "background_reasoning": "The same bathroom vanity is behind the creator in every shot.", "foreground_type": "real_photo_subject", "foreground_reasoning": "The creator and the skincare products are the focus.", "hook_text": "the glass skin routine that changed my life", "hook_type": "bold_claim", "visual_hook_type": "before_state", "has_text_overlay": true, "text_overlay_purpose": "list_item", "text_overlay_content": "Step 1: double cleanse", "has_onscreen_captions": true, "caption_style": "animated_word_by_word", "transcript_quality": "clean", "transcript_word_count": 312, "transcript_character_count": 1680, "language_detected": "en", "is_multilingual": false, "emotional_tone": "inspiring", "sentiment": "positive", "speaking_style": "conversational", "brand_safety_tier": "safe", "is_nsfw": false, "sensitive_topics": ["none"], "is_sponsored": false, "brands_mentioned": ["CeraVe", "The Ordinary"], "cta_usages": [ { "type": "follow", "text": "follow for part 2" }, { "type": "save_post", "text": "save this routine" } ], "social_proof_used": ["before_after_results", "popularity_claim"], "trend_references": ["glass skin"], "summary": "Step-by-step beginner glass skin routine using drugstore products, from double cleansing to SPF. The creator talks to the camera in a bathroom and shows before and after results.", "is_educational": true, "low_confidence_fields": [], "people": [ { "role": "primary", "is_synthetic": false, "apparent_gender": "female", "apparent_age_bracket": "adult_18_24", "apparent_age_min": 20, "apparent_age_max": 26, "confidence": "high" } ], "people_source": "visual", "people_count_bucket": "one", "on_screen_presence": "face_presenting", "presence_style": "on_camera_presenter", "primary_subject_gender": "female", "primary_subject_age_bracket": "adult_18_24", "primary_subject_age_min": 20, "primary_subject_age_max": 26, "primary_subject_confidence": "high", "genders_present": ["female"], "age_brackets_present": ["adult_18_24"], "subject_gender_skew": "all_female", "has_minor_visible": false, "has_real_person": true, "has_baby_visible": false, "has_senior_visible": false, "ai_provenance": "none", "ai_elements": [], "ai_confidence": "high", "ai_reasoning": "Real creator speaking to camera. No AI-made imagery, voice or presenter detected.", "brands_visible": ["CeraVe", "The Ordinary"], "product_presence": "branded_product_featured", "ip_characters": [], "animals_visible": [], "animal_presence": "none", "has_animal": false, "screen_content_type": null, "is_silent": false, "is_compilation": false, "is_ranking": false, "is_repost": false, "is_before_after": true, "is_layered_composition": false, "has_platform_watermark": false, "watermark_source": null }, "intent_match": { "matches": true, "reasoning": "The caption and transcript walk through a beginner glass skin routine step by step and name drugstore products, which fits the intent." }, "sound": null, "intelligence_status": "ready", "intelligence_skip_reason": null } ``` --- # Creator lookup > Look up any creator on TikTok, YouTube Shorts, or Instagram. Start a lookup, get a job ID, then fetch their profile, stats, top hashtags, standout videos, and optional audience, trend analysis, and Data Intelligence add-ons. Source: https://dev.virlo.ai/docs/satellite See how any creator on TikTok, YouTube Shorts, or Instagram performs: their stats, top hashtags, standout videos, and who comments on their posts. **At a glance** - **What it does:** Sums up one creator (or up to 25 at once): profile, stats, top hashtags, and the videos that beat their usual views. - **You send:** A platform (`tiktok`, `youtube`, or `instagram`), the creator's handle, and any add-ons you want. - **You get back:** A job ID right away. You check that job every 15 seconds or so (this is called polling) until the result is ready. - **Cost:** $0.50 per creator, up to $1.75 with every add-on. Repeating it within 6 hours is free if the earlier lookup had every option you ask for. - **How long:** Usually 20 to 40 seconds. Bigger lookups and add-ons can take a few minutes. > **Note:** Every address here starts with `https://api.virlo.ai/v1/satellite/`. Successful answers sit inside a `data` field (the job ID is `data.job_id`). Error messages don't. --- ## What it costs You pay when you start a lookup. Checking status and re-reading saved results are free. | What you ask for | Price | | - | - | | Creator lookup | $0.50 | | Trend analysis (`trend_analysis=true`) | + $0.50 | | Data Intelligence, an AI breakdown of every video (`data_intelligence=true`) | + $0.25 | | Audience breakdown (`audience_demographics` and/or `audience_geography`) | + $0.50 once, even for both. Free if Virlo measured this creator's audience in the last 30 days. | | Every add-on at once | Up to $1.75 | | The same creator again within 6 hours | Free if the earlier lookup had every option you ask for. Batches too. | | Batch lookup | $0.50 per creator that starts a lookup, plus any audience fees | - **Failed lookups are refunded in full**, such as a handle that doesn't exist, a private account, a YouTube channel with no Shorts, 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 a creator lookup's Data Intelligence fee when there were no posts to enrich or its analysis failed. The refund comes later, as [`credits_refunded`](https://dev.virlo.ai/docs/satellite#poll-lookup-status) on the status check and on the [saved run](https://dev.virlo.ai/docs/satellite#durable-runs). - **The exact charge is in the response headers:** `X-Cost` in dollars and `X-Credits-Used` in credits (1 credit = $0.01, so $0.50 = 50 credits). --- ## Start a creator lookup **Endpoint:** `GET https://api.virlo.ai/v1/satellite/creator/:platform/:username` Starts a lookup and returns a job ID in about a second, while the work runs in the background. Each new lookup is charged, even though it's a `GET` request. Cost: $0.50, up to $1.75 with every add-on. Free if repeated within 6 hours. **Repeated within 6 hours by anyone on your team?** You get the same job ID back, already `completed` and marked `"cached": true`. This only happens when the earlier lookup had every add-on and include you ask for, at least as many videos (`max_videos`), and the same `outlier_threshold`. Otherwise a new paid lookup runs. A cached repeat costs nothing, but your balance must still cover the normal price. ### Path parameters - `platform` (string, required): `tiktok`, `youtube`, or `instagram`, in lowercase. YouTube reads Shorts only, so a channel with only long videos fails (and is refunded). Instagram reads the whole feed, but photos and carousels have no views, so view stats and outliers use only the videos. - `username` (string, required): The handle, with or without the `@`. It can't contain `/`, `?`, `#`, `%`, or spaces. ### Query parameters All optional. To turn an option on, send lowercase `true`. `True`, `1`, or `yes` count as off, with no error. A misspelled option name returns `400` and isn't billed. - `include` (string, optional): `videos` (the video list), `outliers` (videos that beat the creator's usual views), or both, comma-separated. Without it you get the profile, stats, and hashtags. - `max_videos` (integer, optional): How many recent videos to analyze, from 1 to 100 (default `20`). It doesn't change the price. - `outlier_threshold` (number, optional): A video is an outlier when its views are more than this many times the creator's median views. Default `2` (double), minimum `0.1`. Use `3` or `5` to see only the biggest hits. - `cross_links` (boolean, optional): Find the creator's profiles on other platforms (YouTube, TikTok, Instagram, X, Spotify). Free. - `trend_analysis` (boolean, optional): **+$0.50.** The formats the creator keeps repeating. Reads up to 300 videos and ignores `max_videos`. Includes the video list. See [Creator trend analysis](https://dev.virlo.ai/docs/satellite#creator-trend-analysis). - `data_intelligence` (boolean, optional): **+$0.25.** An AI breakdown of every video, plus a "what's working" summary. Includes the video list. The results land on the [saved run](https://dev.virlo.ai/docs/satellite#durable-runs) a few minutes after the lookup. See [Data Intelligence](https://dev.virlo.ai/docs/satellite/creators#data-intelligence) for an example. - `context` (string, optional): Only with `data_intelligence`. Up to 600 characters on what you want to learn. The summary focuses on it. - `slideshow_sort` (string, optional): Only with `data_intelligence`. Virlo reads the slide text of up to 100 slideshows. `popular` (default) or `recent` picks which ones. - `audience_demographics` (boolean, optional): **+$0.50 for a new snapshot.** Age, gender, and language of the people who comment, not every follower. Refunded if no new snapshot was needed, or if it fails or is a profile-only guess. - `audience_geography` (boolean, optional): Countries and cities of the same commenters. Uses the same snapshot and fee as `audience_demographics`. - `freshness_days` (integer, optional): How old (0 to 365 days, default `30`) a saved audience snapshot can be before Virlo makes a new one. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/satellite/creator/tiktok/khaby.lame \ -H "Authorization: Bearer YOUR_API_KEY" \ -d include=videos,outliers \ -d cross_links=true \ -d max_videos=30 \ -d outlier_threshold=2.5 ``` **Response 200 Started:** ```json { "data": { "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "processing" } } ``` **Response 200 Cached:** ```json { "data": { "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "completed", "cached": true } } ``` **Response 402:** ```json { "statusCode": 402, "message": "Insufficient balance. $0.50 required, $0.30 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 50, "remaining_credits": 30, "required_amount": "$0.50", "remaining_balance": "$0.30", "code": "insufficient_credits" } ``` **Response 429:** ```json { "statusCode": 429, "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "error": "Too Many Requests", "limit": 5, "remaining": 0, "reset_at": 1790270520, "reset_at_formatted": "September 24th, 2026, 5:22 PM UTC", "retry_after": 15, "resetAt": 1790270520, "resetAtFormatted": "September 24th, 2026, 5:22 PM UTC", "retryAfter": 15 } ``` --- ## Creator trend analysis Add `trend_analysis=true` to see the formats a creator keeps repeating, when they ran, and whether each is growing or fading. It adds a `trends` object to the result. Without it, there's no `trends` key. - **`summary`** sums up the creator's playbook in a sentence or two. - **`result.trends.trends`** lists each format: `name`, `description`, `tactics`, `confidence` (0 to 1), `video_count`, `evidence_video_ids` (matching `id` in `videos`), and `time_windows` (the date ranges it ran, with view and engagement totals). - **`resurged`** is `true` when the creator dropped a format and brought it back. - **`momentum`** compares average views in the latest date range with the one before: `stronger` (15% or more up), `weaker` (15% or more down), or `similar`. It's `null` if the format ran only once. - **`status`** is `ok`, or `insufficient_corpus` when fewer than 10 videos could be read. The trend fee is refunded for `insufficient_corpus`. - **An empty list with `status: "ok"`** means no clear pattern. If `model_used` is also `null`, the analysis failed and the trend fee is refunded. You can run it again after the 6-hour window. - **Ignore** `analyzed`, `cost_usd`, and `tokens_used`. They describe Virlo's own AI cost, not your bill. Full example: [Creator lookup add-ons](https://dev.virlo.ai/docs/satellite/creators). --- ## Check lookup status **Endpoint:** `GET https://api.virlo.ai/v1/satellite/creator/status/:job_id` Shows the lookup's progress, then the full result once it's done. Check every 15 seconds, or whatever `retry_after_seconds` says. It's free. ### When is it done? - **Basic lookup:** stop when `status` is `completed` or `failed`. - **With an audience add-on:** stop when `finalized` is `true`. A failed snapshot never makes it `true`, so if an audience field still reads `processing` after 15 minutes, check its job at `pending_jobs[].poll_url`. It ends `completed` or `failed`, and a failed snapshot is refunded. - **With `data_intelligence`:** the AI breakdown isn't in this response. After `completed`, check the [saved run](https://dev.virlo.ai/docs/satellite#durable-runs) until `intelligence_status` is `ready`, `failed`, or `skipped`, usually a few minutes. Most take 20 to 40 seconds, more with a high `max_videos`. Trend analysis and busy periods can take a few minutes, so don't hard-code a timeout. `status` still `processing` after 15 minutes? It's delayed, not lost. A stuck lookup is marked `failed` on its saved run within a few hours, with no automatic refund. For help sooner, email with the job ID. This check works for 24 hours, then returns `404`. The result stays in the [saved run](https://dev.virlo.ai/docs/satellite#durable-runs) under the same ID. ### Path parameters - `job_id` (string, required): The job ID from the start call. ### What you get back - `status` (string, optional): `processing`, `completed`, or `failed`. - `result` (object, optional): The creator data, once `completed`. - `error` (string, optional): Why the lookup failed, if it did. - `credits_refunded` (integer, optional): Only on a failed lookup that was refunded: the credits paid back (1 credit = $0.01). - `pending_jobs` (array, optional): Work still running after the main result: here, only a new audience snapshot. See [pending_jobs](https://dev.virlo.ai/docs/async-data#pending-jobs). - `finalized` (boolean, optional): `true` once the lookup and any audience snapshot are done. A failed snapshot keeps it `false` (see above). ### What's in `result` - `username, platform` (string, optional): What you looked up. - `profile` (object, optional): Followers, bio, avatar, profile link, and content category. Missing if it couldn't be fetched. - `stats` (object, optional): Averages across the analyzed videos: views, median views, likes, comments, engagement rate, and how often they post. - `hashtags` (object[], optional): The top 10 hashtags by total views. - `content_tags` (string[], optional): 2 to 5 AI-written tags for the creator's niche, like `["asmr", "budget-meals"]`. - `videos` (object[], optional): The analyzed videos. With `include=videos`, `trend_analysis`, or `data_intelligence`. - `outliers` (object, optional): Videos that beat the creator's own median views. With `include=outliers`. - `cross_links` (object, optional): The creator's profiles on other platforms. With `cross_links=true`. - `audience_demographics, audience_geography` (object, optional): The audience snapshot, or `{ "status": "processing", "job_id": "..." }` while a new one is made. It appears here once ready. You can also get a [webhook](https://dev.virlo.ai/docs/webhooks) for `audience.snapshot.completed`. - `trends` (object, optional): Only with `trend_analysis=true`. See [Creator trend analysis](https://dev.virlo.ai/docs/satellite#creator-trend-analysis). - `run_id` (string, optional): Same as your job ID. Use it to [re-read the result](https://dev.virlo.ai/docs/satellite#durable-runs) for free. - `credits_charged` (number, optional): Not for billing. It counts only the base price and trend fee, not the Data Intelligence or audience fees. Your real charge is the `X-Credits-Used` header on the start call. - `credits_refunded` (integer, optional): Only when a trend or audience fee bought nothing: the credits paid back. `X-Cost` still shows the full charge. **Full field reference** Some fields exist only on some platforms. A missing value can come back as `null`, as `0`, or not at all, so handle all three. ### profile - `followers` (number, optional): Followers or subscribers. - `following` (number, optional): Accounts they follow. Always `0` on YouTube. - `avatar_url, url, description` (string | null, optional): Profile picture, profile link, and bio. - `is_verified` (boolean | null, optional): Verified badge. Often `null` on YouTube. - `total_videos, total_posts` (number | null, optional): Totals on the account, when the platform reports them. On TikTok they're equal. - `total_likes` (number | null, optional): TikTok only. - `total_views` (number | null, optional): YouTube only. - `category` (string | null, optional): Content category picked by AI, like `comedy`. `null` if unclear. - `bio_link` (string | null, optional): TikTok and Instagram only. The link in their bio. - `country` (string | null, optional): TikTok only. A two-letter code like `US`, or `null`. - `attribution` (string | null, optional): YouTube only. The link text on the channel header. ### stats - `videos_analyzed` (number, optional): Posts with a view count that went into the view math. - `posts_analyzed` (number, optional): All posts analyzed. Differs only on Instagram, where photos and carousels have no views. - `post_mix` (object, optional): Instagram mixed feeds only: how many `video`, `carousel`, and `image` posts were analyzed. - `avg_views, median_views` (number, optional): Average and median (middle) views per video. - `avg_likes, avg_comments` (number, optional): Average likes and comments per post. - `engagement_rate` (number, optional): Likes plus comments, divided by views, rounded to 2 decimals (`0.07` = 7%). Don't compare it across platforms. - `top_performing_views, lowest_performing_views` (number, optional): Highest and lowest views among the analyzed videos. - `posting_frequency_days` (number, optional): Average days between posts. Needs at least 2 dated posts. ### hashtags Each entry has `hashtag` (lowercase, no `#`), `total_views`, `avg_views`, `avg_likes`, `avg_comments`, and `used_count`. They come from titles and descriptions. ### videos Each video has `id`, `title`, `description`, `views`, `likes`, `comments`, `publish_date`, `url`, `hashtags`, `thumbnail_url`, `is_duet`, and `is_stitch` (always `false` off TikTok). Some keys only exist on some platforms: - `shares`, `bookmarks`, and `is_pinned`: TikTok only. - `duration` (seconds): TikTok and Instagram. Missing on YouTube. - `is_video` and `content_type` (`reel`, `video`, `carousel`, or `image`): Instagram only. Photos and carousels show `views: 0`. - `publishDate` and `isPinned` are old duplicates. Use `publish_date` and `is_pinned`. ### outliers `threshold_multiplier` is the threshold used. `outlier_videos` lists every video above it, sorted by `weighted_score`, highest first. Each has all the video fields, plus: - `outlier_ratio` (number, optional): Views divided by the creator's median views. `6.56` means 6.56 times their usual. - `weighted_score` (number, optional): `ln(outlier_ratio) × ln(median_views)`, always present. The same jump scores higher for a creator with bigger usual views, so use it to compare across creators. ### cross_links - `discovered` (object[], optional): Confident matches only, so it can be empty. Each is `{ platform, username, url, source }`. `platform` is `youtube`, `tiktok`, `instagram`, `twitter`, or `spotify`. `source` is where it was found: `bio_link`, `bio_text`, `linktree`, `spotify_api`, or `ai_search`. - `sources_checked` (string[], optional): Where Virlo searched. Includes `timeout` if the search hit its 8-second limit. You still get the links found so far. - `bio_link_url` (string, optional): The link-in-bio page checked, if any. May be missing. ### Audience snapshot Both audience fields read one snapshot, built from the creator's commenters (on TikTok, followers too when comments are thin). Each has `snapshot_id`, `sample_size` (people analyzed), `confidence_level` (`low`, `medium`, or `high`), `confidence_per_signal`, `data_source`, `signal_breakdown`, and `model_version`. - **`audience_demographics`** adds `snapshot_at`, `evidence_summary` (a short written summary), and `age_distribution`, `gender_distribution`, and `language_distribution`. Shares add up to about 1 (`0.46` = 46%). - **`audience_geography`** adds `country_distribution` and `city_distribution`: lists of `{ code, name, pct, confidence }`, largest first. The last entry groups places with only one person, `{ name: "Other (N countries)", pct }` (or `Other (N cities)`), with no `code` or `confidence`. - **`data_source: "profile_only"`** is a guess from the creator's own profile: always low confidence, no age or gender, and always refunded. See [Audience data](https://dev.virlo.ai/docs/tracking#audience-basics) for every source. **cURL request:** ```bash curl https://api.virlo.ai/v1/satellite/creator/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response Completed:** ```json { "data": { "status": "completed", "result": { "username": "khaby.lame", "platform": "tiktok", "stats": { "videos_analyzed": 30, "posts_analyzed": 30, "avg_views": 45000000, "median_views": 32000000, "avg_likes": 3200000, "avg_comments": 18000, "engagement_rate": 0.07, "top_performing_views": 210000000, "lowest_performing_views": 5000000, "posting_frequency_days": 3.2 }, "hashtags": [ { "hashtag": "comedy", "total_views": 890000000, "avg_views": 59333333, "avg_likes": 4100000, "avg_comments": 22000, "used_count": 15 } ], "content_tags": ["silent-comedy", "life-hacks", "reaction"], "profile": { "followers": 162400000, "following": 78, "url": "https://www.tiktok.com/@khaby.lame", "is_verified": true, "total_videos": 1203, "total_posts": 1203, "total_likes": 2400000000, "category": "comedy", "bio_link": null, "country": null }, "videos": [ { "id": "7234567890123456789", "title": "When people overcomplicate things...", "views": 85000000, "likes": 6200000, "comments": 31000, "shares": 120000, "bookmarks": 95000, "duration": 21, "publish_date": "2025-03-15T12:00:00.000Z", "url": "https://www.tiktok.com/@khaby.lame/video/7234567890123456789", "hashtags": ["comedy", "reaction"], "is_pinned": false, "is_duet": false, "is_stitch": true } ], "outliers": { "threshold_multiplier": 2.5, "outlier_videos": [ { "id": "7229876543210987654", "title": "Why make it hard?", "views": 210000000, "url": "https://www.tiktok.com/@khaby.lame/video/7229876543210987654", "outlier_ratio": 6.56, "weighted_score": 32.51 } ] }, "cross_links": { "discovered": [ { "platform": "youtube", "username": "khaby.lame", "url": "https://www.youtube.com/@khaby.lame", "source": "bio_link" } ], "sources_checked": ["structured_fields", "bio_text", "linktree", "spotify_api", "ai_search"] }, "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credits_charged": 50 }, "pending_jobs": [], "finalized": true } } ``` **Response Processing:** ```json { "data": { "status": "processing", "pending_jobs": [], "finalized": false } } ``` **Response Failed:** ```json { "data": { "status": "failed", "error": "No posts found for @username on tiktok. The account may not exist, be private, or have no public content.", "credits_refunded": 50, "pending_jobs": [], "finalized": true } } ``` **Response 404:** ```json { "message": "Creator lookup job not found or expired", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Re-read past runs Every lookup is saved as a "run" that you can read again for free, with no 24-hour limit. The run ID is the job ID you got at the start. Sound lookups, hashtag lookups, and [video outlier checks](https://dev.virlo.ai/docs/satellite/video-outlier) are saved the same way. A run shows the numbers from when it ran. For fresh numbers, start a new lookup, which is billed. > **Note:** **Every paid lookup is saved as its own run, with a new run ID.** Older runs are never overwritten. A repeat within the 6-hour free window returns the existing run instead. ### Get one run `GET /v1/satellite/runs/:run_id` returns the saved run. **Full field reference** - `run_id` (string, optional): Same as the job ID. - `type` (string, optional): `creator_lookup`, `sound_lookup`, `hashtag_lookup`, or `video_outlier`. - `platform, status` (string, optional): The platform, and `processing`, `completed`, or `failed`. - `subject` (string | null, optional): The handle, sound ID, or hashtag. `null` for video outliers. - `created_at` (string, optional): When the run was created. - `completed_at` (string | null, optional): When it finished. Use it to judge how fresh the data is. - `credits_used` (number | null, optional): Currently always `null`. The real charge is the `X-Credits-Used` header on the call that started the run. - `request` (object, optional): The options the lookup used, such as `data_intelligence`. - `result` (object | null, optional): The same `result` the status check returns. With `data_intelligence`, each video (and outlier) also has an `intelligence` object with 43 fields (35 for slideshows, see [Data Intelligence](https://dev.virlo.ai/docs/intelligence)) and its own `intelligence_status`: `ready`, `pending`, `failed`, `skipped` (no analysis for this video), or `disabled` (not turned on). A slideshow also has `slides`, a list of `{ image_url, position }`, where `image_url` is a storage file name, not a link. Its slide text is in `intelligence.panel_texts`. - `analysis` (object | null, optional): Only with `data_intelligence`: the "what's working" summary, or `null` until it's ready. It has `key_insight`, `what_works` (each with its `evidence_video_ids`), `winning_hooks`, `best_formats`, `top_subjects`, `relevance` (only if you sent `context`), and `degraded`. `degraded` is `true` when under 60% of the videos had their AI breakdown, so the summary leans on plain numbers. - `intelligence_status` (string | null, optional): Progress of the whole Data Intelligence job: `pending`, `ready`, `failed`, or `skipped`. `null` if it wasn't turned on for this run. - `error` (string | null, optional): Why the run failed, if it did. - `credits_refunded` (integer, optional): Credits paid back for this run: the whole charge if it failed, or an add-on fee that produced nothing. Only there when a refund was made. It stays on the run after the 24-hour status check expires. ### List your runs `GET /v1/satellite/runs` lists your team's runs, newest first. Each paid lookup is its own row, so a creator you looked up twice appears twice. Free. - `type` (string, optional): `creator_lookup`, `sound_lookup`, `hashtag_lookup`, or `video_outlier`. Creators from a batch show up as `creator_lookup`. - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. - `limit` (integer, optional): Runs per page, from 1. Default `25`. Anything above 100 counts as 100. - `offset` (integer, optional): How many runs to skip. Default `0`. Each row has `run_id`, `type`, `platform`, `status`, `subject`, `created_at`, `completed_at`, and `credits_used`. The response also has `total`, `limit`, and `offset`. ### Page through a run's videos `GET /v1/satellite/runs/:run_id/videos` returns a run's videos a page at a time. Free. - `limit` (integer, optional): From 1 to 200, default `25`. Above 200 returns `400`. - `offset` (integer, optional): How many videos to skip. Default `0`. - `sort` (string, optional): `recent` or `popular` (views, likes, comments, shares, and saves combined). Leave it out to keep the saved order. - `content_type` (string, optional): `all` (default), `video`, or `slideshow` (photo carousels). On TikTok, `slideshow` only finds carousels on `data_intelligence` lookups, and `video` currently returns carousels too. The response has `videos`, `total` (after the `content_type` filter), `limit`, `offset`, `sort`, and `content_type`. **One run request:** ```bash curl https://api.virlo.ai/v1/satellite/runs/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response One run:** ```json { "data": { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "creator_lookup", "platform": "tiktok", "status": "completed", "subject": "khaby.lame", "created_at": "2026-09-24T17:23:37.665014+00:00", "completed_at": "2026-09-24T17:24:05.374+00:00", "credits_used": null, "request": { "context": "which hooks drive their most-viewed videos", "max_videos": 30, "data_intelligence": true, "trend_analysis": false }, "result": { "username": "khaby.lame", "platform": "tiktok", "videos": [ { "id": "7234567890123456789", "views": 85000000, "intelligence_status": "ready", "intelligence": { "hook_type": "relatable_scenario", "content_format": "reaction", "is_sponsored": false, "brand_safety_tier": "safe" } } ], "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credits_charged": 50 }, "analysis": { "key_insight": "Silent reactions to overcomplicated life hacks drive his biggest videos.", "what_works": [ { "insight": "Reaction videos that undo a needlessly complex hack beat his median by 6x.", "evidence_video_ids": ["7229876543210987654"] } ], "winning_hooks": ["Relatable scenario: someone overcomplicating a simple task"], "best_formats": "Short silent reactions under 25 seconds.", "top_subjects": ["Life hacks", "Everyday tasks"], "relevance": "Lead with the overcomplicated version, then the simple fix.", "degraded": false }, "intelligence_status": "ready", "error": null } } ``` **Response List:** ```json { "data": { "runs": [ { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "creator_lookup", "platform": "tiktok", "status": "completed", "subject": "khaby.lame", "created_at": "2026-09-24T17:23:37.665014+00:00", "completed_at": "2026-09-24T17:24:05.374+00:00", "credits_used": null } ], "total": 1, "limit": 25, "offset": 0 } } ``` **Response Videos:** ```json { "data": { "videos": [ { "id": "7229876543210987654", "title": "Why make it hard?", "views": 210000000, "publish_date": "2025-02-10T08:00:00.000Z", "url": "https://www.tiktok.com/@khaby.lame/video/7229876543210987654", "intelligence_status": "ready" } ], "total": 30, "limit": 50, "offset": 0, "sort": "popular", "content_type": "all" } } ``` --- ## Batch creator lookup **Endpoint:** `POST https://api.virlo.ai/v1/satellite/creators/batch` Starts lookups for up to 25 creators in one request. You get a batch ID plus a job ID for each creator, and each creator gets the same result as a single lookup. The differences: - **No trend analysis or Data Intelligence.** Sending `trend_analysis`, `data_intelligence`, `context`, or `slideshow_sort` returns `400`. - **Free 6-hour repeats.** A creator your team looked up in the last 6 hours, with at least these options, comes back `completed` at once, free, with the saved result (same `job_id`). - **No 5-per-minute limit.** Batches don't count toward the limit on single lookups. Cost: $0.50 per creator that starts a lookup (10 creators = $5.00), plus $0.50 per creator that needs a new audience snapshot. Your balance must cover the whole batch, or it's rejected with `402` and nothing runs. A creator whose lookup fails, such as a handle that doesn't exist, is refunded like a single lookup. If Virlo can't start one creator's lookup (a problem on Virlo's side), it shows `"status": "failed"` with an empty `job_id`, and you aren't charged for it, audience fee included. ### Body parameters - `creators` (array, required): 1 to 25 entries like `{ "platform": "tiktok", "username": "khaby.lame" }`. Optional: `include`, `max_videos`, `outlier_threshold`, `cross_links`, `audience_demographics`, `audience_geography`, and `freshness_days`. They work as on a [single lookup](https://dev.virlo.ai/docs/satellite#start-creator-lookup) and apply to every creator. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/satellite/creators/batch \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "creators": [ { "platform": "tiktok", "username": "khaby.lame" }, { "platform": "youtube", "username": "mkbhd" }, { "platform": "instagram", "username": "therock" } ], "include": "videos,outliers", "cross_links": true, "max_videos": 30 }' ``` **Response 202 One cached:** ```json { "data": { "batch_id": "b7e1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e", "status": "partial", "total": 3, "completed": 1, "failed": 0, "processing": 2, "creators": [ { "platform": "tiktok", "username": "khaby.lame", "job_id": "3f2a9c1e-6b7d-4e8f-9a0b-1c2d3e4f5a6b", "status": "processing" }, { "platform": "youtube", "username": "mkbhd", "job_id": "8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f", "status": "completed" }, { "platform": "instagram", "username": "therock", "job_id": "d4c3b2a1-9f8e-4d7c-8b6a-5f4e3d2c1b0a", "status": "processing" } ] } } ``` **Response 400:** ```json { "message": ["property trend_analysis should not exist"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` --- ## Check batch status **Endpoint:** `GET https://api.virlo.ai/v1/satellite/creators/batch/:batch_id` Shows progress only: one status per creator, plus totals. For each creator's data, check its `job_id` with [Check lookup status](https://dev.virlo.ai/docs/satellite#poll-lookup-status). Free, for 24 hours. - `processing`: nothing has finished yet. - `partial`: some creators are done (or failed) and some are still running. - `completed`: nothing is still running. **Some creators may have failed**, so check the `failed` count. - `failed`: every creator failed. A creator can also show `expired` once its 24-hour record is gone. It counts as failed, but if it finished, its result is still in its saved run (its `job_id` is the [run ID](https://dev.virlo.ai/docs/satellite#durable-runs)). The batch itself isn't saved, and it doesn't show refunds: check each creator's job for `credits_refunded`. ### Path parameters - `batch_id` (string, required): The batch ID from the batch call. **cURL request:** ```bash curl https://api.virlo.ai/v1/satellite/creators/batch/b7e1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 One failed:** ```json { "data": { "batch_id": "b7e1c2d3-4f5a-4b6c-8d7e-9f0a1b2c3d4e", "status": "completed", "total": 3, "completed": 2, "failed": 1, "processing": 0, "creators": [ { "platform": "tiktok", "username": "khaby.lame", "job_id": "3f2a9c1e-6b7d-4e8f-9a0b-1c2d3e4f5a6b", "status": "completed" }, { "platform": "youtube", "username": "mkbhd", "job_id": "8c7d6e5f-4a3b-4c2d-9e1f-0a9b8c7d6e5f", "status": "completed" }, { "platform": "instagram", "username": "therock", "job_id": "d4c3b2a1-9f8e-4d7c-8b6a-5f4e3d2c1b0a", "status": "failed" } ] } } ``` **Response 404:** ```json { "message": "Batch not found or expired", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Errors - `400 Bad Request`: Something in the request is wrong. Not billed. - **Lookup:** an unknown platform (it must be lowercase), a handle with `/`, `?`, `#`, `%`, or spaces, `max_videos` outside 1 to 100, `outlier_threshold` below 0.1, `freshness_days` outside 0 to 365, `context` over 600 characters, or an unknown parameter. - **Batch:** no creators, more than 25, or an option batches don't support. - **Run endpoints:** a run ID that isn't a valid ID, a `limit` out of range, or an unknown `type`, `sort`, or `content_type`. - `401 Unauthorized`: The API key is missing or invalid. Send it as `Authorization: Bearer YOUR_API_KEY`. - `402 Payment Required`: Your balance is too low. The message says how much is needed. Add funds at [dev.virlo.ai/dashboard/billing](https://dev.virlo.ai/dashboard/billing). See [Billing and pricing](https://dev.virlo.ai/docs/credits). - `404 Not Found`: The job ID is unknown or older than 24 hours (read the [saved run](https://dev.virlo.ai/docs/satellite#durable-runs) instead), the batch or run ID doesn't exist, or the username is missing from the path. - `429 Too Many Requests`: Too many lookups started too fast. Single lookups allow 5 per minute, 100 per hour, and 1,000 per day, and rejected `400` requests count too. Status checks, batches, and run reads use the general daily limit. Wait `retry_after` seconds. Ignore the old duplicates (`resetAt`, `resetAtFormatted`, `retryAfter`) and use the underscore names. See [Rate limits](https://dev.virlo.ai/docs/rate-limits). **A creator that can't be found isn't an error response.** The start call succeeds and is billed $0.50. The status check then returns `failed` with `No posts found for @username on tiktok...` (on YouTube, no Shorts found), and the charge is refunded. Fix the handle and start again. With an audience add-on, the lookup doesn't fail: it completes with empty stats and just the audience data. --- ## Other lookups The same start, check, and re-read flow works for: - **[Sound lookup](https://dev.virlo.ai/docs/satellite/sounds):** the TikTok videos or Instagram reels using a sound, with top creators, hashtags, and how fast it's spreading. $0.50, or $1.00 with trend analysis. No YouTube. - **[Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags):** videos posted under a hashtag on TikTok, Instagram, or YouTube Shorts, with top creators, related hashtags, and top sounds. $0.50, up to $2.50 with `depth=full` and trend analysis. - **[Video outlier check](https://dev.virlo.ai/docs/satellite/video-outlier):** whether one video beat its creator's usual views. $0.50 per check. --- # 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 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" ``` **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. `model_used`, `cost_usd` and `tokens_used` are Virlo's AI costs, not your bill. **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" } ], "model_used": "gemini-2.5-flash", "cost_usd": 0.0123, "tokens_used": 18234, "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 a `null` `model_used`, that the AI step failed and the trend fee was refunded. - `trends.model_used` (string | null, optional): `gemini-2.5-flash`, or `anthropic/claude-haiku-4.5` as a backup. - `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": [], "model_used": null, "cost_usd": null, "tokens_used": null, "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" ``` **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). --- # Video outlier check > Check whether one TikTok, YouTube or Instagram video beat its creator's usual views. Start a check for $0.50, then come back for an outlier score, a percentile and a plain label such as viral. Source: https://dev.virlo.ai/docs/satellite/video-outlier Find out whether a video is a real **outlier**, meaning it beat its creator's usual views, or only looks big because the creator is big. **At a glance** - **What it does:** Tells you if one TikTok, YouTube or Instagram video is a real breakout for its creator. - **You send:** The video's link and its platform. - **You get back:** A job ID right away, then the video's stats, the creator's profile, an outlier score and a verdict such as `viral`. - **Cost:** $0.50 per check, charged at the start, even if the check fails. Status checks and saved results are free. - **How long:** Usually 15 to 60 seconds, a few minutes at busy times. It's one of Virlo's [deep-dive lookups](https://dev.virlo.ai/docs/satellite), under `/v1/satellite/`. --- ## Start a video outlier check **Endpoint:** `POST https://api.virlo.ai/v1/satellite/video-outlier` Returns a `job_id` in about a second. Virlo then compares the video with up to 100 of the creator's recent videos. Cost per request: $0.50 Each request is billed, even for a link you already checked. Creator, sound and hashtag lookups repeat free within 6 hours; this one doesn't. ### Body parameters Send only these two fields. Anything missing or extra returns a `400`. - `url` (string, required): The full link, including `https://`. It must match `platform`: Virlo won't catch a mismatch, and still charges. - `platform` (string, required): `tiktok`, `youtube` or `instagram`, lowercase. Limits: 5 starts a minute, 100 an hour, 1,000 a day. Status checks don't count. **Details for developers** - Limits reset on the clock: each minute, each hour and at midnight UTC. - Requests rejected with a `400` still count. - `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` describe the window closest to running out. A `429` adds `Retry-After` (seconds). See [Rate limits](https://dev.virlo.ai/docs/rate-limits). **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/satellite/video-outlier \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.instagram.com/reels/DVMBt9pE4ob/", "platform": "instagram" }' ``` **Response 201 Started:** ```json { "data": { "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "processing" } } ``` **Response 400 Bad input:** ```json { "message": [ "A valid platform is required: youtube, tiktok, or instagram." ], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 402 Low balance:** ```json { "statusCode": 402, "message": "Insufficient balance. $0.50 required, $0.30 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 50, "remaining_credits": 30, "required_amount": "$0.50", "remaining_balance": "$0.30", "code": "insufficient_credits" } ``` **Response 429 Too many:** ```json { "statusCode": 429, "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "error": "Too Many Requests", "limit": 5, "remaining": 0, "reset_at": 1790270580, "reset_at_formatted": "September 24th, 2026, 5:23 PM UTC", "retry_after": 38, "resetAt": 1790270580, "resetAtFormatted": "September 24th, 2026, 5:23 PM UTC", "retryAfter": 38 } ``` --- ## Check the status **Endpoint:** `GET https://api.virlo.ai/v1/satellite/video-outlier/status/:job_id` Free. Returns `processing` while the check runs, then `completed` with the result or `failed` with a reason in `error`. After 24 hours the job ID expires (`404`); the [saved run](https://dev.virlo.ai/docs/satellite/video-outlier#durable-runs) keeps the result. ### Path parameters - `job_id` (string, required): The `job_id` from the start call. ### How to read the result The result (`data.result`) has three parts: `video`, `creator` and `analysis`. In `analysis`, start with: - `outlier_score`: views divided by the creator's median views (half their recent videos got more, half fewer). `6.56` means about 6.6 times their usual. - `performance_label`: the verdict. | `performance_label` | `outlier_score` | | - | - | | `mega_viral` | 10 or more | | `viral` | 3 to under 10 | | `above_average` | 1.5 to under 3 | | `average` | Under 1.5, including flops | `percentile` is a good second check: `89` means it beat 89% of the creator's recent videos. New videos score low at first; judge them after a few days. **Full field reference** **`data`**: `result` and `run_id` (equal to your `job_id`) only when `completed`; `error` only when `failed`. **`result.video`**: `url`, `title`, `views`, `likes`, `comments` and `publish_date` (UTC, or `null`). Ignore `publishDate`, an old duplicate due for removal. **`result.creator`**: `username`, `platform` and `profile` (`null` if it couldn't load). `profile` holds `followers`, `following`, `avatar_url`, `url`, `description`, `is_verified`, `total_videos`, `total_likes`, `total_views`, `total_posts` and `category`, plus: - `bio_link` on TikTok and Instagram, or `attribution` on YouTube (the channel header's link text, such as `X`, not a URL). - `country`, a two-letter code, on TikTok only. - On YouTube, `is_verified` is always `null` and `following` always `0`. - Numbers a platform doesn't share come back `null` or `0`. Instagram often shows `0` for `total_videos` and `total_posts`. **`result.analysis`**: - `videos_analyzed`: up to 100, fewer if the creator has fewer videos or the platform returns fewer. At `0` there's no baseline: ignore the scores and label. - `median_views`, `avg_views`: the creator's median and mean views. - `weighted_score`: ln(`outlier_score`) × ln(`median_views`), `0` if the video didn't beat the median. It rewards beating a bigger baseline, so use it to rank videos. Not the same as the Virality Score `weighted_score` elsewhere, which uses followers. - `percentile`: 0 to 100, can have decimals. - `std_deviation`: how much the creator's views swing around the mean. - `z_score`: how unusual these views are, given that swing (`0` is the mean, negative is below). It stays low for creators with a few huge hits, even when `outlier_score` is high. **cURL request:** ```bash curl https://api.virlo.ai/v1/satellite/video-outlier/status/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response Completed:** ```json { "data": { "status": "completed", "result": { "video": { "url": "https://www.instagram.com/reels/DVMBt9pE4ob/", "title": "Japan is turning footsteps into electricity! Using piezoelectric tiles, every step you take generates a small amount of energy...", "views": 247862, "likes": 27368, "comments": 185, "publish_date": "2026-02-25T16:40:48.000Z", "publishDate": "2026-02-25T16:40:48.000Z" }, "creator": { "username": "notivion.ig", "platform": "instagram", "profile": { "followers": 178543, "following": 12, "avatar_url": "https://instagram.fsac1-2.fna.fbcdn.net/v/t51.82787-19/770751058_n.jpg", "url": "https://www.instagram.com/notivion.ig/", "description": "Small artist and animator", "is_verified": false, "total_videos": 0, "total_likes": null, "total_views": null, "total_posts": 0, "category": null, "bio_link": null } }, "analysis": { "videos_analyzed": 100, "median_views": 37779, "avg_views": 131447, "outlier_score": 6.56, "weighted_score": 19.83, "percentile": 89, "std_deviation": 319747, "z_score": 0.36, "performance_label": "viral" } }, "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` **Response Failed:** ```json { "data": { "status": "failed", "error": "Could not resolve Instagram author from video URL." } } ``` **Response 404 Not found:** ```json { "message": "Video outlier job not found or expired", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Wait for the result Check the status every 10 to 15 seconds, as below. Still `processing` after 10 minutes (rare, such as after a server restart)? Start a new check, at another $0.50. Or have Virlo notify your server when a check finishes or fails: use the `satellite.lookup.completed` [webhook](https://dev.virlo.ai/docs/webhooks) (`data.type` is `video_outlier`). **JavaScript:** ```js const API = 'https://api.virlo.ai/v1'; const headers = { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }; async function analyzeVideo(url, platform) { // 1. Start the check ($0.50) const start = await fetch(`${API}/satellite/video-outlier`, { method: 'POST', headers, body: JSON.stringify({ url, platform }) }); if (!start.ok) throw new Error(`Start failed: ${start.status}`); const jobId = (await start.json()).data.job_id; // 2. Check every 10 seconds until it finishes. Give up after 10 minutes. const deadline = Date.now() + 10 * 60 * 1000; while (Date.now() < deadline) { await new Promise((resolve) => setTimeout(resolve, 10_000)); const res = await fetch(`${API}/satellite/video-outlier/status/${jobId}`, { headers }); if (!res.ok) throw new Error(`Status check failed: ${res.status}`); const job = (await res.json()).data; if (job.status === 'completed') return job.result; if (job.status === 'failed') throw new Error(job.error ?? 'Check failed'); } throw new Error(`Still processing after 10 minutes (job ${jobId})`); } ``` **Python:** ```python import time import requests API = 'https://api.virlo.ai/v1' HEADERS = {'Authorization': 'Bearer YOUR_API_KEY'} def analyze_video(url, platform): # 1. Start the check ($0.50) start = requests.post( f'{API}/satellite/video-outlier', headers=HEADERS, json={'url': url, 'platform': platform} ) start.raise_for_status() job_id = start.json()['data']['job_id'] # 2. Check every 10 seconds until it finishes. Give up after 10 minutes. deadline = time.time() + 10 * 60 while time.time() < deadline: time.sleep(10) res = requests.get( f'{API}/satellite/video-outlier/status/{job_id}', headers=HEADERS ) res.raise_for_status() job = res.json()['data'] if job['status'] == 'completed': return job['result'] if job['status'] == 'failed': raise RuntimeError(job.get('error') or 'Check failed') raise TimeoutError(f'Still processing after 10 minutes (job {job_id})') ``` --- ## Re-read past runs Every check, failed ones too, is saved as a **run** that is free to read and never expires. Its ID is your `job_id`. - `GET /v1/satellite/runs?type=video_outlier`: your checks, newest first. - `GET /v1/satellite/runs/:run_id`: one check, with the verdict at `data.result.analysis`. Its top-level `analysis` is always `null` here. The creator's comparison videos aren't saved, so `/runs/:run_id/videos` returns an empty list. See [all run fields](https://dev.virlo.ai/docs/satellite#durable-runs). --- ## Errors Error responses aren't wrapped in `data` and are never charged. Check the top-level `code`, not the message. See [Errors](https://dev.virlo.ai/docs/errors). A `failed` check is different: it started, so it's charged. For example, the video is deleted or private, or its creator can't be found. Open the link in a logged-out browser first. - `400 Bad Request`: Bad `url` or `platform`, or an extra field. `message` is usually a list, sometimes a string. If it isn't about your input (like `Failed to analyze video`), the problem is on Virlo's side: retry shortly. - `401 Unauthorized`: No API key (`missing_api_key`), or a wrong or inactive one (`invalid_api_key`). - `402 Payment Required`: Balance under $0.50 (50 credits, at $0.01 each). [Add funds](https://dev.virlo.ai/dashboard/billing). - `404 Not Found`: The job ID is unknown, another account's, or over 24 hours old. Read the [saved run](https://dev.virlo.ai/docs/satellite/video-outlier#durable-runs). - `429 Too Many Requests`: Wait `retry_after` seconds, then retry. --- # Sound lookup > Pull a fresh sample of the videos using one TikTok or Instagram sound, with stats on views, creators, hashtags, and posting weeks. Add trend analysis to see the creative formats people make with it. Source: https://dev.virlo.ai/docs/satellite/sounds See who uses a TikTok or Instagram sound, which hashtags go with it, and how those videos perform. Add trend analysis to see the creative formats people make with it. **At a glance** - **What it does:** Pulls a fresh sample of the videos using one sound, straight from the app. Stats describe that sample, not every video ever made with the sound. - **You send:** The platform (`tiktok` or `instagram`) and the sound's ID from its link. - **You get back:** A job ID right away, then the sound, its videos (50 by default, about 300 with trend analysis), and stats. - **Cost:** $0.50 per lookup, or $1.00 with trend analysis. Checking progress and reopening results are free. - **How long:** Under 1.5 minutes, about 2 with trend analysis. Up to several minutes when busy. - **Use it when:** You need fresh data on one sound. For videos Virlo already has, [Sound videos](https://dev.virlo.ai/docs/sounds#sound-videos) is cheaper at $0.25. > **Note:** **TikTok and Instagram only.** YouTube doesn't list videos per sound, so the `youtube` platform, or a YouTube sound ID Virlo already knows, is rejected at no charge. > > Instagram shares less: share and save counts, duet and stitch flags, video country, and the sound's total uses come back as `0`, `false`, or `null`. Instagram views include Facebook plays. --- ## Start a sound lookup **Endpoint:** `GET https://api.virlo.ai/v1/satellite/sounds/:platform/:music_id` Starts the lookup and returns a `job_id` right away. Send it to [Check lookup status](https://dev.virlo.ai/docs/satellite/sounds#poll-sound-status) for the results. Cost per request: $0.50 **With trend analysis:** $1.00. **Repeats within 6 hours are free.** Asking for the same sound within 6 hours of a finished lookup returns the saved result, marked `cached: true`. Watch out: - **Adding trend analysis** to a lookup that ran without it starts a fresh $1.00 lookup. - **Repeating a lookup that is still running** starts a second, billed lookup. Keep checking the first `job_id`. - **A different `max_videos`** still returns the saved result as it was. - **After 6 hours**, a new lookup is billed and saved as [a new run](https://dev.virlo.ai/docs/satellite/sounds#durable-runs). Older runs stay. ### Path parameters - `platform` (string, required): `tiktok` or `instagram` (lowercase). - `music_id` (string, required): The number at the end of the sound's link, such as `7392134672372746241` in `tiktok.com/music/morning-coffee-7392134672372746241` or `instagram.com/reels/audio/`. From Virlo's [sound endpoints](https://dev.virlo.ai/docs/sounds), use `external_id` or `id`. An ID Virlo hasn't seen is looked up as-is and billed, so double-check it. ### Query parameters - `trend_analysis` (string, optional): `true` adds [trend analysis](https://dev.virlo.ai/docs/satellite/sounds#trends-block) for $0.50 more and keeps every video found (usually about 300, up to 500), ignoring `max_videos`. Only `true` or `1` turn it on; `True` or `yes` count as off. The extra $0.50 is refunded if no trends come out. - `max_videos` (integer, optional): Videos to keep and analyze, 1 to 100 (default `50`): the first ones in the platform's order, not the most viewed. Fewer is not faster or cheaper. Ignored with trend analysis. **cURL TikTok request:** ```bash curl -G https://api.virlo.ai/v1/satellite/sounds/tiktok/7392134672372746241 \ -H "Authorization: Bearer YOUR_API_KEY" \ -d trend_analysis=true ``` **Response 200 Started:** ```json { "data": { "job_id": "b5a1c328-53d6-46fe-b33b-d2d757e465d9", "status": "processing" } } ``` **Response 200 Cached:** ```json { "data": { "job_id": "b5a1c328-53d6-46fe-b33b-d2d757e465d9", "status": "completed", "cached": true } } ``` **Response 400:** ```json { "message": "Invalid platform \"youtube\". Sound lookups support tiktok and instagram. YouTube is not supported (no public \"videos using this sound\" feed).", "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 402 With trends:** ```json { "statusCode": 402, "message": "Insufficient balance. $1.00 required, $0.70 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 100, "remaining_credits": 70, "code": "insufficient_credits" } ``` --- ## Check lookup status **Endpoint:** `GET https://api.virlo.ai/v1/satellite/sounds/status/:job_id` Send the `job_id` to check progress, for free. Check again every 10 to 15 seconds (polling) until `status` is `completed` (data in `result`) or `failed` (reason in `error`). To skip polling, listen for the [`satellite.lookup.completed`](https://dev.virlo.ai/docs/webhooks#lookup-payload) webhook with `data.type` `"sound_lookup"`. Still `processing` after 15 minutes? It has likely stalled, so start again (billed as new). Don't wait: the saved run says `failed` only after 3 hours, and isn't refunded. ### What's in the result - `run_id` (string, optional): Same as `job_id`. [Reopen the result](https://dev.virlo.ai/docs/satellite/sounds#durable-runs) with it for free. - `sound` (object, optional): `title`, `duration_seconds`, `cover_url`, `owner` (`handle` and `nickname`, either may be `null`), `is_original` (a creator's own audio), `is_commerce_music`, and `reported_usage_count` (TikTok's count of videos using it). - `stats` (object, optional): See [Stats](https://dev.virlo.ai/docs/satellite/sounds#stats-block). - `trends` (object, optional): `analyzed: false` unless you asked for [trend analysis](https://dev.virlo.ai/docs/satellite/sounds#trends-block). - `videos` (array, optional): The videos kept, in the platform's order, not by views. - `sample_quality` (object, optional): `pages_fetched` (Virlo reads the video list in pages), `truncated_by_cap` (`true` if Virlo stopped at 25 pages or 500 videos with more left), and `note` (corpus = the video set): `insufficient_corpus` (under 10 videos), `small_corpus_workable` (10 to 49), `healthy_corpus` (50 to 199), or `deep_corpus` (200+). - `credits_charged` (number, optional): `50` ($0.50), or `100` ($1.00) with trend analysis. Free repeats show the original charge. - `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. - `request / data_captured_at` (object / string, optional): Your settings (`max_videos` shows `300` with trend analysis) and when collection finished (UTC). **Full field reference** Each `videos[]` item and `stats.top_video` has: - `id / url / description` (string, optional): Platform ID, link, and caption. - `hashtags` (string[], optional): Caption hashtags, lowercase, without `#`. - `publish_date / duration_seconds` (string / number, optional): Post time (UTC) and length in seconds. - `views / likes / comments / shares / collects` (number, optional): `collects` means saves. - `is_duet / is_stitch / region` (boolean / string, optional): TikTok duet and stitch flags, and a country code such as `US`. - `author` (object, optional): `unique_id` (handle), `nickname`, `is_verified`, and `follower_count` (currently always `null`). **cURL request:** ```bash curl https://api.virlo.ai/v1/satellite/sounds/status/b5a1c328-53d6-46fe-b33b-d2d757e465d9 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response Processing:** ```json { "data": { "status": "processing" } } ``` **Response Completed:** ```json { "data": { "status": "completed", "result": { "run_id": "b5a1c328-53d6-46fe-b33b-d2d757e465d9", "request": { "music_id": "7392134672372746241", "platform": "tiktok", "trend_analysis": true, "max_videos": 300 }, "sound": { "music_id": "7392134672372746241", "platform": "tiktok", "title": "morning coffee", "is_original": false, "is_commerce_music": true, "duration_seconds": 74, "cover_url": "https://p16-sg.tiktokcdn.com/...", "owner": { "handle": null, "nickname": "nanaacom" }, "reported_usage_count": 78800 }, "data_captured_at": "2026-09-24T17:27:18.645Z", "credits_charged": 100, "stats": { "videos_analyzed": 289, "views": { "total": 219295476, "avg": 758808, "median": 316466, "max": 11133681, "min": 29280 }, "engagement": { "total_likes": 16981107, "total_comments": 96687, "total_shares": 975378, "total_collects": 2092075, "avg_likes": 58758, "avg_comments": 335, "avg_shares": 3375, "avg_collects": 7239, "engagement_rate": 0.0919 }, "velocity": { "videos_per_week": [{ "iso_week": "2026-W38", "count": 10 }, { "iso_week": "2026-W39", "count": 1 }], "last_4w_avg_videos_per_week": 6.5, "prior_4w_avg_videos_per_week": 14.5, "is_accelerating": false }, "verified_creator_count": 27, "verified_creator_pct": 0.0934, "commerce_music_pct": 1, "duration_distribution": { "under_15s": 15, "between_15_30s": 27, "between_30_60s": 94, "over_60s": 153 }, "top_creators": [ { "unique_id": "creator_a", "nickname": "Creator A", "is_verified": false, "follower_count": null, "video_count": 9, "total_views": 18781269, "avg_views": 2086808 } ], "top_hashtags": [ { "hashtag": "mukbang", "used_count": 11, "total_views": 18489812, "avg_views": 1680892 } ], "top_video": { "id": "7608361337277648158", "url": "https://www.tiktok.com/@creator_b/video/7608361337277648158", "description": "It's the small wins", "publish_date": "2026-02-19T00:08:03.000Z", "duration_seconds": 18, "views": 11133681, "likes": 187486, "comments": 426, "shares": 5491, "collects": 13949, "hashtags": [], "is_duet": false, "is_stitch": false, "region": "US", "author": { "unique_id": "creator_b", "nickname": "Creator B", "is_verified": false, "follower_count": null } } }, "sample_quality": { "truncated_by_cap": true, "pages_fetched": 25, "note": "deep_corpus" }, "trends": { "analyzed": true, "status": "ok", "summary": "The corpus of 240 videos using the \"morning coffee\" sound primarily features daily routines, food and drink creations, and product showcases.", "trends": [ { "name": "Food & Drink Creations/Reviews", "description": "Videos showcasing the step-by-step process of making various coffee, matcha, or other beverages, alongside mukbangs and reviews of diverse food items.", "tactics": ["Detail ingredients and steps for custom drinks", "Feature aesthetic drinkware or accessories"], "confidence": 0.91, "video_count": 47, "evidence_video_ids": ["7660615207307332877", "7671163917615336725", "..."], "time_windows": [ { "start_date": "2025-09-12", "end_date": "2026-09-17", "video_count": 47, "total_views": 67283700, "total_likes": 4268543, "total_comments": 29808, "total_shares": 306930, "avg_views": 1431568, "description": "A continuous focus on food and drink content, from coffee and matcha recipes to mukbangs and fast-food reviews." } ], "resurged": false, "momentum": null } ], "model_used": "gemini-2.5-flash", "cost_usd": 0.0475658, "tokens_used": 72628 }, "videos": [ { "id": "7562628856616684831", "url": "https://www.tiktok.com/@creator_c/video/7562628856616684831", "description": "Trying a science experiment #science #scienceexperiments", "publish_date": "2025-10-18T18:22:39.000Z", "duration_seconds": 35, "views": 7284919, "likes": 1512197, "comments": 6950, "shares": 169766, "collects": 188474, "hashtags": ["science", "scienceexperiments"], "is_duet": false, "is_stitch": false, "region": "US", "author": { "unique_id": "creator_c", "nickname": "Creator C", "is_verified": false, "follower_count": null } } ] } } } ``` **Response Failed:** ```json { "data": { "status": "failed", "error": "A short message explaining what went wrong" } } ``` **Response 404:** ```json { "message": "Sound lookup job not found or expired", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Stats Plain math over the videos in `videos[]`, with no AI. - `videos_analyzed` (number, optional): How many videos the stats cover. - `views` (object, optional): `total`, `avg`, `median`, `max`, and `min`. - `engagement` (object, optional): Totals and averages for likes, comments, shares, and collects. `engagement_rate` is (likes + comments + shares + collects) / views, as a fraction (`0.0919` = 9.19%). - `velocity` (object, optional): `videos_per_week`: sampled videos per week (`2026-W38`, weeks start Monday UTC), oldest first, for the 26 most recent weeks that had videos (empty weeks are skipped). For a big sound it shows the sample, not the sound's true pace. `last_4w_avg_videos_per_week` and `prior_4w_avg_videos_per_week` average the 4 newest listed weeks and the 4 before. `is_accelerating` is `true` if the newer average is 15%+ higher, or if there is nothing earlier to compare, so a sound whose videos all fall in 4 or fewer weeks, like a brand-new one, always reads `true`. - `top_creators` (array, optional): Up to 10 creators by total views, with `video_count`, `total_views`, and `avg_views`. - `top_hashtags` (array, optional): Up to 20 hashtags by total views, with `used_count`, `total_views`, and `avg_views`. - `top_video` (object | null, optional): The most viewed video, or `null` if none were found. - `verified_creator_count / verified_creator_pct` (number, optional): Videos (not creators) from verified accounts, as a count and a 0 to 1 share. - `duration_distribution` (object, optional): Video counts by length band, such as `under_15s`. - `commerce_music_pct` (number, optional): Share of videos (0 to 1) whose sound TikTok licenses for business accounts. Usually matches `sound.is_commerce_music`. --- ## Trend analysis With `trend_analysis=true`, an AI reads up to 240 videos, favoring the most viewed and at most 6 per creator where it can. It uses captions, hashtags, lengths, dates, and stats (it doesn't watch the videos) to group them into named creative patterns, with tactics a creator could copy. Dates, counts, and view totals come from each pattern's real videos, so the AI can't invent them. The summary, descriptions, and confidence score are the AI's own. Each trend shows when it ran, whether it came back, and whether views grew. **Full field reference** - `status` (string, optional): `ok`, `insufficient_corpus` (under 10 videos, no trends, trend fee refunded), or `skipped` (not requested). If the AI step fails, `status` is still `ok` with empty `summary` and `trends` and a `null` `model_used`, and the trend fee is refunded. - `summary` (string, optional): A short overview in the AI's words, counting only the videos it read. - `model_used / cost_usd / tokens_used` (string / number, optional): The AI model, such as `gemini-2.5-flash` (`null` if no AI ran), and Virlo's internal cost, not your charge. - `trends[].name / description / tactics` (string / string[], optional): Label, description, and things a creator could copy. - `trends[].confidence` (number, optional): The AI's own score from 0 to 1. - `trends[].video_count / evidence_video_ids` (number / string[], optional): How many videos show the pattern, and their IDs. Every ID is in `videos[]`. - `trends[].time_windows` (array, optional): Date ranges when the pattern ran, each with `start_date`, `end_date`, `video_count`, view, like, comment, and share totals, `avg_views`, and a `description`. A sample spanning under 14 days gives one window. - `trends[].resurged` (boolean, optional): `true` with two or more windows (it faded and came back). - `trends[].momentum` (string | null, optional): Latest window's average views against the one before: `stronger` (15%+ higher), `weaker` (15%+ lower), or `similar`. `null` with one window. --- ## Re-read past runs Every lookup is saved as a run under its `job_id` (called `run_id` here). Reopening is free, any time: - `GET /v1/satellite/runs/:run_id`: the full result. - `GET /v1/satellite/runs?type=sound_lookup&platform=tiktok`: your past sound lookups. - `GET /v1/satellite/runs/:run_id/videos?limit=50&offset=0`: the videos, a page at a time (25 per page by default, up to 200). **Each paid lookup of a sound gets its own `run_id`.** A newer lookup never overwrites an older run, so every result you paid for stays readable. More in [Re-read past runs](https://dev.virlo.ai/docs/satellite#durable-runs). --- ## Errors Rejected requests cost nothing. A started lookup is billed, and refunded in full if it ends `failed`. - `400 Bad Request`: Platform not `tiktok` or `instagram`, a YouTube sound ID Virlo recognizes, an unknown Virlo `id` (UUID), empty `music_id`, `max_videos` outside 1 to 100, or an unknown query parameter. - `401 Unauthorized`: Key missing (`missing_api_key`), or wrong or revoked (`invalid_api_key`). See [Authentication](https://dev.virlo.ai/docs/authentication). - `402 Payment Required`: Balance under $0.50, or under $1.00 with trend analysis. `required_credits` is in credits (100 = $1.00). Below $0.50 it asks for $0.50 (`required_credits: 50`) even for a trend lookup. Free repeats need this balance too. - `404 Not Found`: Status check: job ID unknown or over 24 hours old. [Open the saved run](https://dev.virlo.ai/docs/satellite/sounds#durable-runs) instead. - `429 Too Many Requests`: Daily request limit reached. See [Rate limits](https://dev.virlo.ai/docs/rate-limits). --- # 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 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 ``` **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" ``` **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). --- # Track creators and videos > Watch a creator or a single video over time. Virlo re-checks it on your schedule, saves the numbers, and writes an AI report each time. Source: https://dev.virlo.ai/docs/tracking Watch competitors, vet a creator before a paid deal, or see how a sponsored post performs. Virlo re-checks a TikTok, YouTube, or Instagram creator or video on your schedule. **At a glance** - **What it does:** Watches a creator or one video, and writes a fresh AI report on each check. - **You send:** A creator's handle or profile link, or a video link, plus how often to check. - **You get back:** An ID right away. Then numbers, history, AI reports, and alerts such as breakout videos. - **Cost:** $0.25 to start, then $0.25 per check until you pause or stop. Reading is free. - **How long:** The first report takes 1 to 2 minutes for a creator, and under a minute for a video. --- ## How tracking works 1. **Start** with [Track a creator](https://dev.virlo.ai/docs/tracking#track-a-creator) or [Track a video](https://dev.virlo.ai/docs/tracking#track-a-video). You get an `id` and pay $0.25, which covers the first check. 2. **Wait for the first check.** Ask for the item again every 15 seconds, or whatever `retry_after_seconds` says (free; this is called polling), until `enrichment_status` is `ready` or `failed`. Or use the `tracking.cycle.completed` [webhook](https://dev.virlo.ai/docs/tracking#webhook-notifications). 3. **Read the results, free:** [snapshots](https://dev.virlo.ai/docs/tracking#get-creator-snapshots) (history), the [AI report](https://dev.virlo.ai/docs/tracking#get-creator-report), [signals](https://dev.virlo.ai/docs/tracking#get-creator-signals) (alerts), and [posts](https://dev.virlo.ai/docs/tracking#list-creator-posts). 4. **It repeats** at $0.25 per check until you [pause](https://dev.virlo.ai/docs/tracking#update-tracked-creator) or [stop](https://dev.virlo.ai/docs/tracking#stop-tracking-creator). | Field | Values | | - | - | | `enrichment_status` | Report progress: `pending`, `processing`, `ready`, or `failed`. **`failed`:** 3 attempts in a row failed (for example, a wrong handle). A new item shows this within about a minute. Tracking pauses, and a failed first check is refunded. | | `status` | `active`, `paused` (no checks or charges: you paused it, or Virlo did after 3 failures or a low balance), or `deleted` (stopped, but still readable by `id`). | A **check** (the API also says _cycle_ or _scrape_) fetches the latest numbers, saves them as a **snapshot**, and writes an AI report. An _audience snapshot_ is different: a $0.50 profile of a creator's commenters. **Details for developers** Base URL: `https://api.virlo.ai/v1/tracking`. Results come in `data`, and lists add `pagination`. Times are ISO 8601 in UTC. Unknown parameters return `400`. Rates are fractions (`0.05` = 5%) unless noted. Tracked creators also have `pending_jobs` and `finalized`, as in [How results load](https://dev.virlo.ai/docs/async-data). Tracked videos don't. **Images.** Every image field is a full link, or `null`. A tracked creator's `avatar_url`, a post's `thumbnail_url`, a sound's `cover_url`, and the thumbnails in a report's `collection_data` point to files Virlo stores, under `https://auth.virlo.ai/storage/v1/object/public/`. Tracked videos return platform links, and TikTok's expire. Some files are HEIC (`.heic`), which most browsers other than Safari can't show, so convert them before display. --- ## What it costs - **Start:** $0.25, covering the first check. Refunded if Virlo can't find the creator or video. - **Each later check:** $0.25, charged only when it succeeds. - **Extras:** [older posts](https://dev.virlo.ai/docs/tracking#collect-creator-posts) $0.50 to $2.00, and an [audience snapshot](https://dev.virlo.ai/docs/tracking#audience-basics) $0.50. - **Everything else** is free. Later checks are billed silently (only the start charge appears in the `X-Cost` header), so watch your [balance](https://dev.virlo.ai/docs/credits#check-your-balance) and [Usage](https://dev.virlo.ai/dashboard/usage) page. If your balance can't cover a check, tracking pauses and won't restart when you add funds: [resume it](https://dev.virlo.ai/docs/tracking#update-tracked-creator) yourself. --- ## Track a creator **Endpoint:** `POST https://api.virlo.ai/v1/tracking/creators` Starts tracking a creator and runs the first check right away. **Cost:** $0.25. Refunded within about a minute if Virlo can't find the creator. **How long:** 1 to 2 minutes for the first report. Poll [Get tracked creator](https://dev.virlo.ai/docs/tracking#get-tracked-creator). Each creator can be tracked once per account, across the web app and the API. If it's already tracked, you get a `409` and no charge. The `message` says where. If it's in this key's workspace, you also get its `creator_id`. A creator you stopped through the API is restored instead. ### Request body - `platform` (string, required): `tiktok`, `youtube`, or `instagram`, in lowercase. - `handle` (string, optional): Such as `khaby.lame`. Send `handle` or `url`. If both, `handle` wins. - `url` (string, optional): The profile link, with `https://`: `https://www.tiktok.com/@username`, `https://www.instagram.com/username`, or `https://www.youtube.com/@handle` (`/channel/`, `/c/`, and `/user/` links work too). - `scrape_cadence` (string, optional): `six_hours`, `twelve_hours`, `daily` (default), `every_other_day`, `weekly`, `bi_weekly`, or `monthly`. - `collection_depth` (string, optional): Also collect older posts once: `standard` (50 videos, +$0.50), `deep` (200, +$1.00) or `full` (500, +$2.00). Refunded if it finds no posts or fails. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/tracking/creators \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "tiktok", "handle": "khaby.lame", "scrape_cadence": "daily" }' ``` **Response 202:** ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "active", "message": "Creator tracking started. Initial metrics and AI report are being generated.", "pending_jobs": [ { "type": "tracking_report", "status": "pending", "poll_url": "/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890", "result_path": "data", "webhook_event": "tracking.cycle.completed", "retry_after_seconds": 15 } ], "finalized": false } } ``` **Response 409 Already tracked:** ```json { "statusCode": 409, "error": "Conflict", "message": "You're already tracking @khaby.lame.", "creator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "code": "conflict" } ``` **Response 402:** ```json { "statusCode": 402, "message": "Insufficient balance. $0.25 required, $0.12 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 25, "remaining_credits": 12, "required_amount": "$0.25", "remaining_balance": "$0.12", "code": "insufficient_credits" } ``` **Response 402 With depth:** ```json { "statusCode": 402, "message": "Insufficient credits", "error": "Payment Required", "required_credits": 125, "remaining_credits": 40, "code": "insufficient_credits" } ``` A `402` has two shapes. Both have `code` and `required_credits`. --- ## List tracked creators **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators` Creators your team tracks through the API, newest first, including paused ones. Stopped creators and creators tracked in the web app aren't listed. Each item is a full creator. **Cost:** Free. ### Query parameters - `search` (string, optional): Part of a handle or display name. - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. - `page` (integer, optional): Default `1`. - `limit` (integer, optional): Default `20`. Over 100 counts as 100. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tracking/creators \ -H "Authorization: Bearer YOUR_API_KEY" \ -d search=khaby ``` **Response 200:** ```json { "data": [ { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "khaby.lame", "status": "active", "scrape_cadence": "daily", "enrichment_status": "ready", "latest_followers": 162973075, "growth_rate": 0.00031009, "next_scrape_at": "2026-09-25T14:00:05.120+00:00" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "total_pages": 1, "has_next_page": false, "has_prev_page": false } } ``` --- ## Get tracked creator **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id` One tracked creator: latest numbers, settings, and check progress. **Cost:** Free. **Full field reference** Every field is in the example. Notes: - `growth_rate`: change since the last check, in total views on YouTube and followers elsewhere. It reads `1` after the first check. - `followers_gained`: equals all followers after the first check. - `latest_total_likes`: `0` on YouTube and Instagram. - `latest_total_views`: the channel total on YouTube. Elsewhere, the sum over stored posts. - `profile_metadata`: differs by platform. `external_links` is an object. - `next_scrape_at`: the check starts within about 5 minutes of this time. **cURL request:** ```bash curl https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "khaby.lame", "profile_url": "https://www.tiktok.com/@khaby.lame", "display_name": "Khabane lame", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/7c2e9f4b1a8d3c6e5f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e.jpg", "bio": "If u wanna laugh u r in the right place", "is_verified": true, "status": "active", "scrape_cadence": "daily", "enrichment_status": "ready", "latest_followers": 162973075, "latest_following": 81, "latest_total_likes": 2681204805, "latest_total_views": 463241900, "latest_total_videos": 1355, "last_scraped_at": "2026-09-24T14:00:05.120+00:00", "growth_rate": 0.00031009, "followers_gained": 50521, "next_scrape_at": "2026-09-25T14:00:05.120+00:00", "category": "comedy", "content_tags": ["silent-comedy", "life-hacks", "reactions"], "profile_metadata": { "country": null, "website": null, "language": "en", "external_links": {} }, "created_at": "2026-09-20T10:30:00.000+00:00", "updated_at": "2026-09-24T14:00:31.402+00:00", "pending_jobs": [], "finalized": true } } ``` **Response 404:** ```json { "message": "Tracked creator not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Get creator report **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/report` The latest AI report on the creator, rewritten each check: what works, breakout videos (`viral_content`), and how commenters react. `report` is `null` until the first check is done. **Cost:** Free. `collection_data` lists the videos the report used. `popular_videos` is TikTok only, and YouTube uses `shorts`. **cURL request:** ```bash curl https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/report \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "account": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "khaby.lame", "latest_followers": 162973075 }, "report": { "id": "c7d8e9f0-a1b2-4c3d-9e4f-5a6b7c8d9e0f", "tracking_account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "analysis": { "overview": { "headline": "163M-follower comedy creator known for silent reactions...", "recent_focus": "Everyday mishaps and travel gags...", "content_focus": "Language-free comedy that works in any country...", "popular_focus": "Silent reactions to overcomplicated life hacks...", "growth_insight": "Follower growth is steady but slowing..." }, "key_insight": "Wordless storytelling travels across languages...", "what_works": [ { "insight": "Silent, non-verbal storytelling", "evidence": "His top videos contain no spoken words...", "evidence_video_ids": ["7678009073421405471"] } ], "discoveries": [ { "text": "Posts at almost the same time of day...", "metric": "3 posts/week", "evidence_video_ids": [] } ], "viral_content": [ { "title": "They were there yesterday, I promise #learnfromkhaby #comedy", "views": 101400000, "outlier_ratio": 8.52, "why_it_worked": "A relatable mix-up with a silent payoff...", "platform_video_id": "7678009073421405471" } ], "content_themes": [ { "name": "Life hack reactions", "avg_views": 89400000, "description": "Watches an overcomplicated solution, then shows the simple one...", "video_count": 8, "evidence_video_ids": ["7678009073421405471"] } ], "posting_patterns": { "frequency": "2 to 3 videos per week...", "best_formats": ["Silent reaction to a life hack"], "optimal_duration": "13 to 18 seconds" }, "subjects_covered": ["Life hack reactions", "Travel mishaps"], "audience_sentiment": { "overall_summary": "Overwhelmingly positive and global...", "sentiment_labels": ["highly_positive", "appreciative"], "notable_comments": [ { "likes": 190974, "content": "Learn from Khaby", "insight": "Fans treat him as a teacher of common sense", "video_id": "7678009073421405471" } ], "key_findings": [ { "finding": "Comments come in many languages...", "takeaway": "Keep videos wordless to reach every market..." } ] } }, "collection_data": { "latest_videos": [ { "url": "https://www.tiktok.com/@khaby.lame/video/7688400123809893662", "title": "Yeah, I'm never taking these glasses off again. #learnfromkhaby", "views": 1100000, "likes": 64000, "comments": 820, "shares": 1900, "bookmarks": 3100, "duration": null, "published_at": "2026-09-22T16:39:49.000Z", "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/0174d2837f7635b2bed46a3bff5e03ec16fe26465600f3839f56b68e69911c2a.webp", "platform_video_id": "7688400123809893662" } ], "popular_videos": [ { "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "title": "They were there yesterday, I promise #learnfromkhaby #comedy", "views": 101400000, "likes": 6200000, "comments": 31000, "shares": 450000, "bookmarks": 120000, "duration": null, "published_at": "2026-08-25T16:36:58.000Z", "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/6893372b005aa6d33df99bfd7c8093090c52ead2dc2a83c346972e8a749f47c1.webp", "platform_video_id": "7678009073421405471" } ], "shorts": [], "shorts_count": 0, "comments_collected": 40, "transcripts_collected": 0, "top_video_transcripts": {} }, "created_at": "2026-09-24T14:00:31.402+00:00" } } } ``` --- ## Get creator signals **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/signals` Alerts from the creator's checks, newest first. **Cost:** Free. | `type` | Raised when | | - | - | | `outlier_video` | A video got over 3 times the median views. Once per video. | | `follower_spike`, `follower_decline` | Followers moved at least 3% and 250 followers since the last check (`critical` at 10% or more). `payload.pct` is a percent. | | `new_subject` | The new report covers new topics. | | `creator_unreachable` | 3 failed attempts paused tracking. | `weighted_score` ranks breakouts: it rises with both `outlier_ratio` and median views. It's not the agent pages' [Virality Score](https://dev.virlo.ai/docs/glossary#words-that-change-meaning). `read` is always `false`. To spot new alerts, keep the newest `created_at` you've seen. ### Query parameters - `limit` (integer, optional): Default `50`, at most 100. There's no paging or date filter. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/signals?limit=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": [ { "id": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a", "type": "outlier_video", "severity": "notable", "title": "Outlier video: They were there yesterday, I promise #learnfromkhaby #comedy", "payload": { "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "title": "They were there yesterday, I promise #learnfromkhaby #comedy", "views": 101400000, "platform": "tiktok", "median_views": 11900000, "outlier_ratio": 8.52, "weighted_score": 34.91, "platform_handle": "khaby.lame", "platform_video_id": "7678009073421405471" }, "created_at": "2026-08-26T14:01:12.431+00:00", "read": false } ] } ``` --- ## Get creator snapshots **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/snapshots` The creator's numbers at each check, one row per check. **Cost:** Free. You get the most recent `limit` checks in your date range, listed oldest first. There's no paging. - `delta_*` compares with the check before it. The first row's are `null` only when there's no earlier check. - `engagement_rate` isn't the usual metric: it's `total_likes` ÷ `total_views` (`null` if views are 0). On TikTok that's profile likes over about 20 stored posts' views, so values like `5.79` are common. Use it only as one creator's trend. It's `0` on YouTube and Instagram. - `subscribers` repeats `followers` on YouTube and is `null` elsewhere. ### Query parameters - `start_date` (string, optional): ISO 8601, such as `2026-09-01`. - `end_date` (string, optional): A date alone means the start of that day, so that day is left out. Add `T23:59:59Z` to include it. - `limit` (integer, optional): How many of the most recent checks to return, 1 to 365. Default `30`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/snapshots \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-09-17T00:00:00Z ``` **Response 200:** ```json { "data": [ { "id": "1b2c3d4e-5f6a-4b7c-9d8e-0f1a2b3c4d5e", "tracking_account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "followers": 162973075, "following": 81, "subscribers": null, "total_videos": 1355, "total_views": 463241900, "total_likes": 2681204805, "collected_post_count": 20, "snapshot_at": "2026-09-24T14:00:05.120+00:00", "engagement_rate": 5.787915, "delta_followers": 50521, "delta_following": 0, "delta_total_videos": 1, "delta_total_views": 1253800, "delta_total_likes": 1254805, "delta_engagement_rate": -0.012992 } ] } ``` **Response 400 Bad date:** ```json { "message": ["start_date must be a valid ISO 8601 date string"], "error": "Bad Request", "statusCode": 400, "code": "invalid_date_range" } ``` --- ## Update tracked creator **Endpoint:** `PATCH https://api.virlo.ai/v1/tracking/creators/:id` Pause, resume, or change the cadence. Send only what changes. The response is the full creator. **Cost:** Free, but resuming triggers a $0.25 check. - Resuming (`status: "active"`) runs that check now, or within an hour if the last one was under an hour ago. - A new `scrape_cadence` sets the next check one full interval from now. Send it with `status` to resume without an immediate check. ### Request body The handle can't change: stop tracking and track the new one. - `status` (string, optional): `active` or `paused`. - `scrape_cadence` (string, optional): As in [Track a creator](https://dev.virlo.ai/docs/tracking#track-a-creator). **cURL request:** ```bash curl -X PATCH https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "scrape_cadence": "weekly" }' ``` **Response 200:** ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "khaby.lame", "status": "active", "scrape_cadence": "weekly", "next_scrape_at": "2026-10-01T16:20:41.918+00:00" } } ``` **Response 400:** ```json { "message": ["status must be one of the following values: active, paused"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` --- ## Stop tracking creator **Endpoint:** `DELETE https://api.virlo.ai/v1/tracking/creators/:id` Stops checks and charges, and hides the creator from your list. To take a break, [pause](https://dev.virlo.ai/docs/tracking#update-tracked-creator) instead. **Cost:** Free. - Everything stays readable by `id`, with `status: "deleted"`. - To come back, track the handle again (same `id` and history, $0.25), or update the `id` to `paused` or `active`. If the last check was under an hour ago, the new one can take up to an hour to start. - A second DELETE also returns `204`. **cURL request:** ```bash curl -X DELETE https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 204 No Content:** ```text (empty response body) ``` --- ## List creator posts **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/posts` The creator's stored videos, with their stats. **Cost:** Free. **Not the full history.** Each check stores TikTok's 10 or so newest and 10 most popular videos, up to Instagram's 90 newest, or YouTube's Shorts only. For older videos, [collect posts](https://dev.virlo.ai/docs/tracking#collect-creator-posts) with `deep` or `full`. ### Query parameters - `sort` (string, optional): `publish_date_desc` (default), `publish_date_asc`, or `views_desc`. - `start_date` (string, optional): Published on or after. ISO 8601. - `end_date` (string, optional): Published on or before. - `page` (integer, optional): Default `1`. - `limit` (integer, optional): 1 to 200. Default `50`. **Full field reference** Every field is in the example. Notes: - `is_outlier`: over 3 times the median views on the check that stored it. Otherwise `outlier_ratio` and `outlier_weighted_score` are `null`. - `shares` and `bookmarks`: TikTok only, `0` elsewhere. - `duration_seconds`: `null` on TikTok. - `sound`: `external_id` is the platform's sound ID. YouTube posts get a sound only from `deep` or `full` collections. `usage_count` and `owner_handle` are often `0` or `null`. **cURL request:** ```bash curl -G "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/posts" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 \ -d sort=views_desc ``` **Response 200:** ```json { "data": [ { "id": "c3d4e5f6-a7b8-4901-8def-123456789012", "tracking_account_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_video_id": "7678009073421405471", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "title": "They were there yesterday, I promise #learnfromkhaby #comedy", "description": "They were there yesterday, I promise #learnfromkhaby #comedy", "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/6893372b005aa6d33df99bfd7c8093090c52ead2dc2a83c346972e8a749f47c1.webp", "publish_date": "2026-08-25T16:36:58+00:00", "views": 101400000, "likes": 6200000, "comments": 31000, "shares": 450000, "bookmarks": 120000, "is_duet": false, "is_stitch": false, "duration_seconds": null, "hashtags": ["learnfromkhaby", "comedy"], "collected_at": "2026-09-24T14:00:21.469+00:00", "is_outlier": true, "outlier_ratio": 8.52, "outlier_weighted_score": 34.91, "sound": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "original sound - khaby.lame", "duration": 15, "platform": "tiktok", "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/abb4e3bcf8d355936aa54a444b83930591647f6c412843f9fa40dbb71478ce9a.jpg", "external_id": "7559312683885201425", "is_original": true, "usage_count": 0, "owner_handle": null, "owner_nickname": "Khabane lame", "is_commerce_music": false } } ], "pagination": { "page": 1, "limit": 20, "total": 20, "total_pages": 1, "has_next_page": false, "has_prev_page": false } } ``` --- ## Get creator post **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/posts/:post_id` One stored post, with the same fields as the list. **Cost:** Free. ### Path parameters - `post_id` (string, required): The post's `id` from [List creator posts](https://dev.virlo.ai/docs/tracking#list-creator-posts). A platform video ID returns `404`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/posts/c3d4e5f6-a7b8-4901-8def-123456789012" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "id": "c3d4e5f6-a7b8-4901-8def-123456789012", "platform_video_id": "7678009073421405471", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "views": 101400000, "is_outlier": true, "outlier_ratio": 8.52, "outlier_weighted_score": 34.91 } } ``` **Response 404:** ```json { "message": "Creator post not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Collect creator posts **Endpoint:** `POST https://api.virlo.ai/v1/tracking/creators/:id/posts/collect` Stores a creator's older videos once, for more history than checks keep. **Cost:** set by `depth`, charged when it starts. Refunded in full if it finds no posts or its last attempt fails. | `depth` | Gets | Cost | | - | - | - | | `standard` (default) | Up to 50 videos | $0.50 | | `deep` | Up to 200 videos | $1.00 | | `full` | Up to 500 videos | $2.00 | **How long:** seconds for `standard`, longer for `deep` and `full`. Poll [Get collection status](https://dev.virlo.ai/docs/tracking#get-collection-status). - **Every depth pages back to its target.** On TikTok it also adds the most popular videos: one page at `standard`, up to 100 at `deep` and `full`. - **Wait for a successful check first** (`enrichment_status: "ready"`). If the last check failed, you get a `409` and no charge. - One collection at a time. Another returns a `409`. ### Request body - `depth` (string, optional): `standard`, `deep`, or `full`. - `force` (boolean, optional): `true` skips the failed-check `409`. A forced collection that finds nothing is still refunded. **cURL request:** ```bash curl -X POST "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/posts/collect" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "depth": "deep" }' ``` **Response 202:** ```json { "data": { "collection_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "status": "processing", "depth": "deep", "max_videos": 200, "credits_reserved": 100 } } ``` **Response 409 Running:** ```json { "message": "A collection is already in progress for this creator (collection_id: e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b). Wait for it to complete before starting a new one.", "error": "Conflict", "statusCode": 409, "code": "conflict" } ``` **Response 409 Unhealthy:** ```json { "statusCode": 409, "error": "Tracking unhealthy", "message": "This creator's most recent tracking cycle failed (scrape_status=failed, enrichment_status=failed). Post collection reads the same upstream profile, so it would come back empty. No credits were charged.", "scrape_status": "failed", "enrichment_status": "failed", "pause_reason": "Scrape failed 3 consecutive times: TikTok profile response missing user (handle may be deactivated, private, or region-blocked)", "last_scraped_at": null, "hint": "Fix the handle or resume tracking via PATCH /v1/tracking/creators/:id and wait for a cycle to succeed, or retry with body { \"force\": true } if the failure was transient.", "code": "conflict" } ``` `credits_reserved` is the charge in credits (`100` = $1.00). --- ## Get collection status **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/posts/collect/:collection_id` Checks on a post collection. `status` is `processing`, `completed`, or `failed` (with an `error`). `videos_collected` includes videos Virlo already had. If a collection found no posts or failed, `credits_refunded` shows what was paid back (in credits: 50 = $0.50). It appears only when there was a refund. The status is kept for 24 hours, then returns `404`. **Cost:** Free. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/posts/collect/e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "collection_id": "e5f6a7b8-c9d0-4e1f-8a2b-3c4d5e6f7a8b", "status": "completed", "depth": "deep", "videos_collected": 200, "max_videos": 200, "started_at": "2026-09-24T16:02:10.114Z", "completed_at": "2026-09-24T16:03:21.580Z" } } ``` --- ## Get posting cadence **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/posting-cadence` How often a creator posts, counted from stored posts only. **Cost:** Free. TikTok checks mix in old hit videos, so a daily poster can show under 1 post a week, and YouTube counts Shorts only. A `deep` or `full` [collection](https://dev.virlo.ai/docs/tracking#collect-creator-posts) helps, but the real rate is usually higher. Weekdays run from `"0"` (Sunday) to `"6"`, in UTC. Months are 30 days. With no posts, the rates are `null`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/posting-cadence" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "total_posts": 200, "avg_days_between_posts": 2.71, "posts_per_week": 2.6, "posts_per_month": 11.13, "day_of_week_distribution": { "0": 24, "1": 31, "2": 30, "3": 29, "4": 28, "5": 30, "6": 28 }, "earliest_post_date": "2025-04-02T16:30:00.000Z", "latest_post_date": "2026-09-23T17:40:45.000Z" } } ``` --- ## Audience data Find out who engages with a tracked creator: age, gender, language, country, and city. Virlo builds an **audience snapshot** from the people who comment on recent videos. - **You ask for it** with [Refresh audience snapshot](https://dev.virlo.ai/docs/tracking#refresh-audience-snapshot). Tracking never makes one, and the creator needs a successful check first. - **Cost:** $0.50 per new snapshot, charged when it starts. Refunded automatically if the job fails or only has the creator's profile. Reading is free. - [Demographics](https://dev.virlo.ai/docs/tracking#get-audience-demographics) and [geography](https://dev.virlo.ai/docs/tracking#get-audience-geography) read the same snapshot. | `data_source` | Meaning | | - | - | | `comments` | Commenters. The usual case. | | `mixed` or `followers` | TikTok, when commenters are few: adds followers, or uses only followers. | | `comments_extended` | Instagram and YouTube: commenters from more posts. | | `profile_only` | A guess from the profile. Always `low` confidence, and refunded. | `confidence_level` is `high` (usually 200 or more people in `sample_size`), `medium` (100 or more), or `low`. `confidence_per_signal` scores each field from 0 to 1. --- ## Refresh audience snapshot **Endpoint:** `POST https://api.virlo.ai/v1/tracking/creators/:id/audience-refresh` Gets audience data. A snapshot younger than `freshness_days` comes back free (`source: "cache"`). Otherwise Virlo starts a new one (`source: "fresh"`) and returns a `job_id`. Both return `202`. **Cost:** $0.50 for a new snapshot. Reusing one is free, but needs a $0.50 balance. **How long:** 5 to 12 minutes. Poll [the job](https://dev.virlo.ai/docs/tracking#check-audience-refresh-status), or wait for the `audience.snapshot.completed` webhook. > **Note:** **Repeating this call while a job runs is free.** You get the same `job_id` back, with `credits_used: 0`. A `409` means no check has succeeded yet, or the last one failed. Wrong handle? Stop tracking and track the right one. PATCH can't fix it, whatever the `hint` says. ### Request body - `freshness_days` (integer, optional): 0 to 365. Default `30`. `0` always makes a new one. - `force` (boolean, optional): `true` ignores saved snapshots and skips the `409`. $0.50 each time, unless a job is already running. **cURL request:** ```bash curl -X POST "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audience-refresh" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "freshness_days": 30 }' ``` **Response 202 New job:** ```json { "data": { "source": "fresh", "snapshot": null, "job_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "status": "processing", "credits_used": 50, "creator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credit_unit": "cent", "pending_jobs": [ { "type": "audience_demographics", "status": "processing", "job_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "poll_url": "/v1/audience/snapshot/9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "result_path": "data.snapshot", "webhook_event": "audience.snapshot.completed", "retry_after_seconds": 15 } ], "finalized": false } } ``` **Response 202 Reused:** ```json { "data": { "source": "cache", "snapshot": { "snapshot_id": "f5a3b2c1-d4e5-4f6a-8b7c-9d0e1f2a3b4c", "snapshot_at": "2026-09-20T12:04:33.120+00:00", "sample_size": 500, "age_distribution": { "13-17": 0.14, "18-24": 0.46, "25-34": 0.32, "35-44": 0.06, "45+": 0.02 }, "gender_distribution": { "male": 0.45, "female": 0.55 }, "country_distribution": [ { "pct": 0.22, "code": "US", "name": "US", "confidence": 0.81 }, { "name": "Other (14 countries)", "pct": 0.78 } ], "city_distribution": null, "language_distribution": { "en": 0.62, "it": 0.18, "pt": 0.09, "es": 0.07, "un": 0.04 }, "confidence_per_signal": { "age": 0.72, "city": 0.69, "gender": 0.84, "country": 0.79, "language": 0.92 }, "confidence_level": "high", "data_source": "comments", "signal_breakdown": { "comments": 500, "followers": 0 }, "evidence_summary": "Analysis of 500 commenters across 30 recent posts shows...", "model_version": "v1" }, "job_id": null, "status": null, "credits_used": 0, "creator_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credit_unit": "cent", "pending_jobs": [], "finalized": true } } ``` **Response 409:** ```json { "statusCode": 409, "error": "Tracking unhealthy", "message": "This creator has not completed a successful tracking cycle yet. Wait for the first cycle to finish (~60-120s after track_creator), then retry audience-refresh.", "scrape_status": "failed", "enrichment_status": "failed", "pause_reason": "Scrape failed 3 consecutive times: TikTok profile response missing user (handle may be deactivated, private, or region-blocked)", "last_scraped_at": null, "hint": "Wait for the next tracking cycle to succeed, fix the handle via PATCH /v1/tracking/creators/:id, or retry this call with body { \"force\": true } if you believe the failure was transient.", "code": "conflict" } ``` `credits_used` is in credits (`50` = $0.50). --- ## Check audience refresh status **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/audience-refresh/:jobId` Checks on an audience job. `status` is `processing`, `completed` (with the snapshot), or `failed`. **Cost:** Free. - A failure has `error.code` and `error.message` (`INSUFFICIENT_SAMPLE` means too few people), and is refunded. - The job is kept for 24 hours, then returns `404`. The snapshot stays readable. - `pending_jobs[].poll_url` works too. This route has friendlier field names. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audience-refresh/9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 Completed:** ```json { "data": { "job_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "status": "completed", "snapshot": { "snapshot_id": "f5a3b2c1-d4e5-4f6a-8b7c-9d0e1f2a3b4c", "snapshot_at": "2026-09-24T16:27:09.261+00:00", "sample_size": 500, "confidence_level": "high", "data_source": "comments", "model_version": "v1" }, "error": null, "pending_jobs": [], "finalized": true } } ``` **Response 200 Failed:** ```json { "data": { "job_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "status": "failed", "snapshot": null, "error": { "code": "INSUFFICIENT_SAMPLE", "message": "Only 12 unique commenters/followers collected after fallback (min 30). Creator likely has very little public engagement or follower visibility. The customer was NOT charged for this attempt." }, "pending_jobs": [], "finalized": true } } ``` --- ## Get audience demographics **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/audience-demographics` Age, gender, and language from the latest audience snapshot. Reading never starts a new one. **Cost:** Free. - **You always get the latest snapshot, however old.** `freshness_days` only sets `is_stale: true` when it's older. Without it, `is_stale` is `false`. - `snapshot` is `null` if there's none. If the first is still running, `pending_jobs[0]` has its `job_id`. - Language `un` means unknown. ### Query parameters - `freshness_days` (integer, optional): 0 to 365. Only sets `is_stale`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audience-demographics?freshness_days=30" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "platform": "tiktok", "handle": "khaby.lame", "snapshot": { "snapshot_id": "f5a3b2c1-d4e5-4f6a-8b7c-9d0e1f2a3b4c", "snapshot_at": "2026-09-24T16:27:09.261+00:00", "sample_size": 500, "age_distribution": { "13-17": 0.14, "18-24": 0.46, "25-34": 0.32, "35-44": 0.06, "45+": 0.02 }, "gender_distribution": { "male": 0.45, "female": 0.55 }, "language_distribution": { "en": 0.62, "it": 0.18, "pt": 0.09, "es": 0.07, "un": 0.04 }, "country_distribution": null, "city_distribution": null, "confidence_per_signal": { "age": 0.72, "city": 0.69, "gender": 0.84, "country": 0.79, "language": 0.92 }, "confidence_level": "high", "data_source": "comments", "signal_breakdown": { "comments": 500, "followers": 0 }, "evidence_summary": "Analysis of 500 commenters across 30 recent posts shows...", "model_version": "v1" }, "is_stale": false, "pending_jobs": [], "finalized": true } } ``` **Response 200 In progress:** ```json { "data": { "platform": "tiktok", "handle": "khaby.lame", "snapshot": null, "is_stale": false, "pending_jobs": [ { "type": "audience_demographics", "status": "processing", "job_id": "9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "poll_url": "/v1/audience/snapshot/9f8e7d6c-5b4a-4392-8a1b-0c9d8e7f6a5b", "result_path": "data.snapshot", "webhook_event": "audience.snapshot.completed", "retry_after_seconds": 15 } ], "finalized": false } } ``` --- ## Get audience geography **Endpoint:** `GET https://api.virlo.ai/v1/tracking/creators/:id/audience-geography` Countries and cities, from the same snapshot as [demographics](https://dev.virlo.ai/docs/tracking#get-audience-demographics) and with the same rules. **Cost:** Free. - A country's `name` repeats its `code`, such as `US`. Small countries roll up into `Other (N countries)`. - A city's `name` is the city. `city_distribution` can be `null`. ### Query parameters - `freshness_days` (integer, optional): 0 to 365. Only sets `is_stale`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audience-geography" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "platform": "tiktok", "handle": "khaby.lame", "snapshot": { "snapshot_id": "f5a3b2c1-d4e5-4f6a-8b7c-9d0e1f2a3b4c", "snapshot_at": "2026-09-24T16:27:09.261+00:00", "sample_size": 500, "age_distribution": null, "gender_distribution": null, "language_distribution": null, "country_distribution": [ { "pct": 0.22, "code": "US", "name": "US", "confidence": 0.81 }, { "pct": 0.19, "code": "IT", "name": "IT", "confidence": 0.84 }, { "name": "Other (14 countries)", "pct": 0.59 } ], "city_distribution": [ { "pct": 0.08, "code": "IT", "name": "Milan", "confidence": 0.71 } ], "confidence_level": "high", "data_source": "comments", "model_version": "v1" }, "is_stale": false, "pending_jobs": [], "finalized": true } } ``` --- ## Track a video **Endpoint:** `POST https://api.virlo.ai/v1/tracking/videos` Starts tracking one video and runs the first check right away. **Cost:** $0.25. The link isn't checked first. If the video can't be read, you're refunded about 40 seconds later, and it stays `paused` with `enrichment_status: "failed"`. **How long:** usually under a minute. Poll [Get tracked video](https://dev.virlo.ai/docs/tracking#get-tracked-video). Videos follow the same once-per-account rule, with `video_id` in the `409`. A video you [stopped](https://dev.virlo.ai/docs/tracking#stop-tracking-video) returns `409` too. `400 Failed to create tracked video` means the `tracking_account_id` isn't a creator you track, or you sent two YouTube `/shorts/` links seconds apart. ### Request body - `url` (string, required): TikTok: `https://www.tiktok.com/@user/video/ID`. YouTube: `https://www.youtube.com/watch?v=ID` or `https://youtu.be/ID` (best), or `/shorts/ID`. Instagram: `https://www.instagram.com/reel/CODE/` or `/p/CODE/`. - `platform` (string, required): `tiktok`, `youtube`, or `instagram`. - `scrape_cadence` (string, optional): As in [Track a creator](https://dev.virlo.ai/docs/tracking#track-a-creator). - `tracking_account_id` (string, optional): A tracked creator's `id`, if it's their video, so the report can weigh views against their followers. Can't change later. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/tracking/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "platform": "tiktok", "scrape_cadence": "daily" }' ``` **Response 202:** ```json { "data": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "status": "active", "message": "Video tracking started. Initial metrics and AI report are being generated." } } ``` **Response 409 Already tracked:** ```json { "statusCode": 409, "error": "Conflict", "message": "You're already tracking this video.", "video_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "code": "conflict" } ``` **Response 402:** ```json { "statusCode": 402, "message": "Insufficient balance. $0.25 required, $0.12 remaining. Add funds at https://dev.virlo.ai/dashboard/billing", "error": "Payment Required", "required_credits": 25, "remaining_credits": 12, "required_amount": "$0.25", "remaining_balance": "$0.12", "code": "insufficient_credits" } ``` --- ## List tracked videos **Endpoint:** `GET https://api.virlo.ai/v1/tracking/videos` Works like [List tracked creators](https://dev.virlo.ai/docs/tracking#list-tracked-creators), with the same `platform`, `page`, and `limit`. Each item includes the whole AI report, so items are large. **Cost:** Free. ### Query parameters - `search` (string, optional): Part of the video's title or link. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tracking/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "platform": "tiktok", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "status": "active", "enrichment_status": "ready", "latest_views": 101400000, "growth_rate": 0.0091, "next_scrape_at": "2026-09-25T14:00:04.723+00:00" } ], "pagination": { "page": 1, "limit": 20, "total": 1, "total_pages": 1, "has_next_page": false, "has_prev_page": false } } ``` --- ## Get tracked video **Endpoint:** `GET https://api.virlo.ai/v1/tracking/videos/:id` One tracked video: numbers, settings, transcript, and latest AI report. **Cost:** Free. **Full field reference** Every field is in the example, and there's no `updated_at`. Notes: - `growth_rate`: view change since the last check. `0.009` means up 0.9% per check, not per day. It reads `0` after the first check. - `latest_shares` and `latest_bookmarks`: TikTok only, `0` elsewhere. - `duration`: often `null` on YouTube. - `platform_video_id`: empty for a YouTube `/shorts/` link until the first check. - `analysis`: the [report](https://dev.virlo.ai/docs/tracking#get-video-report), `null` until the first check. **cURL request:** ```bash curl https://api.virlo.ai/v1/tracking/videos/b2c3d4e5-f6a7-8901-bcde-f12345678901 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "platform": "tiktok", "platform_video_id": "7678009073421405471", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "title": "They were there yesterday, I promise #learnfromkhaby #comedy", "description": "They were there yesterday, I promise #learnfromkhaby #comedy", "thumbnail_url": "https://p16-common-sign.tiktokcdn-us.com/...", "duration": 21, "published_at": "2026-08-25T16:36:58+00:00", "author_handle": "khaby.lame", "author_name": "Khabane lame", "author_avatar_url": "https://p19-common-sign.tiktokcdn-us.com/...", "status": "active", "scrape_cadence": "daily", "enrichment_status": "ready", "latest_views": 101400000, "latest_likes": 6200000, "latest_comments": 31000, "latest_shares": 450000, "latest_bookmarks": 120000, "latest_transcript": null, "growth_rate": 0.0091, "analysis": { "key_insight": "A relatable mix-up with a silent payoff..." }, "analysis_updated_at": "2026-09-24T14:00:31.026+00:00", "tracking_account_id": null, "last_scraped_at": "2026-09-24T14:00:04.723+00:00", "next_scrape_at": "2026-09-25T14:00:04.723+00:00", "created_at": "2026-09-20T10:30:00.000+00:00" } } ``` **Response 404:** ```json { "message": "Tracked video not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Get video report **Endpoint:** `GET https://api.virlo.ai/v1/tracking/videos/:id/report` The latest AI report on the video: how it's doing, why it works, the hook, and how commenters react. `analysis` is `null` until the first check. **Cost:** Free. `video` is the full tracked video, `analysis` included. `overview.engagement_rate` is a percent: `7.06` means 7.06%. **cURL request:** ```bash curl https://api.virlo.ai/v1/tracking/videos/b2c3d4e5-f6a7-8901-bcde-f12345678901/report \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "data": { "video": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "platform": "tiktok", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "latest_views": 101400000 }, "analysis": { "overview": { "summary": "A silent comedy bit about a missing object...", "outlier_ratio": null, "growth_insight": "Views are still climbing a month after posting...", "engagement_rate": 7.06, "growth_trajectory": "steady", "performance_verdict": "overperforming" }, "key_insight": "A relatable mix-up with a silent payoff...", "what_works": [ { "insight": "The punchline lands without a single word", "evidence": "Top comments come in many languages..." } ], "discoveries": [ { "text": "Saves are unusually high for a comedy clip...", "metric": "1.2% save rate" } ], "hook_analysis": { "hook_type": "Pattern interrupt", "description": "Opens mid-problem, so viewers stay to see the fix...", "effectiveness": "strong" }, "content_breakdown": { "topics": ["Everyday mishaps"], "format_style": "Silent reaction with one visual punchline...", "target_audience": "A global audience of all ages..." }, "audience_sentiment": { "overall_summary": "Warm and amused...", "sentiment_labels": ["appreciative", "amused"], "notable_comments": [ { "likes": 52000, "content": "This is literally me every morning", "insight": "Viewers see themselves in the joke" } ], "key_findings": [ { "finding": "Viewers tag friends who do the same thing...", "takeaway": "Relatable mistakes drive shares..." } ] } }, "analysis_updated_at": "2026-09-24T14:00:31.026+00:00" } } ``` --- ## Get video snapshots **Endpoint:** `GET https://api.virlo.ai/v1/tracking/videos/:id/snapshots` The video's views, likes, comments, shares, and saves at each check. **Cost:** Free. Works like [creator snapshots](https://dev.virlo.ai/docs/tracking#get-creator-snapshots), with the same `start_date`, `end_date`, and `limit`: the most recent checks, listed oldest first. `shares` and `bookmarks` are `0` on YouTube and Instagram. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tracking/videos/b2c3d4e5-f6a7-8901-bcde-f12345678901/snapshots \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-09-17T00:00:00Z ``` **Response 200:** ```json { "data": [ { "id": "8734c9e6-efce-4c49-8262-2e20ac8f204f", "tracking_video_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "views": 101400000, "likes": 6200000, "comments": 31000, "shares": 450000, "bookmarks": 120000, "snapshot_at": "2026-09-24T14:00:04.723+00:00", "delta_views": 920000, "delta_likes": 20000, "delta_comments": 200, "delta_shares": 2000, "delta_bookmarks": 1000 } ] } ``` --- ## Update tracked video **Endpoint:** `PATCH https://api.virlo.ai/v1/tracking/videos/:id` Works like [Update tracked creator](https://dev.virlo.ai/docs/tracking#update-tracked-creator), with the same `status` and `scrape_cadence` fields and the same resume rules. The link and `tracking_account_id` can't change. The response is the full video. **Cost:** Free, but resuming triggers a $0.25 check. **cURL request:** ```bash curl -X PATCH https://api.virlo.ai/v1/tracking/videos/b2c3d4e5-f6a7-8901-bcde-f12345678901 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "paused" }' ``` **Response 200:** ```json { "data": { "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "platform": "tiktok", "url": "https://www.tiktok.com/@khaby.lame/video/7678009073421405471", "status": "paused", "scrape_cadence": "daily", "next_scrape_at": "2026-09-25T14:00:04.723+00:00" } } ``` --- ## Stop tracking video **Endpoint:** `DELETE https://api.virlo.ai/v1/tracking/videos/:id` Stops checks and charges, and hides the video from your list. Its data stays readable by `id`, with `status: "deleted"`. A second DELETE also returns `204`. **Cost:** Free. > **Note:** **A stopped video can't be tracked again.** The only way back is to [update](https://dev.virlo.ai/docs/tracking#update-tracked-video) the old `id` to `paused` or `active`. To take a break, pause instead. **cURL request:** ```bash curl -X DELETE https://api.virlo.ai/v1/tracking/videos/b2c3d4e5-f6a7-8901-bcde-f12345678901 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 204 No Content:** ```text (empty response body) ``` --- ## Webhook notifications To get a message on your server instead of polling, subscribe with the [Webhooks API](https://dev.virlo.ai/docs/webhooks#create-webhook) (`POST /v1/webhooks`). Only items tracked through the API send these. | Event | Sent when | | - | - | | `tracking.cycle.completed` | A check finished, and its report is ready. | | `tracking.outlier_video.detected` | A creator has a new breakout video. | | `tracking.paused` | 3 attempts failed, or your balance can't cover a check. | | `audience.snapshot.completed` | An audience snapshot is ready. Not sent on failure, so also [check the job](https://dev.virlo.ai/docs/tracking#check-audience-refresh-status). | After a low-balance pause, add funds, then set `status` to `active`. If Virlo couldn't reach the creator or video, the handle or link can't be edited: stop tracking and track the right one. Message fields: [Tracking updates](https://dev.virlo.ai/docs/webhooks#tracking-payload). --- ## Workflow recipes ### Vet a creator before a paid deal 1. [Track the creator](https://dev.virlo.ai/docs/tracking#track-a-creator) and wait for `ready`: $0.25. 2. [Refresh the audience snapshot](https://dev.virlo.ai/docs/tracking#refresh-audience-snapshot): $0.50. 3. Read the [report](https://dev.virlo.ai/docs/tracking#get-creator-report), [demographics](https://dev.virlo.ai/docs/tracking#get-audience-demographics), and [geography](https://dev.virlo.ai/docs/tracking#get-audience-geography), free. 4. [Stop tracking](https://dev.virlo.ai/docs/tracking#stop-tracking-creator) so no more checks are charged. Total: about $0.75. --- ## Errors Errors are never charged. For the full list, see [Errors](https://dev.virlo.ai/docs/errors). | Status | Usually means | | - | - | | `400` | A missing, misspelled, or unknown field, or a value like `TikTok` (use lowercase). | | `402` | Your balance is too low. Read `code` and `required_credits`. | | `404` | No item with that ID for your team. Use Virlo's `id`, not the platform's video ID. | | `409` | Already tracked on your account, a post collection is already running, or a post collection or audience refresh is blocked by a failed check. | --- # Trends > See which topics are trending on short-form video right now, worldwide or by country, with a momentum reading and example videos for each trend. Source: https://dev.virlo.ai/docs/trends See what is taking off on short-form video right now, so you can post while it is still growing. **At a glance** - **What it does:** Shows what is trending on TikTok, YouTube and Instagram, worldwide or in one country. - **You send:** Nothing required. Optionally a `region` like `gb`, or dates to see past days. - **You get back:** Within seconds, a ranked list of trends, each with a summary, momentum label and example videos. - **Cost:** $0.25 per request, even when nothing matches. Listing regions and failed requests are free. Start with [today's trends](https://dev.virlo.ai/docs/trends#get-trends-digest). [Emerging trends](https://dev.virlo.ai/docs/trends#get-emerging-trends) shows only new and rising trends, and [trends by date](https://dev.virlo.ai/docs/trends#get-trends) covers past days. ## How trends work A **trend** is a topic many people are posting about, like a news moment or a meme. Each region gets one ranked **daily list** (a "trend group" in the API), usually 15 to 20 trends. - Regions are `global` (the default), `us`, `gb`, `au` and `sg`. Each region's list is made on local time for that audience, not cut from `global`. - Virlo finds new trends at 7:00, 13:00 and 19:00 local time. The 7:00 run starts the day's list. - About every 2 hours, Virlo re-checks [momentum](https://dev.virlo.ai/docs/trends#momentum) and re-sorts the list, so rankings shift. - Virlo drops trends that still have no example videos and, beyond the top 20, the coldest ones. Trends under 6 hours old are never dropped. So a trend can vanish mid-day. - Active trends carry over to the next day. - Two trends can occasionally cover the same story. Trends can't be filtered by platform, niche or keyword. For one niche, use a [Content Research Agent](https://dev.virlo.ai/docs/agents). Example videos are the all-time most viewed, often months or years old. Check `publish_date`. ## Momentum Momentum shows how fast a trend is growing. Each trend has a `momentum` object: - `status` (string, optional) - `new`: first spotted less than 8 hours ago. - `rising`: views growing more than 15% faster than in the previous 2 hours. - `steady`: about the same speed. - `fading`: more than 15% slower. - `score` (number, optional): 0 to 1. About 1,000 views an hour scores 0.6, 10,000 scores 0.8, and 100,000 or more scores 1. - `views_per_hour` (integer, optional): Combined hourly view growth of the example videos between the last two checks. - `updated_at` (string, optional): Time of the last check (UTC). A new trend's numbers are rough until the 3rd check, usually within 6 hours. **Details for developers** - `momentum` is `null` until the first check. It stays `null`, sorted last, if fewer than 3 example videos can be re-checked. - After the 1st check, `score` is a 0.3 to 0.7 placeholder and `views_per_hour` is `0`. The 2nd check measures both. - Until the 3rd check, `status` is estimated from how many videos on the topic were posted the day before Virlo found it. - Only the newest list gets updates. Past days keep their last reading. ## Get today's trends **Endpoint:** `GET https://api.virlo.ai/v1/trends/digest` Returns one region's latest daily list. Cost per request: $0.25 - You get the newest list from the last 48 hours. Lists start at 7:00 local, so earlier you get yesterday's. - `title` shows today's date even on yesterday's list. Use `local_date`. - Lists under 15 trends get filled to 15 with earlier days' trends. These come last, with their old `trend_group_id`. - If the newest list is empty, you get the latest one with trends, even an older one. With no list in 48 hours, `data` is empty. - `region` (string, optional): Region code. Default `global`. - `top_exemplars` (integer, optional): Example videos per trend, 0 to 20. Default 5. `0` makes the response much smaller. It also accepts `limit`, `start_date` and `end_date`, but ignores them. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/trends/digest \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=au ``` **Response 200 OK:** ```json { "data": [ { "id": "7cf7a841-e7a7-4844-8ac4-ed9823615bae", "title": "Trends for Sep 25", "region": "au", "local_date": "2026-09-24", "trends": [ { "ranking": 9, "trend": { "id": "745ff63d-09d7-483b-94e2-3c7a6bf5c527", "name": "AFL Grand Final training buzz: Freo, Lions, and imminent crowd energy", "description": "On September 23, 2026, thousands of Fremantle Dockers fans gathered at Cockburn ARC for an open training session ahead of the AFL Grand Final against the Brisbane Lions...", "trend_type": "content" }, "momentum": { "score": 0.6456, "status": "fading", "views_per_hour": 1690, "updated_at": "2026-09-24T16:00:01.056Z" }, "velocity_today_count": 0, "velocity_median_views": 0, "detected_at": "2026-09-23T21:06:49.273+00:00", "last_seen_at": "2026-09-23T21:06:49.273+00:00", "origin_region_codes": null, "global_confidence": null, "exemplar_count": 6, "scrape_status": "success", "id": "04a9f0c1-988c-4f38-844a-3e15e9a80b4e", "trend_id": "745ff63d-09d7-483b-94e2-3c7a6bf5c527", "trend_group_id": "7cf7a841-e7a7-4844-8ac4-ed9823615bae", "top_exemplars": [ { "video_id": "411eabfc-1074-4c5a-b04c-c4deb43174b6", "url": "https://www.tiktok.com/@9news/video/7687586729653783815", "platform": "tiktok", "views": 457017, "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/c3cdffed0e38dd1c3ac48e836c169367feaca911cc77fb2708bf40fd328663f0.jpg", "publish_date": "2026-09-20T12:02:55", "author": { "username": "9news", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/efb8c840b5308a468320874e1f818cfb2214455b4b9afa8e4ea31828454f902f.heic", "verified": false } } ] } ] } ] } ``` **Response 400 Bad input:** ```json { "message": "Unknown region \"xx\". Available regions: au, gb, global, sg, us", "error": "Bad Request", "statusCode": 400, "code": "unknown_region" } ``` **Full field reference** Times are UTC. Each daily list has `id`, `region`, `trends` and: - `title` (string, optional): Display text. [Trends by date](https://dev.virlo.ai/docs/trends#get-trends) shows the UTC start date, a day early for `au` and `sg`. Use `local_date`. - `local_date` (string | null, optional): The list's day in the region's timezone. `null` on `global` lists before July 7, 2026, and a few from August 2026. Each trend has: - `ranking` (integer, optional): 1 has the most momentum. - `trend` (object, optional): `id`, `name`, `description` and `trend_type` (`content` since mid-October 2025, older ones `twitter`, `native` or `null`). - `momentum` (object | null, optional): See [Momentum](https://dev.virlo.ai/docs/trends#momentum). Always present, `null` until the first check. - `top_exemplars` (array, optional): Example videos, most-viewed first. - `exemplar_count` (integer, optional): Total example videos linked. - `detected_at` (string | null, optional): When the trend entered the list. Carried-over trends keep it, so it can predate `local_date`. - `last_seen_at` (string | null, optional): The last run that found it. If older than today's list, the trend carried over unconfirmed. - `id` (string, optional): This entry. New each day. - `trend_id` (string, optional): The trend itself, stable across days. Store this one. - `trend_group_id` (string, optional): The list it came from. - `velocity_today_count` (integer | null, optional): Videos on the topic in the 24 hours before Virlo found it. A one-time snapshot, often `0` or `null`. - `velocity_median_views` (integer | null, optional): Their median views. - `origin_region_codes` (string[] | null, optional): On `global`, the countries that also had the trend, like `["gb", "us"]`. - `global_confidence` (number | null, optional): How sure Virlo is (0 to 1) that those are one story. - `scrape_status` (string | null, optional): Whether example videos are linked yet: `pending`, `success` or `failed`. Each example video has `video_id`, `url`, `platform` (`tiktok`, `youtube` or `instagram`), `views`, `thumbnail_url`, `publish_date` (can be much older than the trend) and `author` (`username`, `avatar_url`, `verified`). ## Get emerging trends **Endpoint:** `GET https://api.virlo.ai/v1/trends/emerging` Returns only the `new` and `rising` trends from a region's latest list, highest momentum first. Cost per request: $0.25 - You get one flat list, not daily lists. Often fewer than `limit`, sometimes none (still $0.25). - Momentum comes as flat fields: `status`, `momentum_score` and `views_per_hour`. - `region` (string, optional): Region code. Default `global`. - `limit` (integer, optional): Most trends to return, a whole number from 1 to 50. Default 20. - `top_exemplars` (integer, optional): Example videos per trend, 0 to 20. Default 5. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/trends/emerging \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=gb \ -d limit=20 ``` **Response 200 OK:** ```json { "region": "gb", "generated_at": "2026-09-24T17:26:34.614Z", "data": [ { "ranking": 1, "trend": { "id": "d968f93c-be19-4598-9100-cad47a732a4c", "name": "Drama & memes around Rory Stewart BBC Newsnight moment", "description": "On September 18, 2026, former MP Rory Stewart appeared on BBC's Newsnight, where he dramatically spun in his chair to stare at Ailbhe Rea...", "created_at": "2026-09-24T06:05:44.925004+00:00", "trend_type": "content" }, "status": "rising", "momentum_score": 1, "views_per_hour": 257371, "region": "gb", "detected_at": "2026-09-24T06:05:45.122+00:00", "last_seen_at": "2026-09-24T12:06:30.905+00:00", "exemplar_count": 13, "id": "8ff9f920-2804-4f6a-91ec-08cddfa5d7db", "trend_id": "d968f93c-be19-4598-9100-cad47a732a4c", "top_exemplars": [ { "video_id": "367da5d1-5510-4e73-b9a5-fd6207e9915e", "url": "https://www.tiktok.com/@thesun/video/7688710703854767392", "platform": "tiktok", "views": 5050311, "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/5f78a37402cb781959c245240f08b7ea1672b0867346cf35771adb32f177e612.jpg", "publish_date": "2026-09-23T12:44:27", "author": { "username": "thesun", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/7a926491c16af666224735afecb62c829a4e11f774fd221b2514238a4ef6a231.webp", "verified": false } } ] } ] } ``` **Response 400 Bad input:** ```json { "message": ["limit must not be greater than 50"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 429 Too many calls:** ```json { "statusCode": 429, "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "error": "Too Many Requests", "limit": 60, "remaining": 0, "reset_at": 1790271060, "reset_at_formatted": "September 24th, 2026, 5:31 PM UTC", "retry_after": 54 } ``` **Full field reference** Top level: `region`, `generated_at`, `data`. Each trend has `region`, `id`, `trend_id`, `detected_at`, `last_seen_at`, `exemplar_count` and `top_exemplars` as on [today's trends](https://dev.virlo.ai/docs/trends#get-trends-digest), plus: - `ranking` (integer, optional): Position in the full daily list, so numbers can skip. - `trend` (object, optional): Adds `created_at`, when Virlo first recorded the trend. It can be much older than `detected_at`. - `status` (string, optional): `new` or `rising`. Before the first check it is estimated, so a just-found trend shows `rising`, not `new`. - `momentum_score` (number, optional): Like `momentum.score`. Before the first check it is an estimate (0.7 or 0.9) where the daily list shows `null`. Trends with under 3 example videos keep it. - `views_per_hour` (integer | null, optional): `null` before the first check. ## Get trends by date **Endpoint:** `GET https://api.virlo.ai/v1/trends` Returns one region's daily lists in a date range, newest first. With no dates, you get the last 24 hours: normally today's list, topped up to 15. Date-range lists are not topped up. Cost per request: $0.25 ### How the dates work Dates match each list's 7:00 local start, in UTC, and a plain date means midnight UTC. So for `global`, `us` and `gb`, set `end_date` to the day after your last day. `au` and `sg` lists start the evening before in UTC, so set `start_date` to the day before your first day instead. History starts September 15, 2025 for `global`, July 6, 2026 for `us` and `gb`, July 7 for `au` and July 29 for `sg`. Before July 7, 2026, `global` lists have no `local_date`, and a day can have two. Read the day from `title` and tell them apart by `id`. - `region` (string, optional): Region code. Default `global`. - `start_date` (string, optional): `2026-07-14` or a UTC time like `2026-07-14T00:00:00Z`. Default 24 hours ago. - `end_date` (string, optional): Same format. Default now. - `limit` (integer, optional): Most daily lists to return, 1 to 100. Default 50. Higher counts as 100. - `top_exemplars` (integer, optional): Example videos per trend, 0 to 20. Default 5. This gets UK lists for July 14 to 16. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/trends \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=gb \ -d start_date=2026-07-14 \ -d end_date=2026-07-17 ``` **Response 200 OK:** ```json { "data": [ { "id": "8108e2f4-3a40-4f85-9f2a-3c9ec1ccf79c", "title": "Trends for Jul 16th", "region": "gb", "local_date": "2026-07-16", "trends": [ { "ranking": 1, "trend": { "id": "7002bfb2-8261-4c64-9921-c6797533e8ba", "name": "World Cup Final Tease & Pre-Show Buzz (Banner & Flags)", "description": "Ahead of the 2026 FIFA World Cup final on July 19, 2026, excitement is building as Argentina, led by Lionel Messi, prepares to face Spain...", "trend_type": "content" }, "momentum": { "score": 1, "status": "rising", "views_per_hour": 2617163, "updated_at": "2026-07-17T05:00:00.708Z" }, "velocity_today_count": 96, "velocity_median_views": 329031, "detected_at": "2026-07-16T12:05:34.855+00:00", "last_seen_at": "2026-07-16T12:05:34.855+00:00", "origin_region_codes": null, "global_confidence": null, "exemplar_count": 144, "scrape_status": "success", "id": "2460d76e-5641-43a0-afb7-82f4e0cd33f7", "trend_id": "7002bfb2-8261-4c64-9921-c6797533e8ba", "trend_group_id": "8108e2f4-3a40-4f85-9f2a-3c9ec1ccf79c", "top_exemplars": [ { "video_id": "a4397cf9-35b9-45ec-a4e3-7ab414c2aadf", "url": "https://www.tiktok.com/@fifaworldcup/video/7662504667452099862", "platform": "tiktok", "views": 154919730, "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/2fdf43d6-0a47-4616-a21f-0c7dc51c67b8.jpg", "publish_date": "2026-07-14T21:51:39", "author": { "username": "fifaworldcup", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/ccd523186bc6d0b89762cb4bb4c9c7d60bc6266497dc5ddedb24ae3c5b50e0f5.heic", "verified": false } } ] }, { "ranking": 9, "trend": { "id": "1e15ebc7-7b09-4118-a092-c5788cc3728d", "name": "New Music from ROLE MODEL", "description": "On July 10, 2026, Role Model (Tucker Pillsbury) released his new single 'Joy,' the second track from his upcoming album...", "trend_type": "content" }, "momentum": null, "velocity_today_count": 0, "velocity_median_views": 0, "detected_at": "2026-07-16T06:04:53.03+00:00", "last_seen_at": "2026-07-16T06:04:53.03+00:00", "origin_region_codes": null, "global_confidence": null, "exemplar_count": 2, "scrape_status": "success", "id": "70a03e12-4624-4641-a84b-37a0a10ca6fc", "trend_id": "1e15ebc7-7b09-4118-a092-c5788cc3728d", "trend_group_id": "8108e2f4-3a40-4f85-9f2a-3c9ec1ccf79c", "top_exemplars": [ { "video_id": "6d6bd156-c88f-4de0-bc9a-54ef6493506c", "url": "https://www.tiktok.com/@popgirly.era/video/7660846467308916000", "platform": "tiktok", "views": 136426, "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/8ca67ff9-a8e5-4af5-af10-e6111b615ba9.jpg", "publish_date": "2026-07-10T10:36:58", "author": { "username": "popgirly.era", "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/fd82bc9bebdbb037c7162c4c33a8f1702e6e3d356df52436d0eaa04af19ddc7c.webp", "verified": false } } ] } ] } ] } ``` **Response 400 Bad input:** ```json { "message": ["start_date must be a valid ISO 8601 date string"], "error": "Bad Request", "statusCode": 400, "code": "invalid_date_range" } ``` The fields match [Get today's trends](https://dev.virlo.ai/docs/trends#get-trends-digest). ## List regions **Endpoint:** `GET https://api.virlo.ai/v1/trends/regions` Free. Returns every region code with its name and timezone. New regions get added, so check here. `AU` and `au` both work. **cURL request:** ```bash curl https://api.virlo.ai/v1/trends/regions \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200 OK:** ```json { "data": [ { "code": "au", "name": "Australia", "timezone": "Australia/Sydney" }, { "code": "gb", "name": "United Kingdom", "timezone": "Europe/London" }, { "code": "global", "name": "Global", "timezone": "America/New_York" }, { "code": "sg", "name": "Singapore", "timezone": "Asia/Singapore" }, { "code": "us", "name": "United States", "timezone": "America/New_York" } ] } ``` ## Errors Failed requests count toward your rate limit. - `400 Bad Request` - A bad date (`invalid_date_range`). Use `2026-07-14`. - `limit` below 1 or not a number. Decimals round down, except on emerging, where decimals and values over 50 fail. - `top_exemplars` outside 0 to 20, or not a whole number. - An unknown `region` (`unknown_region`). - A parameter these calls don't take, like `platform`. `message` is an array of strings, except for `unknown_region`. - `401 Unauthorized`: Missing or invalid API key. - `402 Payment Required`: Balance too low. [Add funds](https://dev.virlo.ai/dashboard/billing). - `429 Too Many Requests`: Wait `retry_after` seconds. Today's trends and trends by date each allow 50 a minute, 500 an hour and 5,000 a day. Emerging allows 60, 600 and 6,000. Some accounts have lower limits; the `X-RateLimit-Limit` header shows yours. More in [Errors](https://dev.virlo.ai/docs/errors) and [Rate limits](https://dev.virlo.ai/docs/rate-limits). --- # Sounds > Find trending and breakout sounds, search sounds by title, look up one sound (its videos, daily usage, and the real song behind it), and list every sound credited to an artist or creator across TikTok, YouTube, and Instagram. Source: https://dev.virlo.ai/docs/sounds See which songs and audio clips (sounds) are catching on across TikTok, YouTube, and Instagram, then dig into one. **At a glance** - **What it does:** Lists trending and breakout sounds, and looks up one sound or an artist's catalog. - **You send:** A search word, a sound's `id`, an artist's name, or nothing for the trending lists. - **You get back:** Sounds with usage numbers, or one sound's videos, daily history, and real song. - **Cost:** $0.05 to $0.25 per request, even when nothing matches. Real-song matching adds $0.10 once per sound. Errors and Agent sounds are free. - Sounds used most this week: [Trending sounds](https://dev.virlo.ai/docs/sounds#trending-sounds) - Sounds suddenly taking off: [Breakout sounds](https://dev.virlo.ai/docs/sounds#breakout-sounds) - A sound, found by its title: [Search sounds](https://dev.virlo.ai/docs/sounds#search-sounds) - One sound's stats and real song: [Sound details](https://dev.virlo.ai/docs/sounds#sound-details) - Top videos using a sound: [Sound videos](https://dev.virlo.ai/docs/sounds#sound-videos) - How a sound grew day by day: [Usage history](https://dev.virlo.ai/docs/sounds#usage-history) - Every sound credited to an artist: [Creator sounds](https://dev.virlo.ai/docs/sounds#creator-sounds) - Sounds in your agent's videos: [Agent sounds](https://dev.virlo.ai/docs/sounds#agent-sounds) These use Virlo's stored data. For fresh videos on one TikTok or Instagram sound, use [Sound lookup](https://dev.virlo.ai/docs/satellite/sounds): $0.50, or $1.00 with trend analysis. --- ## Sound basics **Sound IDs.** `id` is Virlo's ID for a sound. Copy it from any list below or from a video's `sound` field. `external_id` is the platform's own ID and returns `404` where `id` is expected. **Which count to trust.** `usage_count` is TikTok's all-time count of videos using the sound. Other counts only cover Virlo's data. To compare sounds, use `videos_in_window` (Trending) or `videos_7d` (Breakout). `video_count` (Trending and Creator sounds) and `total_videos` (sound details) count every Virlo video using the sound, all time. They and `avg_views` update within a few minutes. **Platforms.** TikTok has the fullest data. YouTube and Instagram sounds never have `usage_count` and are never marked commerce music. YouTube has no `duration`. Any field can be `null`, and `owner_handle` often is. **Images.** `cover_url`, `thumbnail_url`, and `avatar_url` are full links, or `null` when there is no image. Files Virlo stores start with `https://auth.virlo.ai/storage/v1/object/public/`; others are platform links. Some files are HEIC (`.heic`), which most browsers other than Safari can't show, so convert them before display. **Pages.** On Trending's 7-day, 30-day and `video_count` sorts, Breakout, and Agent sounds, `pagination.total` is not a real count. Keep paging while `has_next_page` is `true` ([Pagination](https://dev.virlo.ai/docs/pagination)). **Errors.** Most bad options return a free `400`, and an unknown sound a free `404` ([Errors](https://dev.virlo.ai/docs/errors)). Some typos are ignored and still charged: a `commerce_only` other than `true`, a misspelled Creator sounds `platform`, and unknown options on Sound details. **Fields on every sound** Every sound has `id`, `external_id`, `title`, `platform`, `duration` (seconds), `cover_url`, `owner_handle`, `owner_nickname` (who the platform credits, not always the artist), `is_original`, `is_commerce_music` (`true` when TikTok clears it for ads and branded posts), and `usage_count`. --- ## Trending sounds **Endpoint:** `GET https://api.virlo.ai/v1/sounds/trending` The sounds used in the most new videos, over the last 7 days by default. Set `platform`, or YouTube sounds often fill the top. Cost per request: $0.25 ### Query parameters - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. Default: all three. - `sort` (string, optional): `videos_7d` (default) or `videos_30d`: most new videos in that window. `usage_count`: most used all time (TikTok first). `video_count`: most Virlo videos using the sound, all time, across the whole catalog. Sounds with none are left out. - `commerce_only` (boolean, optional): Exactly `true` keeps only TikTok sounds cleared for ads. Other values, like `yes`, are ignored and you pay for the full list. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **Full field reference** Each result has the [fields on every sound](https://dev.virlo.ai/docs/sounds#sound-basics), plus `video_count`, `avg_views`, and `videos_in_window` (the ranking number, on the 7-day and 30-day sorts only). **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/trending \ -H "Authorization: Bearer YOUR_API_KEY" \ -d platform=tiktok \ -d sort=videos_7d \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "53927570-5593-4bfe-b604-496d5aabd328", "external_id": "7668271352941742081", "title": "Vibin", "platform": "tiktok", "duration": 60, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/13cb83d12e080a4b9e2609f79bebf1619b7700e6c2988b4a2c2ea8fe53819b61.jpg", "owner_handle": null, "owner_nickname": "Wxoda", "is_original": false, "is_commerce_music": false, "usage_count": 245774, "video_count": 214, "avg_views": 48210, "videos_in_window": 51 } ], "pagination": { "page": 1, "limit": 20, "total": 21, "total_pages": 2, "has_next_page": true, "has_prev_page": false }, "sort": "videos_7d" } ``` **Response 400:** ```json { "message": ["platform must be one of the following values: tiktok, youtube, instagram"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` --- ## Breakout sounds **Endpoint:** `GET https://api.virlo.ai/v1/sounds/breakout` Sounds suddenly taking off from a quiet start, like 20 videos this week after about 1 a week before. Results rank by `acceleration`: `(videos_7d + 1) / (prior_weekly + 1)`, where `prior_weekly` is the weekly average over days 8 to 35 ago. A sound needs at least `min_recent` videos this week, between `min_baseline` and 200 in the 4 weeks before, and an `acceleration` of 2+. Sounds above 200 show only in Trending. Cost per request: $0.25 ### Query parameters - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. Default: all three. - `commerce_only` (boolean, optional): Exactly `true` keeps only TikTok sounds cleared for ads, as in Trending. - `min_recent` (number, optional): Minimum videos in the last 7 days. Default 3. - `min_baseline` (number, optional): Minimum videos in days 8 to 35 ago. Default 3. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **Full field reference** Each result has the [fields on every sound](https://dev.virlo.ai/docs/sounds#sound-basics), plus `videos_7d`, `videos_30d`, `videos_90d`, `prior_weekly`, and `acceleration` (ties go to more `videos_7d`). Two older scores always come back too: `burst_ratio` (`videos_7d / videos_90d`) and `breakout_score` (`videos_7d * burst_ratio`). **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/breakout \ -H "Authorization: Bearer YOUR_API_KEY" \ -d platform=tiktok \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "f3c375de-0cac-4a4f-a23e-7fdfc13874d1", "external_id": "7678293159960676415", "title": "Courtly Elegance Boccherini", "platform": "tiktok", "duration": 45, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/5a2e780f5bd991284fac706d04d0c0a3edc3787c057e21b8db823df33c9beb07.jpg", "owner_handle": null, "owner_nickname": "Lucien Marceau", "is_original": false, "is_commerce_music": false, "usage_count": 207622, "videos_7d": 10, "videos_30d": 13, "videos_90d": 13, "burst_ratio": 0.7692, "breakout_score": 7.6923, "prior_weekly": 0.75, "acceleration": 6.286 } ], "pagination": { "page": 1, "limit": 20, "total": 21, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` --- ## Search sounds **Endpoint:** `GET https://api.virlo.ai/v1/sounds/search` Find sounds whose title contains your text exactly as typed. Case doesn't matter, but spelling and word order do: `lofi beats` finds "chill stylish lofi beats", while `lofi beets` finds nothing but is still charged. When unsure, search one distinctive word. Results sort by `usage_count`, so TikTok comes first. Cost per request: $0.10 ### Query parameters - `q` (string, required): At least one word of 3 or more letters or digits: `dj snake` works, `dj` alone is a 400 `validation_error`. Commas and parentheses count as spaces. - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. Default: all three. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/search \ -H "Authorization: Bearer YOUR_API_KEY" \ -d q=lofi+beats \ -d platform=tiktok \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "81a0c517-fbb2-45ca-b2f5-d6d62d3e1a14", "external_id": "7358901665129662465", "title": "chill stylish lofi beats(1535963)", "platform": "tiktok", "duration": 190, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/a6c30ceb5263d65db6be1b7161e24c7438ac7a97554f4ed436efe455bef948f5.jpg", "owner_handle": null, "owner_nickname": "Enokido", "is_original": false, "is_commerce_music": true, "usage_count": 50960 } ], "pagination": { "page": 1, "limit": 20, "total": 9, "total_pages": 1, "has_next_page": false, "has_prev_page": false } } ``` **Response 200 No matches:** ```json { "data": [], "pagination": { "page": 1, "limit": 20, "total": 0, "total_pages": 0, "has_next_page": false, "has_prev_page": false }, "note": "Sound search coverage grows daily as our dataset expands. Try broader terms or check back soon." } ``` --- ## Sound details **Endpoint:** `GET https://api.virlo.ai/v1/sounds/:sound_id` One sound's stats (video count, average views, top video), plus the real song behind it if you ask. Cost per request: $0.05 ### Find the real song Add `resolve=true` to get the artist, the ISRC (a recording's standard code), and a Spotify ID when Spotify made the match. The answer is in `track_resolution` (always present). - **Cost.** The first match adds $0.10 ($0.15 total), found or not, once per sound. Later reads cost $0.05. - **Time.** A first match can take 15 seconds or more. If the match isn't done yet, `status` says `unresolved` or `pending` and you pay $0.05. Call again in about a minute. - **Already matched.** Virlo matches some sounds in the background. If `status` is `resolved` or `not_found`, `resolve=true` won't look again, so leave it off. ### Path parameters - `sound_id` (string, required): Virlo's sound `id`, not `external_id`. ### Query parameters - `resolve` (boolean, optional): `true` or `1`. Other values are ignored. **Full field reference** The [fields on every sound](https://dev.virlo.ai/docs/sounds#sound-basics), plus: - `total_videos` (integer, optional): Videos in Virlo's data using the sound, all time. - `avg_views` (integer, optional): Their average current views. Videos with no view count count as 0. - `top_video_url` (string | null, optional): The most-viewed one. - `track_resolution.status` (string, optional): `unresolved` (not matched yet), `pending` (matching now), `resolved`, or `not_found` (no confident match). - `track_resolution.artist_name` (string | null, optional): The artist. Can differ from `owner_nickname`. - `track_resolution.spotify_track_id` (string | null, optional): An ID, not a link: add it to `https://open.spotify.com/track/`. Only filled when Spotify (or sometimes audio fingerprinting) matched. `spotify_artist_id` works the same with `/artist/`. - `track_resolution.apple_music_id` (string | null, optional): Not filled today: always `null`. - `track_resolution.resolution_source` (string | null, optional): Who matched it. Catalogs first (`spotify`, `deezer`, `musicbrainz`), then `acrcloud` (audio fingerprint) and `llm_websearch` (AI web search). - `track_resolution.confidence` (number | null, optional): 0 to 1. Also returned: `isrc`, `release_status` (`released`, `unreleased`, or `unknown`), `release_date`, `resolved_at`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/8f6e5c50-1451-4007-a120-92744e632dad \ -H "Authorization: Bearer YOUR_API_KEY" \ -d resolve=true ``` **Response 200:** ```json { "data": { "id": "8f6e5c50-1451-4007-a120-92744e632dad", "external_id": "7171140178143266818", "title": "I'll Never Let You Go", "platform": "tiktok", "duration": 62, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/8bef57e2f1987f109f345a7b603ab2635277e7cb4f37a26ada16c11d07252572.jpg", "owner_handle": null, "owner_nickname": "BCD Studio", "is_original": false, "is_commerce_music": true, "usage_count": 86631999, "total_videos": 633, "avg_views": 1596202, "top_video_url": "https://www.tiktok.com/@cristineni34/video/7177832058478234885", "track_resolution": { "status": "resolved", "artist_name": "BCD Studio", "isrc": "SGB502290414", "spotify_track_id": "4fDHlmlEWbJnHa7dO5MZwW", "spotify_artist_id": "6ENUuaqy7QqbKD4M1X3siN", "apple_music_id": null, "release_status": "released", "resolution_source": "spotify", "release_date": null, "confidence": 1, "resolved_at": "2026-07-02T01:07:01.982+00:00" } } } ``` **Response 404:** ```json { "message": "Sound not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Sound videos **Endpoint:** `GET https://api.virlo.ai/v1/sounds/:sound_id/videos` Videos in Virlo's data that use a sound, most-viewed first, to see how creators use it. Cost per request: $0.25 ### Path parameters - `sound_id` (string, required): Virlo's sound `id`, not `external_id`. ### Query parameters - `sort` (string, optional): `views_desc` (default): most views first. `publish_date_desc`: newest first. - `platform` (string, optional): `tiktok`, `youtube`, or `instagram`. Rarely needed. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **Full field reference** Each video has `id`, `url`, `description` (can be `""`), `platform`, `views`, `likes`, `shares`, `comments`, `bookmarks` (saves), `hashtags`, `is_duet`, `is_stitch`, and: - `publish_date` (string, optional): No time zone marker. Read it as UTC. - `thumbnail_url` (string | null, optional): A full link, often `null` (see [Images](https://dev.virlo.ai/docs/sounds#sound-basics)). - `author` (object | null, optional): `username`, `verified`, `followers` (can be `null`), `avatar_url`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/8f6e5c50-1451-4007-a120-92744e632dad/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -d sort=views_desc \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "43721b48-f4f0-4ae4-8677-3f187aac28a4", "url": "https://www.tiktok.com/@cristineni34/video/7177832058478234885", "description": "", "platform": "tiktok", "views": 23135811, "likes": 600093, "shares": 228445, "comments": 15982, "bookmarks": 32971, "publish_date": "2022-12-16T19:34:22", "hashtags": [], "thumbnail_url": null, "is_duet": false, "is_stitch": false, "author": { "username": "cristineni34", "verified": false, "followers": 764158, "avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/8ed0b0d3-b8af-478a-b20f-d27e65bd1d0c.webp" } } ], "pagination": { "page": 1, "limit": 20, "total": 633, "total_pages": 32, "has_next_page": true, "has_prev_page": false } } ``` --- ## Usage history **Endpoint:** `GET https://api.virlo.ai/v1/sounds/:sound_id/usage-history` A day-by-day record of one sound, from about one snapshot a day: TikTok's count, Virlo's count, and the change since the snapshot before. **Always set `start_date`.** Snapshots come oldest first, and `limit` keeps the oldest. With no dates you get the first 90 ever recorded, which can be months old. Cost per request: $0.05 No `pagination`. ### Path parameters - `sound_id` (string, required): Virlo's sound `id`, not `external_id`. ### Query parameters - `start_date` (string, optional): First day to include (YYYY-MM-DD). - `end_date` (string, optional): Last day to include (YYYY-MM-DD). - `limit` (number, optional): 1 to 365. Default 90. **Full field reference** Each snapshot has `usage_count` (TikTok only), `video_count_local`, `delta_usage_count`, `delta_video_count_local`, and `snapshot_at` (UTC). Deltas compare with the previous row in this response, so the first row's are always `null`. A stray `0` snapshot shows as a fake drop and jump back. Ignore it. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/8f6e5c50-1451-4007-a120-92744e632dad/usage-history \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-09-22 \ -d end_date=2026-09-24 ``` **Response 200:** ```json { "data": [ { "usage_count": 83700000, "video_count_local": 628, "delta_usage_count": null, "delta_video_count_local": null, "snapshot_at": "2026-09-22T05:00:00.015+00:00" }, { "usage_count": 84981718, "video_count_local": 628, "delta_usage_count": 1281718, "delta_video_count_local": 0, "snapshot_at": "2026-09-23T05:00:00.027+00:00" }, { "usage_count": 86353566, "video_count_local": 630, "delta_usage_count": 1371848, "delta_video_count_local": 2, "snapshot_at": "2026-09-24T05:00:00.019+00:00" } ] } ``` --- ## Creator sounds **Endpoint:** `GET https://api.virlo.ai/v1/sounds/by-creator/:platform/:handle` Every sound credited to one artist or creator (their catalog), not the sounds they use in their videos. Your text is checked against each sound's credited handle, display name, and [matched artist](https://dev.virlo.ai/docs/sounds#sound-details). **Start with the display name:** `tiktok/Kygo` finds 7 sounds, while the handle `tiktok/kygomusic` finds none. Every try is charged, even an empty one or a misspelled `platform`. Names match whole and ignore case: `kygo` finds Kygo's sounds, `Kyg` finds nothing. `%` and `_` are ordinary characters, not wildcards. Cost per request: $0.25 ### Path parameters - `platform` (string, required): `tiktok`, `youtube`, or `instagram`. - `handle` (string, required): Display name or handle, `@` optional. Spaces become `%20`: `Atomica%20Music`. ### Query parameters - `sort` (string, optional): `usage_count` (default). `video_count`: most Virlo videos, across the artist's whole catalog. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **Full field reference** Each sound has the [fields on every sound](https://dev.virlo.ai/docs/sounds#sound-basics), plus `video_count` and `avg_views`. The response also has `aggregates`: - `aggregates.total_sounds` (integer, optional): Sounds in the whole catalog. - `aggregates.total_ugc_videos` (integer, optional): Sum of `video_count` **on this page only**, including the artist's own videos. - `aggregates.total_usage_count` (integer, optional): Sum of `usage_count` **on this page only**. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/sounds/by-creator/tiktok/Kygo \ -H "Authorization: Bearer YOUR_API_KEY" \ -d sort=usage_count \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "ede0f487-8998-45b5-aef0-802a83c1e821", "external_id": "7094381072426747905", "title": "Freeze", "platform": "tiktok", "duration": 60, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/fab1fb05c9863b5df89f8d81f22c109fa0e9d9b22c81bd36f0d5863d0a3318ef.jpg", "owner_handle": null, "owner_nickname": "Kygo", "is_original": false, "is_commerce_music": true, "usage_count": 8492, "video_count": 1, "avg_views": 433216 } ], "pagination": { "page": 1, "limit": 20, "total": 6, "total_pages": 1, "has_next_page": false, "has_prev_page": false }, "aggregates": { "total_sounds": 6, "total_ugc_videos": 278, "total_usage_count": 28183 } } ``` --- ## Agent sounds **Endpoint:** `GET https://api.virlo.ai/v1/agents/:agent_id/sounds` The sounds used most in the videos your [Content Research Agent](https://dev.virlo.ai/docs/agents) collected. **Free.** Here, `video_count` counts videos in the agent's results. ### Path parameters - `agent_id` (string, required): Your agent's ID. A malformed ID returns `400`, and an agent you don't own returns `404`. ### Query parameters - `sort` (string, optional): `video_count` (default), `usage_count`, or `rising` (most new videos since the previous run; old name `growth_7d`). Unknown values fall back to the default. - `limit` (number, optional): 1 to 100. Default 20. - `page` (number, optional): Default 1. **Full field reference** The [fields on every sound](https://dev.virlo.ai/docs/sounds#sound-basics), plus `video_count`, `avg_views`, and: - `growth_video_count` (integer | null, optional): Change in `video_count` since the previous run, or `null` with no earlier run. - `growth_views` (integer | null, optional): Change in their total views, also `null` with no earlier run. - `lifecycle` (string, optional): Total views against the previous run: `rising` (up 25% or more), `fading` (down 25% or more), `steady`, or `new` (no earlier run). **cURL request:** ```bash curl -G https://api.virlo.ai/v1/agents/2b1f9c3d-7c4e-4d8a-9f12-6e8b4a2c1d05/sounds \ -H "Authorization: Bearer YOUR_API_KEY" \ -d sort=rising \ -d limit=20 ``` **Response 200:** ```json { "data": [ { "id": "9479ec9f-2150-40a9-8088-23b4b7979a76", "external_id": "7274023553422772278", "title": "Million Dolla Hip Hop", "platform": "tiktok", "duration": 188, "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/acc0a71eb81d1cbb7bc487047488722a2671dfc2f448d1c8b844ec6900ca3156.jpg", "owner_handle": null, "owner_nickname": "Brentin Davis", "is_original": false, "is_commerce_music": false, "usage_count": 86590, "video_count": 2, "avg_views": 53877, "growth_video_count": 1, "growth_views": 31792, "lifecycle": "rising" } ], "pagination": { "page": 1, "limit": 20, "total": 21, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` **Response 404:** ```json { "message": "Agent not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- --- # Hooks > See which opening lines work on short-form video. Look up real hooks from about 1.4M TikTok, YouTube and Instagram posts with their views and Virality Score, find which hook types win in your niche, and browse 4,100+ hook templates. Source: https://dev.virlo.ai/docs/hooks The hook is what a short-form post says or shows in its first seconds. Virlo has the hooks of about 980,000 videos and 440,000 photo slideshows from TikTok, YouTube and Instagram, with how each post did. **At a glance** - **What it does:** Shows which openings work on short-form video, with real examples and templates for writing your own. - **You send:** Optional filters. Search also needs a phrase or a hook type, and agent hooks needs your agent's ID. - **You get back:** Real hooks with their posts (views, creator, link), a scoreboard of hook types, or templates. - **Cost:** Free: hook type stats and library categories. $0.10: templates. $0.25: everything else (agent hooks are free with Data Intelligence, or before any are found). Each page of results is charged. ## Which one do I need? - **Which kinds of hooks win:** [hook types and stats](https://dev.virlo.ai/docs/hooks#hook-taxonomy-stats) - **Top hooks of the last 7, 14 or 30 days:** [trending](https://dev.virlo.ai/docs/hooks#trending-hooks) - **Search a phrase, find who posted a hook, or see a type's best:** [search](https://dev.virlo.ai/docs/hooks#search-hooks) - **The best hooks in your [Content Research Agent](https://dev.virlo.ai/docs/agents)'s videos:** [agent hooks](https://dev.virlo.ai/docs/hooks#agent-hooks) - **Templates for writing your own:** the [library](https://dev.virlo.ai/docs/hooks#hook-template-library) ## Key terms - **Hook** (`hook_text`): the opening, word for word. For a video, the first line spoken in about the first 6 seconds, else the on-screen text in the opening shot. For a slideshow, the first slide's text. - **Virality Score** (`weighted_score`): how far a post beat its creator's follower count, with extra credit for big accounts, so every creator size and platform shares one scale. Formula: ln(views ÷ followers) × ln(followers). - 35 and up: exceptional - 25 to 35: very strong - 18 to 25: strong - 10 to 18: promising - 0 to 10: more views than followers, but not a standout - Below 0: fewer views than followers (about half of all videos) - **Outlier ratio** (`outlier_ratio`): views ÷ followers. - **Usage count** (`usage_count`): how many posts across Virlo open with the same words. `1` means an original line. - **Strong hit rate** (`strong_hit_rate`): the share of a hook type's videos scoring 18 or more. `0.28` means 28%. ## Hook types, explained Every hook has a `hook_type` (what the opening line does). Videos usually also have a `visual_hook_type` (what the opening shot shows). Slideshows and some videos have no visual type (`null` or `none`) but still appear in results. Videos with no clear hook get `hook_type` `none`, shown only in the stats and never filterable. **Hook types:** `question`, `bold_claim`, `shock_statement`, `story_tease`, `tutorial_promise`, `controversy` (takes a side), `before_after` (a transformation reveal), `pov_setup` ("POV:"), `statistic`, `direct_address` ("If you have oily skin…"), `trend_reference`, `cliffhanger` (holds back the payoff), `negation` ("Stop doing this"), `relatable_scenario`, `comparison` and `mystery_setup` (a puzzle to solve). Virlo adds new hook types over time, so handle values you don't recognize. [Hook types and stats](https://dev.virlo.ai/docs/hooks#hook-taxonomy-stats) lists every type that has videos. **The 12 visual hook types:** `text_hook` (big on-screen text), `extreme_closeup`, `before_state`, `shocking_image`, `aesthetic_setup`, `person_speaking_to_camera`, `motion_action`, `crowded_scene`, `mystery_object`, `dramatic_zoom`, `animal_pet` and `product_reveal` (a product held up, unboxed or shown close up). ## Content categories The `category` filter takes only these 28 values. The template library has [its own categories](https://dev.virlo.ai/docs/hooks#library-categories). `art_design`, `automotive`, `beauty`, `business_career`, `crafts_diy`, `education`, `entertainment`, `fashion`, `finance`, `fitness`, `food_beverage`, `gaming`, `health_wellness`, `home_garden`, `kids_content`, `lifestyle`, `music`, `news_politics`, `parenting_family`, `pets`, `real_estate`, `relationships_dating`, `religion_spirituality`, `science_nature`, `sports`, `tech`, `travel`, `other` --- ## Hook types and stats **Endpoint:** `GET https://api.virlo.ai/v1/hooks/types` Which kinds of hooks win most often, overall or on one platform or category. It lists every type plus `none`, minus types with no videos in your platform or category. Cost per request: Free - **Sort by `strong_hit_rate`** to see what works. Common is not the same as effective: across all videos, the two most-used types, `tutorial_promise` and `bold_claim`, have the two lowest hit rates. - **Check `video_count`.** A rare type can top the list on a tiny sample: `cliffhanger` leads overall with only 268 videos. - **Videos only**, not slideshows, refreshed daily. `share_of_corpus` is each type's share of the slice you asked for, so the rows add up to 1. ### Query parameters All optional. `page` and `limit` are rejected (`400`). - `dimension` (string, optional): `hook_type` (default) or `visual_hook_type`. - `platform` (string, optional): `tiktok`, `youtube` or `instagram`. - `category` (string, optional): One of the [28 content categories](https://dev.virlo.ai/docs/hooks#content-categories). - `sort` (string, optional): Highest first: `video_count` (default), `strong_hit_rate`, `median_weighted_score` or `p90_weighted_score`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/hooks/types?sort=strong_hit_rate" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "data": { "dimension": "hook_type", "platform": "all", "category": "all", "snapshot_date": "2026-09-24", "types": [ { "value": "cliffhanger", "video_count": 268, "share_of_corpus": 0.0003, "total_views": 189174617, "avg_views": 705875, "median_views": 8817, "avg_weighted_score": 5.77, "median_weighted_score": 8.65, "p90_weighted_score": 35.95, "strong_hit_rate": 0.2786 }, { "value": "pov_setup", "video_count": 43974, "share_of_corpus": 0.0449, "total_views": 32986509016, "avg_views": 750137, "median_views": 19811, "avg_weighted_score": 2.95, "median_weighted_score": 5.05, "p90_weighted_score": 35.93, "strong_hit_rate": 0.2729 } ] } } ``` --- ## Trending hooks **Endpoint:** `GET https://api.virlo.ai/v1/hooks/trending` The top hooks from posts published in the last 7, 14 or 30 days, ranked by Virality Score. Cost per request: $0.25 ### Query parameters All optional. - `days` (integer, optional): `7` (default), `14` or `30`. - `category` (string, optional): One of the [28 content categories](https://dev.virlo.ai/docs/hooks#content-categories). - `platform` (string, optional): `tiktok`, `youtube` or `instagram`. - `content_type` (string, optional): `video` (default), `slideshow` or `all`. Slideshows currently time out here with a `503`. Use [search](https://dev.virlo.ai/docs/hooks#search-hooks) with `content_type=slideshow` and a `hook_type` instead (all time, not just recent). - `hook_type, visual_hook_type` (string, optional): One of the [hook types](https://dev.virlo.ai/docs/hooks#hook-types-explained). An unknown value returns `400`. `visual_hook_type` leaves out all slideshows. - `language` (string, optional): Like `en` or `es`. - `min_views` (integer, optional): Minimum views. - `sort` (string, optional): `weighted_score` (default) or `views`. - `page, limit` (integer, optional): Defaults 1 and 20, `limit` up to 100. Only the top 1,000 results. **Details for developers** Search and agent hooks use this shape too. Each hook appears once, from its strongest post. - `brand_safety_tier` is `safe`, `low_risk` or `medium_risk` (riskier and adult posts are left out). It can be `null` on some slideshows. - `usage_count` matches words, ignoring capitals and spacing but not punctuation. - `video.shares` is `null` on almost all YouTube posts, and `video.duration` on slideshows. - Only YouTube `author_handle` values start with `@`. **cURL request:** ```bash curl "https://api.virlo.ai/v1/hooks/trending?category=fitness&days=7&limit=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "data": [ { "hook_text": "10 signals your body is losing FAT", "hook_type": "tutorial_promise", "visual_hook_type": "text_hook", "content_type": "video", "platform": "tiktok", "category": "fitness", "language": "en", "brand_safety_tier": "safe", "outlier_ratio": 64.43, "weighted_score": 35.88, "usage_count": 1, "video": { "video_id": "7570d1bc-f0c6-4aa1-8f13-98fa919bcc2f", "url": "https://www.tiktok.com/@scully_fitness/video/7686773818983042325", "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/8ed30a72cd9c1f9ab952eca2a4cf6462e9583076bd2f69f43e572f790dd0ae77.jpg", "views": 354321, "likes": 10851, "comments": 34, "shares": 271, "duration": 6, "publish_date": "2026-09-18T07:28:22+00:00", "author_handle": "scully_fitness", "author_followers": 5499 } } ], "pagination": { "page": 1, "limit": 10, "total": 11, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` --- ## Search hooks **Endpoint:** `GET https://api.virlo.ai/v1/hooks/search` Finds hooks by phrase or by type, across all time: the top hooks built on a phrase like "nobody talks about", or the post a pasted hook came from (a reverse lookup). Needs a phrase (`q`) or a type (`hook_type`, `visual_hook_type`). Other filters alone are rejected (`400`). Cost per request: $0.25 ### Searching by phrase Results come in three groups, in this order, named in `match_type`: 1. **`exact`**: the hook is your text, ignoring capitals and spacing but not punctuation. Always first, even if the post did poorly. 2. **`contains`**: your words, whole and side by side, ignoring punctuation, emoji and line breaks between them. Highest Virality Score first. 3. **`similar`**: near-matches, closest first. Only for a `q` of 12+ characters, when the page isn't full. A paste whose punctuation between words differs lands in `contains`. Punctuation inside a word counts: `dont` won't find "don't". `match_score` (0 to 1) rates the whole hook against your text, so long hooks score low. Only `similar` is sorted by it. ### Browsing by type Without `q`, you get the strongest hooks of a type, all time, in mixed languages unless you set `language`. `match_type` and `match_score` are `null`. ### Query parameters `days` and `sort` are rejected (`400`). - `q` (string, optional): 3 to 200 characters, including a word of 3 or more letters or digits. - `content_type` (string, optional): `video` (default), `slideshow` or `all`. - `hook_type, visual_hook_type, platform, category, language, min_views, page, limit` (string | integer, optional): As on [trending](https://dev.virlo.ai/docs/hooks#trending-hooks). **Phrase request:** ```bash curl "https://api.virlo.ai/v1/hooks/search?q=nobody%20talks%20about&language=en&limit=10" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Phrase search:** ```json { "data": [ { "hook_text": "NOBODY TALKS ABOUT", "match_type": "exact", "match_score": 1, "weighted_score": -0.43, "usage_count": 1, "video": { "views": 2863, "author_handle": "hello.inertia" } }, { "hook_text": "Nobody talks about\nhow hard\nit is\nto go from this...", "match_type": "contains", "match_score": 0.422222, "weighted_score": 61.21, "usage_count": 4, "video": { "views": 7014838, "author_handle": "naysnetwork" } }, { "hook_text": "Nobody talks about hard it is\nto go from this...", "match_type": "contains", "match_score": 0.452381, "weighted_score": 53.64, "usage_count": 1, "video": { "views": 3838283, "author_handle": "mmadsgains" } } ], "pagination": { "page": 1, "limit": 10, "total": 11, "total_pages": 2, "has_next_page": true, "has_prev_page": false } } ``` --- ## Hook template library **Endpoint:** `GET https://api.virlo.ai/v1/hooks/library` More than 4,100 hand-picked hook templates in 33 categories, each with an example and a note on why it works. Most have `[blanks]` to fill in, and some are complete lines. They come in a fixed order, not ranked by performance. Cost per request: $0.10 You pay even when nothing matches. `tags` is currently always empty. ### Query parameters All optional. - `category` (string, optional): A `slug` from [library categories](https://dev.virlo.ai/docs/hooks#library-categories), like `educational-hooks`. A misspelled slug returns an empty list. - `psychology_tag` (string, optional): One tag, matched exactly, spaces and capitals included: `social proof` works, `social_proof` finds nothing. There are about 3,500 tags and no list. If a tag finds nothing, try `q`: it skips tags but searches the psychology note. - `q` (string, optional): Text in the template, example or psychology note, any case. 2 to 200 characters. - `page, limit` (integer, optional): Defaults 1 and 20, `limit` up to 100. `total` is exact. **cURL request:** ```bash curl "https://api.virlo.ai/v1/hooks/library?psychology_tag=curiosity&limit=5" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "data": [ { "hook_text": "Here's what happened when I [took big risk].", "example": "Here's what happened when I quit my job without a backup plan.", "psychology": "Fear + curiosity about bold decisions.", "psychology_tags": ["fear", "curiosity"], "tags": [], "library_category": { "slug": "case-studies-proof-hooks", "name": "Case Studies & Proof Hooks" } } ], "pagination": { "page": 1, "limit": 5, "total": 115, "total_pages": 23, "has_next_page": true, "has_prev_page": false } } ``` --- ## Library categories **Endpoint:** `GET https://api.virlo.ai/v1/hooks/library/categories` The 33 library categories, each with its template count. Pass a `slug` as the [library](https://dev.virlo.ai/docs/hooks#hook-template-library)'s `category`. `description` is always `null`. Cost per request: Free **cURL request:** ```bash curl "https://api.virlo.ai/v1/hooks/library/categories" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "data": [ { "slug": "educational-hooks", "name": "Educational Hooks", "description": null, "hook_count": 200 } ] } ``` --- ## Agent hooks **Endpoint:** `GET https://api.virlo.ai/v1/agents/:id/hooks` The strongest hooks in the videos your [Content Research Agent](https://dev.virlo.ai/docs/agents) has collected, ranked by Virality Score. $0.25 per request. Free while `coverage.videos_with_hooks` is `0`, and always free when the agent has [Data Intelligence](https://dev.virlo.ai/docs/intelligence) on (`data_intelligence_enabled: true`). - Videos only. Each video counts once. - `usage_count` counts matching hooks across all of Virlo, not just this agent. - `pagination.total` is exact, with no 1,000-result cap. - Unknown or someone else's agent: `404`. A malformed ID currently gives a `500` instead. ### Coverage `coverage.videos_collected` counts the agent's distinct videos, and `videos_with_hooks` those with an analyzed hook. Agents have hooks with or without Data Intelligence, but only analyzed videos have one. Off-topic, year-old and underperforming videos are not analyzed, so the second number is normally well below the first. `videos_with_hooks: 0` usually means a new agent or nothing analyzed yet: try [trending](https://dev.virlo.ai/docs/hooks#trending-hooks) meanwhile. ### Parameters `category`, `visual_hook_type`, `language` and `content_type` are rejected (`400`) here. - `id` (string, required): Your agent's full 36-character ID (a UUID), from [`GET /v1/agents`](https://dev.virlo.ai/docs/agents#list-agents). - `platform, hook_type, min_views, sort` (string | integer, optional): As on [trending](https://dev.virlo.ai/docs/hooks#trending-hooks). - `page, limit` (integer, optional): Defaults 1 and 20, `limit` up to 100. **cURL request:** ```bash curl "https://api.virlo.ai/v1/agents/{agent_id}/hooks?limit=30" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response:** ```json { "data": { "agent_id": "c1e483c3-...", "coverage": { "videos_collected": 311, "videos_with_hooks": 65 }, "hooks": [ { "hook_text": "Have you ever wondered what all these espresso tools actually do?", "hook_type": "question", "visual_hook_type": "text_hook", "platform": "youtube", "weighted_score": 41.55, "usage_count": 1, "video": { "views": 24275222, "author_handle": "@Ethanrodecoffee", "author_followers": 1260000 } } ] }, "pagination": { "page": 1, "limit": 30, "total": 65, "total_pages": 3, "has_next_page": true, "has_prev_page": false } } ``` --- ## Paging, costs and errors - **Top 1,000.** Trending and search stop at the top 1,000 results (page 50 at `limit=20`). - **Trending and search `total` isn't a full count.** It counts results through this page, plus one if more exist (page 1 at `limit=10` shows `11`). Keep paging while `has_next_page` is `true`. - **Charges.** Empty results are charged, except agent hooks with `videos_with_hooks: 0`. Errors never are. `X-Cost` shows the charge in dollars and `X-Credits-Used` in credits (1 credit = $0.01). Errors come back with `message`, `error`, `statusCode` and `code` ([more on errors](https://dev.virlo.ai/docs/errors)): - `400`: a bad value, an unknown parameter (library categories ignores them) or a page past the top 1,000. The message lists valid values. - `402`: your balance is too low. - `503`: the query ran past about 8 seconds. Retrying the same call usually fails again. For trending slideshows, use [search](https://dev.virlo.ai/docs/hooks#search-hooks) instead. --- # Hashtags > The most-used and most-viewed hashtags in short-form video for any period of up to 90 days, plus video count, views, likes and comments for a single hashtag. Source: https://dev.virlo.ai/docs/hashtags See which hashtags short-form videos used most between two dates, or one hashtag's numbers. Answers take seconds. **At a glance** - **What it does:** Ranks hashtags across the TikTok, YouTube Shorts and Instagram Reels videos Virlo collects, or sums up one hashtag. - **You send:** For the top list: a start and end date, at most 90 days apart. For one hashtag: the hashtag, with optional dates. - **You get back:** Up to 100 hashtags with video counts and total views. Or one hashtag's total and average views, likes and comments. - **Cost:** $0.05 per successful request, even an empty list. Errors are free. > **Note:** **What the numbers mean.** `count` is how many videos in Virlo's data used the hashtag and were posted in your dates (UTC, both days included). It is not the platform's own total. `total_views` is those videos' combined views. Stats refresh every 6 hours. Views freeze about a week after posting. Videos Virlo finds after that week are never counted. --- ## Top hashtags for a date range **Endpoint:** `GET https://api.virlo.ai/v1/hashtags` The list isn't limited to one niche, so broad tags like `fyp` lead. For hashtags inside one niche, use a [Content Research Agent](https://dev.virlo.ai/docs/agents#get-agent-hashtags). Cost per request: $0.05 - `start_date` (string, required): Earliest post date, as `YYYY-MM-DD`. Stats start June 1, 2024; ranges before that come back empty. - `end_date` (string, required): Latest post date, included. At most 90 days after `start_date`: April 1 to June 30 works. - `limit` (number, optional): How many hashtags: 1 to 100, default 50. Above 100 returns 100. No page 2: `page` repeats the list and charges again. - `order_by` (string, optional): `count` (default) or `views`. - `sort` (string, optional): `desc` (default) or `asc`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/hashtags \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-04-01 \ -d end_date=2026-06-30 \ -d order_by=views ``` **Response 200:** ```json { "data": [ { "hashtag": "fyp", "count": 134222, "total_views": 33504743144 }, { "hashtag": "shorts", "count": 130485, "total_views": 14703578435 }, { "hashtag": "viral", "count": 73151, "total_views": 14453567398 } ] } ``` **Response 400:** ```json { "statusCode": 400, "code": "invalid_date_range", "error": "Bad Request", "message": "Date range cannot exceed 90 days (91 days provided)" } ``` --- ## One hashtag's performance **Endpoint:** `GET https://api.virlo.ai/v1/hashtags/:hashtag/performance` Totals and averages for one hashtag, all platforms combined. It works for hashtags outside the top 100. Cost per request: $0.05 - `hashtag` (string, required): In the URL, with no `#`. Case doesn't matter. A typed `#` breaks the URL and returns `start_date must be a string`; write `%23` if needed. - `start_date` (string, optional): Optional, as `YYYY-MM-DD`. Without `end_date`, you get that day to today. - `end_date` (string, optional): Optional. Without `start_date`: everything up to that day. Leave both out for all-time totals. With both: at most 90 days apart. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/hashtags/fyp/performance \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-04-01 \ -d end_date=2026-06-30 ``` **Response 200:** ```json { "data": { "hashtag": "fyp", "video_count": 134222, "total_views": 33504743144, "avg_views": 249621.84, "total_likes": 3308657742, "avg_likes": 24650.64, "total_comments": 26521561, "avg_comments": 197.59 } } ``` **Response 404:** ```json { "statusCode": 404, "code": "not_found", "error": "Not Found", "message": "No videos found with hashtag: notarealtag123" } ``` `video_count` is the top list's `count` for the same dates. --- ## Results for one platform There is no `platform` parameter. For one platform's top list, call `/v1/youtube/hashtags`, `/v1/tiktok/hashtags` or `/v1/instagram/hashtags`. Same parameters, same price. For one hashtag on one platform: - **That platform's top list**, if the hashtag is in its top 100. You get only count and views. - **A [Hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags)**: fresh videos, top creators and momentum. $0.50, or up to $2.50 with every option. You get a job ID; results take 15 seconds to a few minutes. - **A [Content Research Agent](https://dev.virlo.ai/docs/agents#get-agent-hashtags)** set to that platform. It collects videos (from $0.50, usually under 20 minutes), then reading its hashtags is free. The last two count different videos, so numbers won't match this page. --- ## Errors - `400 Bad Request`: A date isn't `YYYY-MM-DD`, the start is after the end, or the dates are over 90 days apart. On the top list, also a missing date (the message names it), a bad `order_by` or `sort`, or `limit` below 1. Or an unsupported parameter like `platform` or `offset`. - `401 Unauthorized`: The API key is missing or invalid. - `402 Payment Required`: Your balance is too low. [Add funds](https://dev.virlo.ai/dashboard/billing). - `404 Not Found`: One hashtag only: none of the counted videos used it in your dates. - `429 Too Many Requests`: Over 50 requests a minute, 500 an hour or 5,000 a day on one endpoint. Wait `retry_after` seconds. See [Rate limits](https://dev.virlo.ai/docs/rate-limits). - `500 Internal Server Error`: An impossible date, such as `2026-02-30`, returns a `500` for now. Fix the date and retry. See also [Errors](https://dev.virlo.ai/docs/errors). --- # Videos > Get the most-viewed short videos posted in the last 48 hours across TikTok, YouTube Shorts, and Instagram Reels. Source: https://dev.virlo.ai/docs/videos See which short videos posted in the last two days have the most views so far, across TikTok, YouTube Shorts, and Instagram Reels. **At a glance** - **What it does:** Lists short videos posted in the last 48 hours, ranked by total views so far. It covers the videos Virlo collects, not every video posted. - **You send:** Nothing required. Optionally, how many videos (1 to 100). - **You get back:** In about a second: each video's link, caption, transcript, views, likes, comments, hashtags, sound, and thumbnail. No creator name or follower count. - **Cost:** $0.25 each time you call, for 1 video or 100. Failed calls are free. ## Get top videos **Endpoint:** `GET https://api.virlo.ai/v1/videos/digest` One list covers all three platforms, sorted by views alone, so TikTok often takes the most spots. Older videos have had more time to build views, so a video posted an hour ago rarely makes it. To filter by topic, use a [Content Research Agent](https://dev.virlo.ai/docs/agents). Cost per request: $0.25 - `limit` (integer, optional): How many videos to return, from 1 to 100 (default 50). Numbers above 100 count as 100. The only setting: adding `platform` or anything else gets a `400` error. For one platform, see [below](https://dev.virlo.ai/docs/videos#platform-paths). The example shortens the transcripts and the YouTube hashtag list. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/videos/digest \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200 OK:** ```json { "data": [ { "id": "367da5d1-5510-4e73-b9a5-fd6207e9915e", "url": "https://www.tiktok.com/@thesun/video/7688710703854767392", "type": "tiktok", "description": "This is the bizarre moment Rory Stewart suddenly turns and stares at fellow Newsnight guest, as viewers say ‘I can’t stop laughing’. Click on the link for more. #NewsNight #RoryStewart #tv ", "publish_date": "2026-09-23T12:44:27", "views": 5050311, "number_of_likes": 381807, "number_of_comments": 4803, "number_of_shares": 105189, "bookmarks": 23851, "hashtags": ["newsnight"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/5f78a37402cb781959c245240f08b7ea1672b0867346cf35771adb32f177e612.jpg", "duration": 15, "external_id": "7688710703854767392", "author_id": "d9c40165-62e0-493c-8b34-7d60f7f13559", "niche": "unknown", "region": "GB", "upload_region": "GB", "upload_region_source": "tiktok_region", "transcript_raw": "All the value goes to them. It's terrifying. What's your sense of what's going on in government?", "is_eligible_for_commission": false, "is_duet": false, "is_stitch": false, "sound": { "id": "05b90659-49c5-4f94-ba15-da80d6ef8c11", "title": "original sound - thesun", "duration": 14, "platform": "tiktok", "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/7a926491c16af666224735afecb62c829a4e11f774fd221b2514238a4ef6a231.webp", "is_original": true, "usage_count": 51, "owner_handle": "thesun", "owner_nickname": "The Sun", "is_commerce_music": true } }, { "id": "72a237ab-30d8-4e8a-8579-b2e746a43d04", "url": "https://www.youtube.com/shorts/z3Ao0FH-0fU", "type": "youtube", "description": "I could NOT see that coming #relatablestories #comedy #funnymemes", "publish_date": "2026-09-23T08:25:27", "views": 1841170, "number_of_likes": 42179, "number_of_comments": 634, "number_of_shares": null, "bookmarks": 0, "hashtags": ["#storytelling", "#shorts", "#relatablestories", "#comedy", "#funnymemes"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/99306a7d461bd83fab98a3154f19faab1145f0b385e69210af6f33567af38ee7.jpg", "duration": 13, "external_id": "z3Ao0FH-0fU", "author_id": "d28e53d4-9ff1-482f-9dfa-0544ea34e7ad", "niche": "unknown", "region": null, "upload_region": "US", "upload_region_source": "youtube_channel_country", "transcript_raw": "There's a clip in the Cars movie where Sally splashes mud on Lightning McQueen.", "is_eligible_for_commission": null, "is_duet": null, "is_stitch": null, "sound": { "id": "36d69972-708a-4e5e-893f-c1143757abf2", "title": "Original Sound", "duration": null, "platform": "youtube", "cover_url": "https://auth.virlo.ai/storage/v1/object/public/sound-covers/c01121a411aca7f631d30acf872b53ebf262e9ff7524eb6bac1e535693fa68db.jpg", "is_original": true, "usage_count": null, "owner_handle": "tyler.vitelli", "owner_nickname": "@tyler.vitelli", "is_commerce_music": null } } ] } ``` **Response 400:** ```json { "message": ["limit must not be less than 1"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 429:** ```json { "statusCode": 429, "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "error": "Too Many Requests", "limit": 50, "remaining": 0, "reset_at": 1790270940, "reset_at_formatted": "September 24th, 2026, 5:29 PM UTC", "retry_after": 54 } ``` **Errors and limits.** Errors are free. `402`: your balance is under $0.25. `429`: too many calls (50 a minute, 500 an hour, 5,000 a day per list, bad requests included); wait `retry_after` seconds. See [Rate limits](https://dev.virlo.ai/docs/rate-limits). **Full field reference** Every video has these 24 fields. `null` (an empty value) means not reported, and so does `0` where noted. - `id, external_id` (string, optional): Virlo's ID and the platform's ID. On Instagram, `external_id` is the numeric media ID, not the code in the Reel link. - `url, type` (string, optional): Link to the video, and its platform: `tiktok`, `youtube`, or `instagram`. - `description` (string | null, optional): The caption. On Instagram and some TikToks, the text often appears twice. - `publish_date` (string, optional): Posting time in UTC, like `2026-09-23T12:44:27`. It has no UTC marker (`Z`), so add one or your code may read it as local time. - `views` (integer, optional): Total views at Virlo's last check. The list is sorted by it. - `number_of_likes, number_of_comments` (integer, optional): On YouTube, `0` often means not reported. - `number_of_shares, bookmarks` (integer | null, optional): Real counts on TikTok only (shares are sometimes `null`). On YouTube and Instagram, `null` or `0` means not reported. - `hashtags` (string[] | null, optional): YouTube tags usually start with `#` and others don't, so remove it before comparing. TikTok often lists one tag; the caption (`description`) has them all. No tags: `[]` or `null`. - `thumbnail_url` (string | null, optional): Link to the thumbnail image. - `duration` (integer | null, optional): Length in seconds. Usually `null` on Instagram. - `transcript_raw` (string | null, optional): The spoken words as text. - `sound` (object | null, optional): The audio. Usually `null` on Instagram. When present, it always has 10 fields. On YouTube, `duration` and `usage_count` are `null`. `cover_url` is a full link, or `null`. For full stats, pass `sound.id` to [Sound details](https://dev.virlo.ai/docs/sounds#sound-details) (from $0.05). - `upload_region, upload_region_source` (string | null, optional): The creator's country code, like `US` (usually `null` on Instagram). A source starting with `inferred` is Virlo's estimate. Others come from the platform. - `is_duet, is_stitch, is_eligible_for_commission` (boolean | null, optional): TikTok only. `true` marks a duet, a stitch, or a video that can earn a commission, such as through TikTok Shop. Elsewhere, `null` or `false` both mean no. - `author_id` (string | null, optional): Virlo's creator ID. For the handle, open `url`. - `region, niche` (string | null, optional): Old fields. `region` is often `null` (always on YouTube), so use `upload_region`. `niche` is always `"unknown"`. ## Top videos for one platform Each platform has its own top list from the same 48 hours, with the same `limit`, fields, and price. It is that platform's own top 100, not a slice of the mixed list, so Reels aren't pushed out by TikTok. - [YouTube Shorts](https://dev.virlo.ai/docs/youtube-videos): `GET /v1/youtube/videos/digest` - [TikTok](https://dev.virlo.ai/docs/tiktok-videos): `GET /v1/tiktok/videos/digest` - [Instagram Reels](https://dev.virlo.ai/docs/instagram-videos): `GET /v1/instagram/videos/digest` Only these three exist. Others, such as Facebook, return `404` (not found). --- # YouTube hashtags > The most-used and most-viewed hashtags on YouTube Shorts for any period of up to 90 days. Works like the Hashtags top list, $0.05 per successful request. Source: https://dev.virlo.ai/docs/youtube-hashtags See which hashtags YouTube Shorts used most between two dates. **At a glance** - **What it does:** Ranks hashtags by use or views. - **You send:** Two dates, at most 90 days apart. - **You get back:** Up to 100 hashtags with Shorts counts and first-week views. Counts cover only Shorts Virlo found in their first week, not YouTube's total, so don't compare platforms. - **Cost:** $0.05, even if empty. Errors are free. ## Top YouTube hashtags **Endpoint:** `GET https://api.virlo.ai/v1/youtube/hashtags` Same as the [Hashtags top list](https://dev.virlo.ai/docs/hashtags#get-hashtags), for YouTube Shorts only. A `platform` option gets a `400` error. Cost per request: $0.05 - `start_date` (string, required): First posting day (UTC), `YYYY-MM-DD`. - `end_date` (string, required): Last posting day, included. Max 90 days later. - `limit` (number, optional): 1 to 100, default 50. - `order_by` (string, optional): `count` (default) or `views`. - `sort` (string, optional): `desc` (default) or `asc`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/youtube/hashtags \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-08-25 \ -d end_date=2026-09-23 \ -d limit=10 \ -d order_by=views ``` **Response 200:** ```json { "data": [ { "hashtag": "shorts", "count": 261871, "total_views": 29994351920 }, { "hashtag": "viral", "count": 87501, "total_views": 11205449485 } ] } ``` **Response 400 No dates:** ```json { "statusCode": 400, "code": "invalid_date_range", "error": "Bad Request", "message": ["start_date must be a string", "end_date must be a string"] } ``` > **Note:** **One hashtag, YouTube only?** [One hashtag's performance](https://dev.virlo.ai/docs/hashtags#get-hashtag-performance) always mixes all platforms. [Other options](https://dev.virlo.ai/docs/hashtags#platform-paths). --- # YouTube videos > Get the most-viewed YouTube Shorts posted in the last 48 hours in one request. Source: https://dev.virlo.ai/docs/youtube-videos See which YouTube Shorts posted in the last two days have the most views so far. **At a glance** - **What it does:** Ranks Shorts Virlo has collected, not all of YouTube. Takes about a second. - **You send:** Optionally, how many Shorts you want. - **You get back:** Link, caption, views, likes, comments, hashtags, usually a transcript. No channel name; open the link. - **Cost:** $0.25 per request, even for 100 Shorts. Errors are free. ## Get top YouTube Shorts **Endpoint:** `GET https://api.virlo.ai/v1/youtube/videos/digest` Cost per request: $0.25 - `limit` (integer, optional): How many Shorts: 1 to 100 (default 50; above 100 gives 100). Other settings, even `platform`, fail. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/youtube/videos/digest \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200 OK:** ```json { "data": [ { "id": "72a237ab-30d8-4e8a-8579-b2e746a43d04", "url": "https://www.youtube.com/shorts/z3Ao0FH-0fU", "type": "youtube", "description": "I could NOT see that coming #relatablestories #comedy #funnymemes", "publish_date": "2026-09-23T08:25:27", "views": 1841170, "number_of_likes": 42179, "number_of_comments": 634, "number_of_shares": null, "bookmarks": 0, "hashtags": ["#storytelling", "#shorts", "#relatablestories", "#comedy", "#funnymemes"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/99306a7d461bd83fab98a3154f19faab1145f0b385e69210af6f33567af38ee7.jpg", "duration": 13, "external_id": "z3Ao0FH-0fU", "author_id": "d28e53d4-9ff1-482f-9dfa-0544ea34e7ad", "niche": "unknown", "region": null, "upload_region": "US", "upload_region_source": "youtube_channel_country", "transcript_raw": "There's a clip in the Cars movie where Sally splashes mud on Lightning McQueen.", "is_eligible_for_commission": null, "is_duet": null, "is_stitch": null, "sound": { "id": "36d69972-708a-4e5e-893f-c1143757abf2", "title": "Original Sound", "duration": null, "platform": "youtube", "cover_url": "c01121a411aca7f631d30acf872b53ebf262e9ff7524eb6bac1e535693fa68db.jpg", "is_original": true, "usage_count": null, "owner_handle": "tyler.vitelli", "owner_nickname": "@tyler.vitelli", "is_commerce_music": null } } ] } ``` **Response 400:** ```json { "message": ["property platform should not exist"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` `0` likes or comments can mean "not reported", even on hit Shorts. Shares and saves (`bookmarks`) aren't reported. Hashtags usually keep the `#`, unlike other platforms. [What each field means](https://dev.virlo.ai/docs/videos#get-videos-digest) --- # TikTok hashtags > The most-used and most-viewed hashtags on TikTok for any period of up to 90 days. Works like the Hashtags top list, $0.05 per successful request. Source: https://dev.virlo.ai/docs/tiktok-hashtags See which hashtags TikTok videos used most between two dates. **At a glance** - **What it does:** Ranks hashtags by use or views. - **You send:** Two dates, at most 90 days apart. - **You get back:** Up to 100 hashtags with video counts and first-week views. Counts cover only videos Virlo found in their first week, not TikTok's total, so don't compare platforms. - **Cost:** $0.05, even if empty. Errors are free. ## Top TikTok hashtags **Endpoint:** `GET https://api.virlo.ai/v1/tiktok/hashtags` Same as the [Hashtags top list](https://dev.virlo.ai/docs/hashtags#get-hashtags), for TikTok only. A `platform` option gets a `400` error. Cost per request: $0.05 - `start_date` (string, required): First posting day (UTC), `YYYY-MM-DD`. - `end_date` (string, required): Last posting day, included. Max 90 days later. - `limit` (number, optional): 1 to 100, default 50. - `order_by` (string, optional): `count` (default) or `views`. - `sort` (string, optional): `desc` (default) or `asc`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tiktok/hashtags \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-08-25 \ -d end_date=2026-09-23 \ -d limit=10 \ -d order_by=views ``` **Response 200:** ```json { "data": [ { "hashtag": "fyp", "count": 31049, "total_views": 5162705739 }, { "hashtag": "viral", "count": 9971, "total_views": 1693054907 } ] } ``` **Response 400 No dates:** ```json { "statusCode": 400, "code": "invalid_date_range", "error": "Bad Request", "message": ["start_date must be a string", "end_date must be a string"] } ``` > **Note:** **One hashtag, TikTok only?** [One hashtag's performance](https://dev.virlo.ai/docs/hashtags#get-hashtag-performance) always mixes all platforms. [Other options](https://dev.virlo.ai/docs/hashtags#platform-paths). [Python walkthrough](https://dev.virlo.ai/guides/tiktok-hashtag-analytics-api) --- # TikTok videos > Get the most-viewed TikTok videos posted in the last 48 hours in one request. Source: https://dev.virlo.ai/docs/tiktok-videos See which TikToks posted in the last two days have the most views so far. **At a glance** - **What it does:** Ranks TikToks Virlo has collected, not all of TikTok. Takes about a second. - **You send:** Optionally, how many TikToks you want. - **You get back:** Link (shows the handle), caption, views, likes, comments, shares, saves, hashtags, sound. No follower counts. - **Cost:** $0.25 per request, even for 100 TikToks. Errors are free. ## Get top TikTok videos **Endpoint:** `GET https://api.virlo.ai/v1/tiktok/videos/digest` Cost per request: $0.25 - `limit` (integer, optional): How many TikToks: 1 to 100 (default 50; above 100 gives 100). Other settings, even `platform`, fail. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/tiktok/videos/digest \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200 OK:** ```json { "data": [ { "id": "367da5d1-5510-4e73-b9a5-fd6207e9915e", "url": "https://www.tiktok.com/@thesun/video/7688710703854767392", "type": "tiktok", "description": "This is the bizarre moment Rory Stewart suddenly turns and stares at fellow Newsnight guest, as viewers say ‘I can’t stop laughing’. Click on the link for more. #NewsNight #RoryStewart #tv ", "publish_date": "2026-09-23T12:44:27", "views": 5050311, "number_of_likes": 381807, "number_of_comments": 4803, "number_of_shares": 105189, "bookmarks": 23851, "hashtags": ["newsnight"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/5f78a37402cb781959c245240f08b7ea1672b0867346cf35771adb32f177e612.jpg", "duration": 15, "external_id": "7688710703854767392", "author_id": "d9c40165-62e0-493c-8b34-7d60f7f13559", "niche": "unknown", "region": "GB", "upload_region": "GB", "upload_region_source": "tiktok_region", "transcript_raw": "All the value goes to them. It's terrifying. What's your sense of what's going on in government?", "is_eligible_for_commission": false, "is_duet": false, "is_stitch": false, "sound": { "id": "05b90659-49c5-4f94-ba15-da80d6ef8c11", "title": "original sound - thesun", "duration": 14, "platform": "tiktok", "cover_url": "7a926491c16af666224735afecb62c829a4e11f774fd221b2514238a4ef6a231.webp", "is_original": true, "usage_count": 51, "owner_handle": "thesun", "owner_nickname": "The Sun", "is_commerce_music": true } } ] } ``` **Response 400:** ```json { "message": ["limit must not be less than 1"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` `hashtags` often lists only one tag. The caption (`description`) has them all. Sound stats: look up `sound.id` in [Sound details](https://dev.virlo.ai/docs/sounds#sound-details) ($0.05 to $0.15). [What each field means](https://dev.virlo.ai/docs/videos#get-videos-digest) --- # Instagram hashtags > The most-used and most-viewed hashtags on Instagram Reels for any period of up to 90 days. Works like the Hashtags top list, $0.05 per successful request. Source: https://dev.virlo.ai/docs/instagram-hashtags See which hashtags Instagram Reels used most between two dates. **At a glance** - **What it does:** Ranks hashtags by use or views. - **You send:** Two dates, at most 90 days apart. - **You get back:** Up to 100 hashtags with Reels counts and first-week views. Counts cover only Reels Virlo found in their first week, not Instagram's total, so don't compare platforms. - **Cost:** $0.05, even if empty. Errors are free. ## Top Instagram hashtags **Endpoint:** `GET https://api.virlo.ai/v1/instagram/hashtags` Same as the [Hashtags top list](https://dev.virlo.ai/docs/hashtags#get-hashtags), for Instagram Reels only. A `platform` option gets a `400` error. Cost per request: $0.05 - `start_date` (string, required): First posting day (UTC), `YYYY-MM-DD`. - `end_date` (string, required): Last posting day, included. Max 90 days later. - `limit` (number, optional): 1 to 100, default 50. - `order_by` (string, optional): `count` (default) or `views`. - `sort` (string, optional): `desc` (default) or `asc`. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/instagram/hashtags \ -H "Authorization: Bearer YOUR_API_KEY" \ -d start_date=2026-08-25 \ -d end_date=2026-09-23 \ -d limit=10 \ -d order_by=views ``` **Response 200:** ```json { "data": [ { "hashtag": "viral", "count": 16212, "total_views": 1992187266 }, { "hashtag": "fyp", "count": 15774, "total_views": 1504892057 } ] } ``` **Response 400 No dates:** ```json { "statusCode": 400, "code": "invalid_date_range", "error": "Bad Request", "message": ["start_date must be a string", "end_date must be a string"] } ``` > **Note:** **One hashtag, Instagram only?** [One hashtag's performance](https://dev.virlo.ai/docs/hashtags#get-hashtag-performance) always mixes all platforms. [Other options](https://dev.virlo.ai/docs/hashtags#platform-paths). --- # Instagram videos > Get the most-viewed Instagram Reels posted in the last 48 hours in one request. Source: https://dev.virlo.ai/docs/instagram-videos See which Instagram Reels posted in the last two days have the most views so far. In the [all-platform list](https://dev.virlo.ai/docs/videos), TikToks and Shorts take most spots. **At a glance** - **What it does:** Ranks Reels Virlo has collected, not all of Instagram. Takes about a second. - **You send:** Optionally, how many Reels you want. - **You get back:** Link, caption, views, likes, comments, hashtags. No shares, saves, country, or transcript; open the link to see the creator. - **Cost:** $0.25 per request, even for 100 Reels. Errors are free. ## Get top Instagram Reels **Endpoint:** `GET https://api.virlo.ai/v1/instagram/videos/digest` Cost per request: $0.25 - `limit` (integer, optional): How many Reels: 1 to 100 (default 50; above 100 gives 100). Other settings, even `platform`, fail. **cURL request:** ```bash curl -G https://api.virlo.ai/v1/instagram/videos/digest \ -H "Authorization: Bearer YOUR_API_KEY" \ -d limit=50 ``` **Response 200 OK:** ```json { "data": [ { "id": "94616037-3d94-4a4a-bf68-10366ad1c65f", "url": "https://www.instagram.com/reel/DdnXu6WRcxR/", "type": "instagram", "description": "Panel solar cleaning #solarfarm\nPanel solar cleaning #solarfarm", "publish_date": "2026-09-23T03:43:40", "views": 455078, "number_of_likes": 1454, "number_of_comments": 6, "number_of_shares": null, "bookmarks": null, "hashtags": ["solarfarm"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/cf36718c0c4afcad47b8d098a4bfb67b478589d3f7391e6e72de1293eb50634c.jpg", "duration": null, "external_id": "3992263973503618129", "author_id": "a63dfcf1-2bee-47a1-81ba-cff9e0deb171", "niche": "unknown", "region": null, "upload_region": null, "upload_region_source": null, "transcript_raw": null, "is_eligible_for_commission": null, "is_duet": null, "is_stitch": null, "sound": null } ] } ``` **Response 400:** ```json { "message": ["limit must not be less than 1"], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` Many captions repeat after a line break; ignore the repeat. [What each field means](https://dev.virlo.ai/docs/videos#get-videos-digest) --- # How results load > Some Virlo jobs keep working in the background after you start them. How to tell when the results are fully ready, and how to wait for them. Source: https://dev.virlo.ai/docs/async-data Agents, lookups, tracking, and audience snapshots keep working after you start them. Here is how to tell when results are ready. **At a glance** - **What it does:** Tells you when a background job's results are ready. - **You send:** A free status check (polling) every 15 seconds, or whatever `retry_after_seconds` says. Or a webhook message telling you when to check. - **You get back:** `finalized: true` once everything is ready, even if `status` says `completed`. A few jobs only have `status` (see the table). - **Cost:** Checking is free. You pay when a job starts, and for each recurring agent run or tracking check. - **How long:** About 30 seconds for a creator lookup. Usually under 20 minutes for a Content Research Agent to collect, plus a few for its AI report. ## Where to check on each job Replace each `:placeholder` with its ID. | Job | Check (free `GET`) | Done when | | - | - | - | | [Content Research Agent](https://dev.virlo.ai/docs/agents) | `/v1/agents/:id` | `finalized: true` | | [Creator lookup](https://dev.virlo.ai/docs/satellite) | `/v1/satellite/creator/status/:job_id` | `finalized: true` | | Creator lookup's Data Intelligence (about 3 min after `finalized`) | `/v1/satellite/runs/:run_id` (your `job_id`) | `intelligence_status` is `ready`, `failed`, or `skipped` | | [Batch creator lookup](https://dev.virlo.ai/docs/satellite#poll-batch-status) | `/v1/satellite/creators/batch/:batch_id` | `status` is `completed` or `failed` | | [Video outlier](https://dev.virlo.ai/docs/satellite/video-outlier), [sound](https://dev.virlo.ai/docs/satellite/sounds), or [hashtag](https://dev.virlo.ai/docs/satellite/hashtags) lookup | `/v1/satellite//status/:job_id` (`video-outlier`, `sounds`, `hashtags`) | `status` is `completed` or `failed` | | [Tracked creator](https://dev.virlo.ai/docs/tracking) | `/v1/tracking/creators/:id` | `finalized: true` | | [Post collection](https://dev.virlo.ai/docs/tracking#get-collection-status) | `/v1/tracking/creators/:id/posts/collect/:collection_id` | `status` is `completed` or `failed` | | [Tracked video](https://dev.virlo.ai/docs/tracking) | `/v1/tracking/videos/:id` | `enrichment_status` is `ready` or `failed` | | [Audience snapshot](https://dev.virlo.ai/docs/tracking#audience-basics) | `/v1/audience/snapshot/:job_id` | `finalized: true` | After 24 hours, a lookup's status check returns `404`. Use `/v1/satellite/runs/:run_id` with the same ID. ## The done signal (`finalized`) `finalized` turns `true` once the main job and every follow-up job, such as the AI report, have stopped. Until then, empty (`null`) fields mean "not ready", not "nothing found". This agent has finished collecting but is still writing its report: **Report still being written:** ```json { "data": { "id": "df04c83b-7104-4579-bb4d-43f64c392c87", "analysis": null, "pending_jobs": [ { "type": "viral_analysis", "status": "processing", "poll_url": "/v1/agents/df04c83b-7104-4579-bb4d-43f64c392c87", "result_path": "data", "webhook_event": "content_research_agent.run.completed", "retry_after_seconds": 15 } ], "finalized": false, "stage": "analyzing", "progress_pct": 75, "eta_seconds": 893, "latest_run": { "status": "completed" } } } ``` **Finished is not the same as worked.** First check that the job's status is not `failed`: `latest_run.status` on agents, `enrichment_status` on tracked creators, `status` elsewhere. On agents, `partial_failure` means one keyword or platform failed. The rest is usable. **It can turn back to `false`** when a recurring agent's next run or a tracked creator's next check starts. **It doesn't cover** per-video [Data Intelligence](https://dev.virlo.ai/docs/async-data#intelligence-status). An agent's outlier-creator list can also shift for about a minute afterward. ## What is still running (`pending_jobs`) `pending_jobs` lists follow-up work still queued or running. Jobs leave the list when they finish or fail, as does an AI report not started 15 minutes after collection ends. A failed job's result stays `null`. So an agent with `finalized: true` and `analysis: null` has no report coming for this run, but its videos are still good. **An empty list does not mean done.** An agent that is still collecting shows `finalized: false` with `pending_jobs: []`. So does an audience snapshot job while its `status` is `processing`. **Audience snapshots have two traps.** A failed snapshot never lets a creator lookup's `finalized` turn `true`. During a refresh, a tracked creator's audience results show the old snapshot with `finalized: true`. So check the audience job, `GET /v1/audience/snapshot/:job_id` (on a lookup, if an audience field still reads `processing` after 15 minutes). It ends `completed` or `failed`, and failures are refunded. For tracked creators, [the tracking route](https://dev.virlo.ai/docs/tracking#check-audience-refresh-status) works too, with friendlier field names. **Full field reference** - `type` (string, required): `viral_analysis` (an agent's AI report), `tracking_report` (a tracked creator's AI report), or `audience_demographics` and `audience_geography` (one audience snapshot job). - `status` (string, required): `pending` or `processing`. - `job_id` (string, optional): Audience snapshot jobs only. - `poll_url` (string, optional): Free path to check. - `result_path` (string, optional): Where the result appears (`data` is the whole object). - `webhook_event` (string, optional): Event sent when it ends. - `started_at` (string, optional): Not sent today. - `retry_after_seconds` (integer, optional): `15` today. ## Per-video breakdowns (`intelligence_status`) With [Data Intelligence](https://dev.virlo.ai/docs/intelligence) (a paid breakdown of the videos worth studying), every video and slideshow an agent returns has an `intelligence_status`: - `ready`: the `intelligence` object is filled in. Agents with Data Intelligence off also get this, free, on videos Virlo already analyzed. - `pending`: not analyzed yet, and it may never be. After `finalized`, re-fetch a few times, then use the `ready` videos. - `skipped`: Virlo chose not to analyze it, so stop checking. `intelligence_skip_reason` says why: `intent_mismatch`, `too_old`, or `under_followers` ([details](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing)). - `disabled`: off for this agent. Turning it on only affects future runs: [update](https://dev.virlo.ai/docs/agents#update-agent) a recurring agent, or create a new one. **Creator lookups** ([`GET /v1/satellite/runs/:run_id`](https://dev.virlo.ai/docs/satellite#durable-runs)) add `failed` (the run's analysis failed), and their `skipped` (this video got no analysis) carries no reason. The lookup's overall `intelligence_status` can be `null` for a few seconds after `finalized`. Check `request.data_intelligence` to see if you asked for it, and keep checking while the status is `null` or `pending`. Slideshows leave out 16 video-only fields ([details](https://dev.virlo.ai/docs/intelligence#slideshow-intelligence)). ## Waiting by polling These functions wait for `finalized: true`. For other jobs, use the done condition in the [table](https://dev.virlo.ai/docs/async-data#per-resource). **Python:** ```python import time import requests def wait_until_finalized(url, headers, timeout_s=3600, default_wait_s=15): started = time.time() while time.time() - started < timeout_s: res = requests.get(url, headers=headers) res.raise_for_status() data = res.json()["data"] if data.get("finalized") is True: return data # Wait as long as the pending jobs suggest (15 seconds today). pending = data.get("pending_jobs") or [] wait_s = max( (job.get("retry_after_seconds") or default_wait_s for job in pending), default=default_wait_s, ) time.sleep(wait_s) raise TimeoutError(f"{url} not finalized after {timeout_s} seconds") agent = wait_until_finalized( "https://api.virlo.ai/v1/agents/YOUR_AGENT_ID", {"Authorization": "Bearer YOUR_API_KEY"}, ) # Finished is not the same as worked: check for a failed run first. if (agent.get("latest_run") or {}).get("status") == "failed": raise RuntimeError("The agent run failed, so there are no results") print(agent["analysis"]) ``` **TypeScript:** ```ts // Uses top-level await: run it as an ES module (for example, a .mts file). async function waitUntilFinalized( url: string, headers: Record, { timeoutSeconds = 3600, defaultWaitSeconds = 15 } = {}, ) { const started = Date.now() while (Date.now() - started < timeoutSeconds * 1000) { const res = await fetch(url, { headers }) if (!res.ok) throw new Error(`${url} returned HTTP ${res.status}`) const { data } = await res.json() if (data.finalized === true) return data // Wait as long as the pending jobs suggest (15 seconds today). const pending: Array<{ retry_after_seconds?: number }> = data.pending_jobs ?? [] const waitSeconds = pending.length ? Math.max(...pending.map((job) => job.retry_after_seconds ?? defaultWaitSeconds)) : defaultWaitSeconds await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)) } throw new Error(`${url} not finalized after ${timeoutSeconds} seconds`) } const agent = await waitUntilFinalized( 'https://api.virlo.ai/v1/agents/YOUR_AGENT_ID', { Authorization: 'Bearer YOUR_API_KEY' }, ) // Finished is not the same as worked: check for a failed run first. if (agent.latest_run?.status === 'failed') { throw new Error('The agent run failed, so there are no results') } console.log(agent.analysis) ``` They stop after an hour. Raise that for large agents, but keep a limit. ### Show progress while you wait Agents also return `stage`, `progress_pct`, and `eta_seconds` on `GET /v1/agents/:id` and `/summary`, for a progress bar: | `stage` | `progress_pct` | `eta_seconds` | | - | - | - | | `collecting` | `40` | `null` | | `analyzing` (AI report) | `75` | Counts down from about 900, a worst case | | `finalizing` | `90` | `null` | | `completed` | `100` | `0` | Recurring agents often jump from `collecting` straight to `completed`. ## Waiting with a webhook A webhook is a message Virlo sends to your server when a job ends. Register one for all four events: **One webhook for all jobs:** ```bash curl -X POST https://api.virlo.ai/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/virlo", "description": "All finished jobs", "enabled_events": [ "content_research_agent.run.completed", "satellite.lookup.completed", "tracking.cycle.completed", "audience.snapshot.completed" ] }' ``` Use your own public `https://` address; the example gets a `400`. **Messages are not copies of the `GET` response** and have no `finalized` or `pending_jobs`. Some carry the full result, others only IDs. When one arrives, call the job's `GET` once. See [event contents](https://dev.virlo.ai/docs/webhooks#data-shape) and [retries](https://dev.virlo.ai/docs/webhooks#retry-ladder). **Poll too.** Messages can be late or lost, and some jobs send none: post collections, a lookup's Data Intelligence, failed audience snapshots, and free repeat lookups within 6 hours. A tracked item that can't be checked sends `tracking.paused` instead. Also check each job's `GET` every 10 minutes or so. --- # Pagination > How to page through long lists in the Virlo API: which parameters each endpoint takes and how to tell when you have reached the last page. Source: https://dev.virlo.ai/docs/pagination Long lists come in pages. Keep asking for the next page until you hit that list's stop rule. **At a glance** - **What it does:** Splits long lists, like an agent's videos, into pages. - **You send:** `limit` (items per page) and `page` (starting at 1). A few lists take `offset` or `cursor` instead. - **You get back:** One page of items, plus fields that say whether there is another page. - **Cost:** Each page is its own request. Paid lists charge per page: trending sounds is $0.25 a page. - **When to stop:** See [the table](https://dev.virlo.ai/docs/pagination#parameters). A short page does not always mean the end. ## Which parameters each list takes | List | Send | `limit` default / max | Stop when | | - | - | - | - | | An agent's videos, slideshows, ads, and creator outliers | `page` | 50 / 100 | [Empty page](https://dev.virlo.ai/docs/pagination#shape-a) | | An agent's hashtags, trend history, and analysis history | `page` | 50 / 100 | [`has_next_page` is `false`](https://dev.virlo.ai/docs/pagination#shape-b) | | Sound and hook lists, an agent's sounds and hooks, tracked creators and videos | `page` | 20 / 100 | [`has_next_page` is `false`](https://dev.virlo.ai/docs/pagination#shape-b) | | A tracked creator's posts | `page` | 50 / 200 (`400` if over) | [`has_next_page` is `false`](https://dev.virlo.ai/docs/pagination#shape-b) | | Your agents (`GET /v1/agents`) and an agent's runs | `page` | 50 / 100 | [Page shorter than the returned `limit`](https://dev.virlo.ai/docs/pagination#shape-c) | | Your past lookups (`GET /v1/satellite/runs`) | `offset` | 25 / 100 | [You have `total` items](https://dev.virlo.ai/docs/pagination#shape-a) | | Videos from one lookup (`.../runs/:run_id/videos`) | `offset` | 25 / 200 (`400` if over) | [You have `total` items](https://dev.virlo.ai/docs/pagination#shape-a) | | A webhook's delivery log | `cursor` | 50 / 100 | [`next_cursor` is `null`](https://dev.virlo.ai/docs/pagination#cursor) | - `limit` (integer, optional): Items per page. Over the maximum, most lists use the maximum; those marked "`400` if over" return a `400` error. Below 1 is an error on most lists. - `offset` (integer, optional): How many items to skip. Page 2 with `limit=25` is `offset=25`. > **Note:** Only the two lookup lists take `offset`, and they reject `page`. Many others show `offset` in responses but take `page`. Most other lists reject `offset` with a `400`. Your agents, an agent's runs, and an agent's ads ignore it and return page 1 every time, so an `offset` loop never ends. ## Lists with a total Items sit in `data` (under `videos`, `runs`, and so on) beside `total`, `limit`, and `offset`. On an agent's lists, stop only at an empty page. A page can come back short with more still to come. `total` is approximate and items can repeat across pages, so skip any `id` you already have. **GET /v1/agents/:id/videos?limit=50&page=2:** ```json { "data": { "agent_id": "2b1f9c3d-...", "total": 283, "limit": 50, "offset": 50, "videos": [ { "id": "e30b40b1-...", "url": "https://...", "views": 11166267 } ] } } ``` ## Lists with a pagination block A `pagination` object sits next to `data`. Keep going while `has_next_page` is `true`. > **Note:** On trending sounds (sorted by `videos_7d` or `videos_30d`), breakout sounds, an agent's sounds and hashtags, and hook trending and search, `total` and `total_pages` can be too low. Below, `total: 21` just means "more than 20". Follow `has_next_page`, and stop if a page comes back empty. Hook trending and search end at result 1,000; a page past that returns `400`. **GET /v1/sounds/trending:** ```json { "data": [ { "id": "4d06645e-...", "title": "PASSO BEM SOLTO (Slowed)", "platform": "youtube" } ], "pagination": { "page": 1, "limit": 20, "total": 21, "total_pages": 2, "has_next_page": true, "has_prev_page": false }, "sort": "videos_7d" } ``` ## Lists with only a count Only `GET /v1/agents` and `GET /v1/agents/:id/runs` work this way. The list sits in `data.agents` or `data.runs`, with `count` (items on this page) and no `total`. Stop at the first page shorter than the `limit` in the response, not the one you sent: a `limit` over 100 is lowered to 100. **GET /v1/agents:** ```json { "data": { "limit": 50, "page": 1, "count": 3, "agents": [ { "id": "2b1f9c3d-...", "name": "Skincare Routine Research", "is_recurring": true } ] } } ``` ## Lists with a cursor A webhook's delivery log has no `data` wrapper. It returns `endpoint_id`, `deliveries`, `limit`, and `next_cursor`. Send `next_cursor` back as `cursor` until it comes back `null`. Replace its `+` with `%2B` (URL-encoding) or you get a `400`. The last page can be empty. [Full details](https://dev.virlo.ai/docs/webhooks#list-deliveries). **GET /v1/webhooks/:id/deliveries?limit=2:** ```json { "endpoint_id": "b64573fc-...", "deliveries": [ { "id": "718bdf28-...", "event_type": "content_research_agent.run.completed", "status": "pending" } ], "limit": 2, "next_cursor": "2026-09-24T17:32:27.503726+00:00" } ``` ## Results that aren't paged These return everything in one response. Some take `limit`, but none has a second page. - An agent's benchmarks, audience affinity, activity, events, proposals, and similar creators. Activity, events, and proposals show a `count` but ignore `page`, so don't loop on them. - Ranked lists of up to 100: `GET /v1/hashtags`, `GET /v1/videos/digest` (both also per platform), and `GET /v1/trends`. On hashtags, `page=2` repeats page 1 and is still charged. - `GET /v1/trends/emerging`: `limit` default 20, max 50 (`400` if over). - Snapshots of a tracked creator or video (default 30) and a sound's usage history (default 90): `limit` max 365 (`400` if over). - `GET /v1/webhooks`: a plain array with no `data` wrapper. --- # Rate limits > How many requests you can make, when limits reset, and what to do when you hit one. Source: https://dev.virlo.ai/docs/rate-limits Rate limits cap how often your key can call each endpoint (one API address, like `/v1/trends`) per minute, hour, or day. Most accounts never reach them. **At a glance** - **What it does:** Caps requests per API key, separately for each endpoint. - **You get back:** Responses show your requests left. Over a limit, a `429` error says how long to wait. - **Cost:** Free. Blocked requests are not charged. - **Normal limit:** 10,000 requests per day for each endpoint. A few have tighter limits. - **When it resets:** At fixed UTC times, not rolling: each new minute, each hour on the hour, and midnight UTC. ## How rate limits work - **Per key and per endpoint.** Each endpoint gets its own 10,000 per day, not a shared total. Different creators or hashtags share that endpoint's limit. - **Reading and creating share a limit.** `GET /v1/webhooks` and `POST /v1/webhooks` count as one. Agent endpoints (and the old ones under Legacy in the sidebar) count them apart: listing agents with `GET /v1/agents` never uses up `POST /v1/agents`. - **What counts.** Most requests count and get limit headers, even bad-input errors (`400`). A bad key (`401`) and most low-balance errors (`402`) don't count and get none. - **No valid key.** If the key is missing or doesn't start with `virlo_tkn_`, your IP address gets about 20 quick tries, then about 1 per second. Faster requests get a `429` with code `too_many_requests`. ## Response headers - `X-RateLimit-Limit` (integer, optional): Requests allowed in the current window (a minute, hour, or day). - `X-RateLimit-Remaining` (integer, optional): Requests left in that window. - `X-RateLimit-Reset` (integer, optional): When that window resets, as a Unix timestamp (`1790294400` is midnight UTC, September 25, 2026). - `Retry-After` (integer, optional): Only on a `429` from your key's limit: seconds to wait before retrying. > **Note:** When an endpoint has minute, hour, and day limits, the headers show only the one closest to running out. `GET /v1/hashtags` can report `50` (per minute) on one call and `500` (per hour) on the next. ## Default rate limits Every standard API key gets these. If your account has custom limits, the headers show yours. | Feature | Endpoints | Per minute | Per hour | Per day | | - | - | - | - | - | | Creator lookup | `GET /v1/satellite/creator/:platform/:username` | 5 | 100 | 1,000 | | Video outlier check | `POST /v1/satellite/video-outlier` | 5 | 100 | 1,000 | | [Hashtag stats](https://dev.virlo.ai/docs/hashtags) | `GET /v1/hashtags`, `GET /v1/hashtags/:hashtag/performance`, `GET /v1/tiktok/hashtags`, `GET /v1/youtube/hashtags`, `GET /v1/instagram/hashtags` | 50 | 500 | 5,000 | | Video digests | `GET /v1/videos/digest`, `GET /v1/tiktok/videos/digest`, `GET /v1/youtube/videos/digest`, `GET /v1/instagram/videos/digest` | 50 | 500 | 5,000 | | Trends | `GET /v1/trends`, `GET /v1/trends/digest` | 50 | 500 | 5,000 | | Emerging trends | `GET /v1/trends/emerging` | 60 | 600 | 6,000 | | [Agent results](https://dev.virlo.ai/docs/agents) | `GET /v1/agents/:id`, and its `/videos`, `/slideshows`, `/ads`, `/creators/outliers`, `/hooks`, `/sounds`, `/hashtags` | 500 | 5,000 | 10,000 | | Other agent reads | `GET /v1/agents` (and `/list`), and an agent's `/summary`, `/runs`, `/activity`, `/events`, `/trends`, `/analysis`, `/benchmarks`, `/affinity`, similar creators | 120 | 2,000 | 10,000 | | Create an agent | `POST /v1/agents` | 30 | 300 | 1,000 | | Keyword suggestions | `POST /v1/agents/suggest-keywords` | 30 | 300 | 1,000 | | Agent changes | `PUT` and `DELETE /v1/agents/:id`, `PUT /v1/agents/:id/autonomy` | 30 | 300 | 2,000 | | Everything else, including `/v1/satellite/hashtags` and `/v1/satellite/sounds` | All others | No cap | No cap | 10,000 | Each endpoint in a row has its own counter. - **[Batch creator lookup](https://dev.virlo.ai/docs/satellite#batch-creator-lookup)** (`POST /v1/satellite/creators/batch`) takes up to 25 creators per call and uses the 10,000-per-day limit, not the 5-per-minute one. Each creator costs $0.50, or up to $1.00 with audience data, unless the 6-hour cache answers it for free. - **Lookup status checks** also use the 10,000-per-day limit. Check every 15 seconds, or whatever `retry_after_seconds` says. - **Agents.** These limits sit above the busiest real use we have seen, so normal polling and paging never meets them. Check a running agent every 15 seconds (or whatever `retry_after_seconds` says), not in a tight loop. Old agent endpoints (Legacy) go as low as 2 per minute. ## Handling rate limits When you get a `429`: 1. Check the [`code`](https://dev.virlo.ai/docs/errors). `rate_limit_exceeded`: you hit a limit, so wait. `too_many_requests`: fix your missing or mistyped key. 2. Wait the seconds in `Retry-After` (or `retry_after` in the body), then retry. 3. No wait time given? Wait a few seconds, doubling after each failed try. To avoid more, pace to the limit: 5 per minute is one request every 12 seconds; 50 per minute, one every 1.2 seconds. **429 response:** ```json { "statusCode": 429, "code": "rate_limit_exceeded", "message": "Rate limit exceeded", "error": "Too Many Requests", "limit": 5, "remaining": 0, "reset_at": 1790270580, "reset_at_formatted": "September 24th, 2026, 5:23 PM UTC", "retry_after": 42 } ``` > **Note:** The body also repeats `resetAt`, `resetAtFormatted`, and `retryAfter`. These older names will be removed, so read the ones above. ## Need higher limits? Email or [book a call](https://cal.com/virlo-support). --- # Webhooks > Get a message at your own web address when an agent run, lookup, tracking check or trend update is done. Set up webhooks, check deliveries and resend failed ones. Source: https://dev.virlo.ai/docs/webhooks A webhook sends a message to your server when something happens in Virlo, such as an agent run finishing. You don't have to keep checking back. **At a glance** - **What it does:** Tells your server the moment a job is done or something you watch changes. - **You send:** An HTTPS address on your server and the events you want. - **You get back:** A message at that address for each event, with the details inside. - **Cost:** Free. The work that triggers a message is billed as usual. - **How long:** Usually seconds after the event. --- ## How webhooks work 1. **Add a webhook** (the API calls it an endpoint): your address and the events you want. Use [`POST /v1/webhooks`](https://dev.virlo.ai/docs/webhooks#create-webhook) for any event, or the [Integrations page](https://dev.virlo.ai/dashboard/integrations) for agent runs, lookups and global trends. 2. **Virlo sends a message** (an HTTP `POST`) when an event happens. 3. **Your server replies** with a success code (200 to 299, written `2xx`) within 30 seconds. 4. **Failures are retried.** The [delivery log](https://dev.virlo.ai/docs/webhooks#list-deliveries) shows each delivery, how many attempts it took, and your server's last reply. Webhooks belong to your team and get events for all its work. --- ## Supported events | Event | Fires when | | - | - | | [`content_research_agent.run.completed`](https://dev.virlo.ai/docs/webhooks#agent-run-payload) | A Content Research Agent run finishes or fails. | | [`content_research_agent.event.detected`](https://dev.virlo.ai/docs/webhooks#event-detected-payload) | A recurring agent spots a breaking story in its topic. | | [`satellite.lookup.completed`](https://dev.virlo.ai/docs/webhooks#lookup-payload) | A creator, sound, hashtag or video outlier lookup finishes or fails. | | [`trends.daily.completed`](https://dev.virlo.ai/docs/webhooks#trend-payload) | The global trend list updates (up to 3 times a day, despite the name). | | [`trends.region.completed`](https://dev.virlo.ai/docs/webhooks#trend-payload) | A country trend list updates. One message per country. | | [`tracking.cycle.completed`](https://dev.virlo.ai/docs/webhooks#tracking-payload) | A tracked creator or video gets its scheduled check. | | [`tracking.outlier_video.detected`](https://dev.virlo.ai/docs/webhooks#tracking-payload) | A tracked creator posts a video with over 3 times their median views. | | [`tracking.paused`](https://dev.virlo.ai/docs/webhooks#tracking-payload) | Virlo pauses tracking: 3 failed checks in a row, or your balance can't cover a check. | | [`audience.snapshot.completed`](https://dev.virlo.ai/docs/webhooks#audience-payload) | An audience snapshot you asked for is ready. | Trend events are public: no team or user details. There's no separate failure event. Check `data.status`: agent runs and lookups say `success` or `failed`; trend updates include it only when they fail; breaking stories always say `confirmed`; tracking and audience events don't have it. > **Note:** **Older names.** `orbit.run.completed` (agents that run once) and `comet.run.completed` (recurring agents) still work and still fire. Subscribe to the matching old name and the new one, and you get two messages per run. --- ## What we send Every message has the same outer layer. `data` depends on the [event](https://dev.virlo.ai/docs/webhooks#data-shape). **JSON:** ```json { "id": "718bdf28-2a09-4813-88cd-71013dfeeab3", "event": "content_research_agent.run.completed", "created_at": "2026-09-24T17:32:28.733Z", "data": { ... } } ``` `id` is a UUID matching the `Virlo-Event-Id` header and the log's delivery `id`. Retries reuse it, so use it to skip repeats. Two webhooks getting the same event get different `id`s. Headers we send: ``` Content-Type: application/json Content-Encoding: gzip # only when the body is over 1 KB User-Agent: Virlo-Webhooks/1.0 Virlo-Event-Id: Virlo-Event-Type: content_research_agent.run.completed ``` ### Large messages Bodies over 1 KB arrive gzip-compressed. Express unzips them; Flask doesn't (see below). A message over 10 MB compressed is never sent: it's marked `payload_too_large` and can't be retried. --- ## Responding to a delivery Reply `2xx` within 30 seconds. Anything else is a failure and gets [retried](https://dev.virlo.ai/docs/webhooks#retry-ladder). Only the status code matters. Redirects are not followed, so register the final URL. Reply first, then do the slow work. This receiver checks a secret header, replies fast and skips repeats: **Node.js:** ```js import express from 'express' const app = express() // Demo only: store seen IDs in your database so repeats are caught after a restart. const seen = new Set() // express.json() unzips gzip bodies for you. // Raise the size limit: lookup results can be large. app.post('/webhooks/virlo', express.json({ limit: '25mb' }), (req, res) => { // 1. Check your secret header (see Security). if (req.get('X-Webhook-Secret') !== process.env.VIRLO_WEBHOOK_SECRET) { return res.sendStatus(401) } // 2. Reply fast, then do the work. res.sendStatus(200) // 3. Skip duplicates. A retry reuses the same id. const event = req.body if (seen.has(event.id)) return seen.add(event.id) console.log(event.event, event.data) // your code here }) app.listen(3000) ``` **Python:** ```python import gzip import json import os from flask import Flask, abort, request app = Flask(__name__) # Demo only: store seen IDs in your database so repeats are caught after a restart. seen = set() @app.post('/webhooks/virlo') def virlo_webhook(): # 1. Check your secret header (see Security). if request.headers.get('X-Webhook-Secret') != os.environ['VIRLO_WEBHOOK_SECRET']: abort(401) # 2. Flask leaves gzip bodies compressed, so unzip them here. body = request.get_data() if body[:2] == b'\x1f\x8b': body = gzip.decompress(body) event = json.loads(body) # 3. Skip duplicates. A retry reuses the same id. # For slow work, save the event and hand it to a background job, then return 200. if event['id'] not in seen: seen.add(event['id']) print(event['event'], event['data']) # your code here return '', 200 ``` --- ## Retries > **Note:** **Retries run late for now.** Each usually goes out about a day after the last, not on the schedule below. If you miss a message, check the result with the matching GET call. The planned schedule, counting from the previous attempt: | Attempt | When | | - | - | | 1 | Right away | | 2 | 30 seconds later | | 3 | 2 minutes later | | 4 | 10 minutes later | | 5 | 1 hour later | | 6 | 6 hours later | | 7 | 24 hours later | `408`, `429`, `5xx`, timeouts and network errors get all 7 attempts, then the delivery is `dead`. Other codes, including redirects and `404`, get one more try, then it's `failed`. Resend either with [Retry delivery](https://dev.virlo.ai/docs/webhooks#retry-delivery). ### Automatic shutdown Every failed attempt adds 1 to `consecutive_failures`, and any success resets it to 0. At 20, Virlo shuts the webhook down (`status: "disabled_by_system"`). Until you [re-enable it](https://dev.virlo.ai/docs/webhooks#reenable-webhook): - New events are not sent or logged, and can't be resent. - Deliveries waiting for a retry are marked `failed`. - It still counts toward your limit of 5 webhooks. --- ## Security - **Messages are not signed** (no signature header or secret), so anyone could fake one. Add a secret header, like `X-Webhook-Secret` with a long random value, and reject requests without it. - **Header values are visible** in plain text to anyone with your team's API keys. Use a secret made just for this. - **HTTPS and public addresses only.** `localhost`, `.local`, `.internal` and private IP addresses are rejected. To test locally, use a tunnel with a public HTTPS address. - **Duplicates and order.** A delivery can arrive twice with the same `id`, and order isn't guaranteed. Sort by `created_at` if needed. --- ## What each event contains ### Agent run done `content_research_agent.run.completed` fires once per run. For a run that worked, it waits for the AI report. Where the agent's settings sit depends on the outcome: | Outcome | `status` | Agent settings are under | Also included | | - | - | - | - | | It worked | `success` | `orbit` (runs once) or `comet` (recurring) | `is_recurring`, `analysis` | | It failed | `failed` | `content_research_agent`, with `is_recurring` inside | `intent_summary` | Read the settings either way with `data.orbit ?? data.comet ?? data.content_research_agent`. `analysis` is `null` when the AI report failed or found no new videos. The videos are still saved ([`GET /v1/agents/:id/videos`](https://dev.virlo.ai/docs/agents#get-agent-videos)). `metrics.credits_used` is an internal count of the data requests the run made, not your charge. A run costs $0.50, or $1.50 with Data Intelligence. Each charge is on the [Usage](https://dev.virlo.ai/dashboard/usage) page. **Agent run finished:** ```json { "id": "5359b43e-9efe-4326-83c8-293281d38e74", "event": "content_research_agent.run.completed", "created_at": "2026-09-20T00:06:19.544Z", "data": { "run_id": "8781db67-9f9a-41ac-8ba2-90c7a7dded3a", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "user_id": "f70e0123-e5f6-7890-abcd-ef1234567890", "status": "success", "started_at": "2026-09-20T00:00:11.819+00:00", "completed_at": "2026-09-20T00:04:52.223+00:00", "is_recurring": true, "metrics": { "credits_used": 158, "execution_time_ms": 280513, "videos_linked": 70, "videos_inserted": 44, "videos_updated": 73, "meta_ads_linked": 0, "outliers_identified": 0, "language_filtered": 28, "exclude_keywords_filtered": 0, "platforms": { "youtube": 43, "tiktok": 135, "instagram": 5 } }, "comet": { "id": "ca818bcf-05e3-4143-9434-a087df93a74d", "name": "AI video research", "keywords": ["ai video", "faceless ai"], "platforms": ["youtube", "tiktok", "instagram"], "cadence": "0 0 * * 0", "min_views": 5000, "time_range": "this_week", "intent": "Find which AI video formats get the most saves", "last_run_at": "2026-09-13T00:06:27.196+00:00", "next_run_at": "2026-09-27T00:00:00+00:00", "is_active": true }, "analysis": { "id": "fab02395-c200-45e4-a84d-17efd0fd8228", "status": "completed", "analysis_data": { "key_highlight": "POV-style rants dominate AI video content with 3x higher save rates", "overview": { "avg_virality": 0.794, "total_videos": 36 }, "themes": [ { "stable_key": "pov-first-person-rant", "name": "POV First-Person Rants", "why_it_works": "Direct eye-contact delivery drives saves.", "tactics": ["open with strong emotional claim", "handheld camera"], "confidence": 0.91, "video_count": 12, "evidence_video_ids": ["36d99a1e-c7d1-4d78-bc01-fe6717ec0c8c"] } ], "viral_tactics": ["Hook in first 2 seconds"], "timing_analysis": { "peak_hours": [9, 12, 18] }, "connecting_thread": "Every theme leans on a strong first-person opinion.", "top_10_breakdown": { "intro_header": "Opinions beat tutorials this week", "videos": [ ... ] }, "excluded_videos": [ ... ] }, "trends": [ ... ], "video_count": 36, "cost_usd": 0.0182842 }, "error": null } } ``` **Details for developers** A run that failed: **JSON:** ```json { "id": "9d10dfc2-4269-4655-b294-f4a0609fd19b", "event": "content_research_agent.run.completed", "created_at": "2026-09-20T00:10:02.117Z", "data": { "run_id": "b7e1c0d2-5f3a-4e8b-9c1d-2a3b4c5d6e7f", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "user_id": "f70e0123-e5f6-7890-abcd-ef1234567890", "status": "failed", "started_at": "2026-09-20T00:00:11.819+00:00", "completed_at": "2026-09-20T00:10:01.904+00:00", "metrics": { "credits_used": 0, "execution_time_ms": 0, "videos_linked": 0, "videos_inserted": 0, "videos_updated": 0, "meta_ads_linked": 0, "outliers_identified": 0, "youtube_count": 0, "tiktok_count": 0, "instagram_count": 0 }, "intent_summary": null, "content_research_agent": { "id": "ca818bcf-05e3-4143-9434-a087df93a74d", "name": "AI video research", "is_recurring": true, "keywords": ["ai video", "faceless ai"], "platforms": ["youtube", "tiktok", "instagram"], "cadence": "0 0 * * 0", "min_views": 5000, "time_range": "this_week", "intent": "Find which AI video formats get the most saves", "last_run_at": "2026-09-13T00:06:27.196+00:00", "next_run_at": "2026-09-27T00:00:00+00:00", "active": true }, "error": { "code": "TIMEOUT_ERROR", "message": "Request timeout" } } } ``` - `status` (string, optional): A run where some keywords or platforms failed still arrives as `success`. [`GET /v1/agents/:id/runs`](https://dev.virlo.ai/docs/agents#list-agent-runs) shows it as `partial_failure`. Rarely, a run whose AI report couldn't start says `success` or `partial_failure` but is laid out like a failed run. - `metrics` (object, optional): `videos_linked` is how many videos the run added to your agent. `platforms` counts videos before some filters, so it's often higher than `videos_linked`. Failed runs send zeros, with `youtube_count`, `tiktok_count` and `instagram_count` in place of `platforms`. - `orbit, comet, content_research_agent` (object, optional): `cadence` is a cron expression, such as `0 0 * * 0` for weekly, even if you chose `weekly`. `min_views` and `time_range` are set by Virlo: you can't change them through `/v1/agents`. - `intent_summary` (object | null, optional): Failed runs only: `{ matched, total_evaluated }`, or `null`. - `analysis` (object | null, optional): `analysis.trends` is a copy of `analysis_data.themes`: patterns in your agent's videos, not the global trend list. A theme's `stable_key` stays the same across runs. `cost_usd` is Virlo's own AI cost, not yours. ### Breaking story found `content_research_agent.event.detected` arrives when a recurring agent confirms a breaking story in its topic. [`GET /v1/agents/:id/events`](https://dev.virlo.ai/docs/agents/autopilot#get-agent-events) lists them too. **content_research_agent.event.detected:** ```json { "id": "2f6c9a1e-3b7d-4e8f-a0c2-5d9b1e7f3a64", "event": "content_research_agent.event.detected", "created_at": "2026-08-03T14:05:00.312Z", "data": { "agent_id": "bc6b7fb3-fa0e-4c21-9e06-27d36558bbe4", "agent_name": "Peak Climbing Insights", "is_recurring": true, "event_id": "e1111111-1111-4111-8111-111111111111", "title": "Broad Peak avalanche", "summary": "A serac collapse on Broad Peak triggered a rescue effort.", "salience": 9, "source": "news_scan", "status": "confirmed", "detected_at": "2026-08-03T14:04:41Z", "confirmed_at": "2026-08-03T14:05:02Z", "expires_at": "2026-08-17T14:04:41Z", "event_date": null, "keywords": ["broad peak avalanche", "nimsdai tribute"], "keywords_added": ["broad peak avalanche", "nimsdai tribute"], "evidence": [ { "title": "Avalanche strikes Broad Peak, rescue underway", "url": "https://www.bbc.com/news/world-asia-...", "domain": "bbc.com" } ], "action_taken": "collecting_early", "next_run_at": "2026-08-03T14:30:00Z" } } ``` - `salience`: how big the story is, 0 to 10. - `source`: `news_scan` (news articles) or `corpus_burst` (a jump in the agent's own videos). - `keywords_added`: temporary story keywords. Agents on [autopilot](https://dev.virlo.ai/docs/agents/autopilot#autonomy) add them; agents with autopilot off leave a [proposal](https://dev.virlo.ai/docs/agents/autopilot#list-agent-proposals) instead. - `action_taken`: `collecting_early` (next run sooner), `breaking_ingest` (fresh videos now), `timely_keywords` (keywords only) or `none`. No extra cost. ### Lookup done In `satellite.lookup.completed`, `data.type` is `creator_lookup`, `sound_lookup`, `hashtag_lookup` or `video_outlier`. A batch creator lookup sends one message per creator. On success, the full result is in `data.results`. On failure, `error` says why. No message comes for a creator, sound or hashtag lookup repeated within 6 hours (the start call returns `cached: true`), or for a lookup that stalls. Read those with `GET /v1/satellite/runs/:run_id` (free). **Creator lookup finished:** ```json { "id": "3b0f2c4e-8d1a-4f6b-9e2c-7a5d1b3c9e0f", "event": "satellite.lookup.completed", "created_at": "2026-09-08T00:13:59.235Z", "data": { "run_id": "22222222-3333-4444-5555-666666666666", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "user_id": "f70e0123-e5f6-7890-abcd-ef1234567890", "status": "success", "type": "creator_lookup", "platform": "tiktok", "started_at": "2026-09-08T00:12:41.018+00:00", "completed_at": "2026-09-08T00:13:59.101Z", "metrics": { "credits_used": 0, "execution_time_ms": 0 }, "request": { "include_videos": true, "max_videos": 50, "trend_analysis": false, "data_intelligence": false }, "results": { "username": "khaby.lame", "platform": "tiktok", "profile": { ... }, "stats": { ... }, "videos": [ ... ] }, "error": null } } ``` - `metrics` is always `0` for now. Your charge is the `X-Credits-Used` header on the call that started the lookup. - With [Data Intelligence](https://dev.virlo.ai/docs/intelligence), that part can finish after the message. Read the run again until its `intelligence_status` is `ready`. ### Trend list updated `trends.daily.completed` is the global list, `trends.region.completed` each country's. Each updates up to 3 times daily, around 7am, 1pm and 7pm in that country's time zone (New York for global). Same-day updates share a `trend_group_id`. **trends.daily.completed:** ```json { "id": "48571bb3-8861-4f11-a992-81282a3045f4", "event": "trends.daily.completed", "created_at": "2026-09-21T17:09:45.359Z", "data": { "trend_group_id": "eee23630-ac9c-4b1f-9ed1-9b9d678d2e6c", "date": "2026-09-20", "region": "global", "trends": [ { "trend_id": "3d8e6476-14f3-4ce5-aefa-161e9a03a386", "trend_ranking_id": "843bd83d-5cfa-4bca-9b78-9ca3a086cbd1", "title": "Back-to-school desk makeovers", "description": "Students film before-and-after tours of cheap desk upgrades.", "ranking": 1, "scrape_status": "success", "exemplar_count": 358, "velocity": { "band": "sustained", "count_24h_total": 37, "median_views": 31513, "source": "trend_scrape_finalize", "per_platform": { "tiktok": 27, "youtube": 10, "instagram": 0 } }, "top_exemplars": [ { "video_id": "0b50764e-cc0a-4959-bf95-e5cb404efe41", "url": "https://www.tiktok.com/@creator/video/7687529554881039638", "platform": "tiktok", "views": 8169413, "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/a2197c4e.jpg", "publish_date": "2026-09-20T08:20:37", "author": { "username": "creator", "avatar_url": "...", "verified": false } } ] } ] } } ``` **Details for developers** - `date` (string, optional): The previous day, whose videos the trends are based on. - `trends[].velocity` (object, optional): An early speed reading from matching videos posted in the 24 hours before detection. `band` is `breaking`, `accelerating`, `sustained`, `fading` or `null`. It is not the [momentum](https://dev.virlo.ai/docs/trends#momentum) label. A failed update sends `status: "failed"`. The next update runs as usual. **trends.region.completed (failed):** ```json { "id": "c41a9e27-5b3d-4f08-8e6a-2d7f1c9b0a53", "event": "trends.region.completed", "created_at": "2026-09-21T06:14:08.221Z", "data": { "run_id": null, "region": "gb", "status": "failed", "completed_at": "2026-09-21T06:14:08.198Z", "error": { "code": "trend_analysis_failed", "message": "Trend analysis failed" } } } ``` ### Tracking updates These only come for creators and videos tracked through the [Tracking API](https://dev.virlo.ai/docs/tracking), not the web app. Video messages have `video_url` in place of `platform_handle`. **`tracking.cycle.completed`**: `snapshot` has the latest numbers, `delta_` fields the change since the last check. Zero or unknown numbers are left out, so treat every field as optional. Creators may also have `subscribers`, `following`, `total_videos`, `posts_analyzed` and `delta_subscribers`. Videos have `title`, `views`, `likes`, `comments`, `shares` and `bookmarks`. Read the check's report with [Get creator report](https://dev.virlo.ai/docs/tracking#get-creator-report) or [Get video report](https://dev.virlo.ai/docs/tracking#get-video-report). **tracking.cycle.completed:** ```json { "id": "a3f9c2d1-7e4b-4c8a-9f1e-2b6d8c0a4e71", "event": "tracking.cycle.completed", "created_at": "2026-09-22T14:30:00.412Z", "data": { "tracking_type": "creator", "tracking_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "fitnessguru", "snapshot": { "followers": 245000, "total_views": 18700000, "total_likes": 920000, "delta_followers": 1250, "delta_views": 340000, "delta_likes": 15800 }, "new_posts_count": 6, "report_available": true, "dashboard_url": "https://app.virlo.ai/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` **`tracking.outlier_video.detected`**: `outlier_ratio` is views divided by the creator's median. `weighted_score` is ln(`outlier_ratio`) × ln(`median_views`), so the same jump scores higher for a bigger creator. **tracking.outlier_video.detected:** ```json { "id": "e7b1d4a2-9c3f-4e6b-8a0d-5f2c7e9b1a34", "event": "tracking.outlier_video.detected", "created_at": "2026-09-22T14:30:00.598Z", "data": { "tracking_type": "creator", "tracking_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "fitnessguru", "outlier_videos": [ { "url": "https://tiktok.com/@fitnessguru/video/7400123456", "title": "This 30-second trick changed my morning routine", "views": 4800000, "median_views": 320000, "outlier_ratio": 15.0, "weighted_score": 34.33, "publish_date": "2026-09-18T14:30:00Z" } ], "creator_median_views": 320000, "dashboard_url": "https://app.virlo.ai/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` **`tracking.paused`**: `reason` is `insufficient_credits` or `max_failures_reached`. Add funds or fix the problem, then set `status` to `active` with [Update tracked creator](https://dev.virlo.ai/docs/tracking#update-tracked-creator) or [Update tracked video](https://dev.virlo.ai/docs/tracking#update-tracked-video). **tracking.paused:** ```json { "id": "0c9d8e7f-6a5b-4c3d-9e2f-1a0b9c8d7e6f", "event": "tracking.paused", "created_at": "2026-09-22T14:31:12.004Z", "data": { "tracking_type": "creator", "tracking_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "platform": "tiktok", "platform_handle": "fitnessguru", "reason": "insufficient_credits", "pause_reason": "insufficient_credits", "dashboard_url": "https://app.virlo.ai/tracking/creators/a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } ``` ### Audience snapshot ready A snapshot estimates a creator's audience (age, gender, country, city, language), mostly from comments. Request one with [Refresh audience snapshot](https://dev.virlo.ai/docs/tracking#refresh-audience-snapshot) or a [creator lookup](https://dev.virlo.ai/docs/satellite) with audience options. A failed one sends nothing and is refunded. No message? Check `GET /v1/audience/snapshot/:job_id` (free). **audience.snapshot.completed:** ```json { "id": "f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b", "event": "audience.snapshot.completed", "created_at": "2026-09-01T12:04:33.120Z", "data": { "snapshot_id": "f5a3b2c1-d4e5-4f6a-8b7c-9d0e1f2a3b4c", "platform": "tiktok", "handle": "khaby.lame", "sample_size": 712, "cost_usd": 0.31, "confidence_per_signal": { "age": 0.72, "gender": 0.84, "country": 0.79, "city": 0.69, "language": 0.81 }, "confidence_level": "high", "data_source": "comments", "signal_breakdown": { "comments": 712, "followers": 0 } } } ``` - `confidence_level` (`low`, `medium` or `high`) says how far to trust it. - `data_source: "profile_only"` means no audience data was found: it's always `low`, and the $0.50 charge is refunded. - `cost_usd` is Virlo's own AI cost, not yours. Full breakdowns: [demographics](https://dev.virlo.ai/docs/tracking#get-audience-demographics) and [geography](https://dev.virlo.ai/docs/tracking#get-audience-geography) for a tracked creator, or the lookup result (`GET /v1/satellite/runs/:run_id`) for a creator lookup. `GET /v1/audience/snapshot/:job_id` covers either for 24 hours. --- ## Managing webhooks The calls below manage your webhooks. Unlike other Virlo calls, their responses are not wrapped in `{ "data": ... }`. **Webhook object fields** - `status` (string, optional): `active`, `inactive` (you deleted it or switched it off) or `disabled_by_system` (20 failed attempts in a row). - `is_active` (boolean, optional): Stays `true` after an automatic shutdown, so check `status`. ### Errors Errors use the standard [error body](https://dev.virlo.ai/docs/errors#error-shape). | Status | `code` | When | | - | - | - | | `400` | `validation_error` | Bad URL or header, empty `enabled_events`, unknown field, malformed ID, or a key with no team | | `400` | `unknown_event_type` | Unsupported event name (the message lists every accepted one) | | `401` | `missing_api_key`, `invalid_api_key` | No API key, or a wrong one | | `404` | `not_found` | No webhook or delivery with that ID on your team | | `409` | `conflict` | Your team already has 5 webhooks switched on | **400:** ```json { "message": "Unknown event type(s): foo.bar. Supported: content_research_agent.run.completed, content_research_agent.event.detected, comet.run.completed, orbit.run.completed, satellite.lookup.completed, trends.daily.completed, trends.region.completed, tracking.cycle.completed, tracking.outlier_video.detected, tracking.paused, audience.snapshot.completed", "error": "Bad Request", "statusCode": 400, "code": "unknown_event_type" } ``` --- ## Create webhook **Endpoint:** `POST https://api.virlo.ai/v1/webhooks` Adds a webhook for your team. **Cost:** free. Your team can have 5 webhooks switched on, counting any Virlo shut down for failing. Only [Delete](https://dev.virlo.ai/docs/webhooks#delete-webhook) or `is_active: false` frees a slot. ### Required - `url` (string, required): Must start with `https://`, be at most 2048 characters, and point to a public server. - `enabled_events` (string[], required): Names from [Supported events](https://dev.virlo.ai/docs/webhooks#supported-events), or the two older agent names. Can't be empty. ### Optional - `headers` (object, optional): Up to 5 headers sent with every message. Use one as a shared secret. Names: letters, numbers and dashes, up to 64 characters. Values: text, up to 256 characters. Reserved in any capitalization: `Authorization`, `Content-Type`, `Content-Encoding`, `Content-Length`, `Host`, `User-Agent`, and anything starting with `Virlo-`. - `description` (string, optional): A label, up to 256 characters. - `is_active` (boolean, optional): `false` creates it switched off. Defaults to `true`. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": [ "content_research_agent.run.completed" ], "headers": { "X-Webhook-Secret": "your-shared-secret" } }' ``` **Response 201:** ```json { "id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": ["content_research_agent.run.completed"], "headers": { "X-Webhook-Secret": "your-shared-secret" }, "is_active": true, "status": "active", "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "created_by": "f70e0123-e5f6-7890-abcd-ef1234567890", "created_at": "2026-09-24T17:31:07.851257+00:00", "updated_at": "2026-09-24T17:31:07.851257+00:00" } ``` **Response 400:** ```json { "message": "header key \"Authorization\" is reserved and cannot be set by the customer", "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` **Response 409:** ```json { "message": "Team already has the maximum of 5 active webhook endpoints. Disable or delete one before creating another.", "error": "Conflict", "statusCode": 409, "code": "conflict" } ``` --- ## List webhooks **Endpoint:** `GET https://api.virlo.ai/v1/webhooks` Returns all your team's webhooks, newest first, with no paging. **Cost:** free. Deleted webhooks stay in the list with `status: "inactive"`. **cURL request:** ```bash curl https://api.virlo.ai/v1/webhooks \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json [ { "id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": ["content_research_agent.run.completed"], "headers": { "X-Webhook-Secret": "your-shared-secret" }, "is_active": true, "status": "active", "consecutive_failures": 0, "last_success_at": "2026-09-24T18:02:11.204+00:00", "last_failure_at": null, "created_by": "f70e0123-e5f6-7890-abcd-ef1234567890", "created_at": "2026-09-24T17:31:07.851257+00:00", "updated_at": "2026-09-24T18:02:11.204+00:00" } ] ``` --- ## Get webhook **Endpoint:** `GET https://api.virlo.ai/v1/webhooks/:id` Returns one webhook. **Cost:** free. - `id` (uuid, required): The webhook's ID. An unknown ID returns `404`. A malformed one returns `400`. **cURL request:** ```bash curl https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": ["content_research_agent.run.completed"], "headers": { "X-Webhook-Secret": "your-shared-secret" }, "is_active": true, "status": "active", "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "created_by": "f70e0123-e5f6-7890-abcd-ef1234567890", "created_at": "2026-09-24T17:31:07.851257+00:00", "updated_at": "2026-09-24T17:31:07.851257+00:00" } ``` **Response 404:** ```json { "message": "Webhook not found", "error": "Not Found", "statusCode": 404, "code": "not_found" } ``` --- ## Update webhook **Endpoint:** `PATCH https://api.virlo.ai/v1/webhooks/:id` Changes only the fields you send and returns the full, updated webhook. **Cost:** free. - `url` (string, optional): A new HTTPS address. Same checks as on create. - `enabled_events` (string[], optional): Replaces the whole list. Can't be empty. - `headers` (object, optional): Replaces all headers, so send every one you want to keep. `{}` removes them all. `null` returns `400`. - `description` (string | null, optional): A new label. `null` clears it. - `is_active` (boolean, optional): Switch it on or off. This doesn't undo an automatic shutdown: use [Re-enable webhook](https://dev.virlo.ai/docs/webhooks#reenable-webhook). **cURL request:** ```bash curl -X PATCH https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled_events": [ "content_research_agent.run.completed", "satellite.lookup.completed" ] }' ``` **Response 200:** ```json { "id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": [ "content_research_agent.run.completed", "satellite.lookup.completed" ], "headers": { "X-Webhook-Secret": "your-shared-secret" }, "is_active": true, "status": "active", "consecutive_failures": 0, "last_success_at": null, "last_failure_at": null, "created_by": "f70e0123-e5f6-7890-abcd-ef1234567890", "created_at": "2026-09-24T17:31:07.851257+00:00", "updated_at": "2026-09-24T17:45:00.112+00:00" } ``` --- ## Delete webhook **Endpoint:** `DELETE https://api.virlo.ai/v1/webhooks/:id` Switches a webhook off and frees one of your 5 slots. **Cost:** free. Nothing is erased: it stays in your list with its delivery log, and no call removes it. Deleting twice returns `204` both times. **cURL request:** ```bash curl -X DELETE https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_API_KEY" ``` --- ## Re-enable webhook **Endpoint:** `POST https://api.virlo.ai/v1/webhooks/:id/reenable` Turns a webhook back on after an [automatic shutdown](https://dev.virlo.ai/docs/webhooks#auto-disable). Fix your server first. **Cost:** free. It sets `status` to `active`, `consecutive_failures` to 0 and `is_active` to `true`, so it also revives a deleted webhook, even past the limit of 5. Failed deliveries aren't resent: use [Retry delivery](https://dev.virlo.ai/docs/webhooks#retry-delivery). **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890/reenable \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 201:** ```json { "id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "url": "https://hooks.example.com/virlo", "description": "Production webhook", "enabled_events": ["content_research_agent.run.completed"], "headers": { "X-Webhook-Secret": "your-shared-secret" }, "is_active": true, "status": "active", "consecutive_failures": 0, "last_success_at": "2026-09-20T09:12:44.018+00:00", "last_failure_at": "2026-09-22T06:24:17.698+00:00", "created_by": "f70e0123-e5f6-7890-abcd-ef1234567890", "created_at": "2026-09-01T17:31:07.851257+00:00", "updated_at": "2026-09-24T17:36:10.998+00:00" } ``` --- ## Send test event **Endpoint:** `POST https://api.virlo.ai/v1/webhooks/:id/test` Sends a fake message, usually within 2 seconds, so you can check your server replies `2xx`. The result shows in the [delivery log](https://dev.virlo.ai/docs/webhooks#list-deliveries). **Cost:** free. - `event_type` (string, optional): Send `"content_research_agent.run.completed"`. If you leave it out or send an unknown name, the test is labeled `orbit.run.completed`, which your code may ignore. The body is the same stub for every event name (`data.test: true`, `data.run_id: "test-run"`). It's sent even if the webhook isn't subscribed to that event. On a switched-off webhook it fails at once, with `last_error` `endpoint_inactive` (you switched it off) or `endpoint_disabled_by_system` (Virlo did). **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890/test \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"event_type": "content_research_agent.run.completed"}' ``` **Response 202:** ```json { "delivery_id": "718bdf28-2a09-4813-88cd-71013dfeeab3", "event_type": "content_research_agent.run.completed" } ``` **Test message:** ```json { "id": "718bdf28-2a09-4813-88cd-71013dfeeab3", "event": "content_research_agent.run.completed", "created_at": "2026-09-24T17:32:28.733Z", "data": { "run_id": "test-run", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "user_id": null, "status": "success", "started_at": "2026-09-24T17:32:28.733Z", "completed_at": "2026-09-24T17:32:28.733Z", "metrics": { "credits_used": 0, "execution_time_ms": 0 }, "results": null, "error": null, "test": true } } ``` --- ## List deliveries **Endpoint:** `GET https://api.virlo.ai/v1/webhooks/:id/deliveries` Shows messages sent to one webhook, newest first, with your server's last reply. **Cost:** free. Each delivery has a `status`: - `pending`: not sent yet, or waiting for a retry. - `succeeded`: your server replied `2xx`. - `failed`: a reply not worth retrying, or the webhook was off. - `dead`: all 7 attempts failed. - `payload_too_large`: over 10 MB, never sent. - `status, event_type` (string, optional): Filter by one value. An unknown value returns an empty list. - `limit` (integer, optional): 1 to 100, default 50. Out-of-range values are adjusted, not rejected. - `cursor` (string, optional): The last page's `next_cursor` (such as `2026-09-24T17:32:30.077677+00:00`), URL-encoded. The `+` must become `%2B`, or you get `400`. Stop at `null`. A full page always has a cursor, so the last page can be empty. **Full field reference** - `max_attempts` (integer, optional): Shows 3 for tests, but every delivery gets up to 7. - `last_response_status, last_response_headers, last_response_body_snippet` (various, optional): Your server's reply to the last attempt only. The body is cut at 4,096 characters. - `last_error` (string | null, optional): `http_` plus the code (such as `http_404`), a network error, `endpoint_inactive`, `endpoint_disabled_by_system`, `endpoint_deleted` or `payload_too_large`. - `completed_at` (string | null, optional): After a retry, this and `last_error` keep their old values until the new attempt finishes. **cURL request:** ```bash curl "https://api.virlo.ai/v1/webhooks/1a2b3c4d-e5f6-7890-abcd-ef1234567890/deliveries?status=failed&limit=25" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 200:** ```json { "endpoint_id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "deliveries": [ { "id": "5359b43e-9efe-4326-83c8-293281d38e74", "endpoint_id": "1a2b3c4d-e5f6-7890-abcd-ef1234567890", "team_id": "0d3f1234-e5f6-7890-abcd-ef1234567890", "event_type": "content_research_agent.run.completed", "source_run_id": "8781db67-9f9a-41ac-8ba2-90c7a7dded3a", "source_table": "content_research_agent_runs", "payload": { "id": "5359b43e-9efe-4326-83c8-293281d38e74", "event": "content_research_agent.run.completed", "created_at": "2026-09-20T00:06:19.544Z", "data": { ... } }, "payload_size_bytes": 17791, "status": "failed", "attempt_count": 2, "max_attempts": 7, "next_attempt_at": null, "last_attempted_at": "2026-09-20T00:06:50.102+00:00", "last_response_status": 404, "last_response_headers": { "content-type": "text/plain" }, "last_response_body_snippet": "Not Found", "last_error": "http_404", "completed_at": "2026-09-20T00:06:50.102+00:00", "created_at": "2026-09-20T00:06:19.606703+00:00" } ], "limit": 25, "next_cursor": null } ``` --- ## Retry delivery **Endpoint:** `POST https://api.virlo.ai/v1/webhooks/deliveries/:deliveryId/retry` Sends a `failed` or `dead` delivery again. **Cost:** free. It goes back to `pending` with the same `id`, so your duplicate check still works. If the last attempt was under a day ago, the new one can take up to a day. It's one more try, not a new schedule. Any other status, including `payload_too_large`, returns `400`. **cURL request:** ```bash curl -X POST https://api.virlo.ai/v1/webhooks/deliveries/5359b43e-9efe-4326-83c8-293281d38e74/retry \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Response 202:** ```json { "queued": true } ``` **Response 400:** ```json { "message": "Only deliveries in 'failed' or 'dead' status can be retried (current: pending)", "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` --- # Errors > What a Virlo API error looks like, what each error code means, and what to do about it. Source: https://dev.virlo.ai/docs/errors Every error names its problem in `code`, such as `insufficient_credits`. 400-range errors are yours to fix; 500-range are ours. **At a glance** - **What it does:** Lists every error code and its fix. - **You get back:** Four fields. `code` is the one to check. - **Cost:** An error reply is free and has no [cost headers](https://dev.virlo.ai/docs/credits#response-headers). A creator, sound, or hashtag lookup that starts and then fails is refunded later. ## Error format Every error has four fields: **400 from POST /v1/agents:** ```json { "message": [ "keywords must contain at least 1 elements", "each value in platforms must be one of the following values: youtube, tiktok, instagram, meta_ads" ], "error": "Bad Request", "statusCode": 400, "code": "validation_error" } ``` - `code`: the error's fixed name. New codes may appear, but existing ones are never renamed or reused. - `statusCode`: the status number, such as `400` (not `status_code`). - `error`: a short label, such as `Bad Request`. - `message`: what went wrong. Usually a list when parameters fail checks, even one. A badly formed ID and other errors send one sentence. Handle both. Wording can change, so rely on `code`. Some errors add fields, for example: - `402` adds `required_credits` (the price) and `remaining_credits` (your balance), and sometimes `required_amount` and `remaining_balance` in dollars. See [Insufficient balance](https://dev.virlo.ai/docs/credits#insufficient-balance). - `429` with `rate_limit_exceeded` adds `retry_after` (seconds to wait), `limit`, `remaining` and `reset_at`. See [Rate limits](https://dev.virlo.ai/docs/rate-limits). ## Error codes | Error | When it happens | What to do | | - | - | - | | **400** `validation_error` | A parameter is missing, wrong, out of range or not accepted (`property colour should not exist`), or an ID is badly formed. A few endpoints, such as `GET /v1/agents`, ignore extra parameters. | Fix what `message` names | | **400** `invalid_date_range` | A date is missing or invalid, or the range is too long (hashtags: 90 days max) | Send both dates within the limit | | **400** `unknown_region` | The trend `region` isn't supported | See [`GET /v1/trends/regions`](https://dev.virlo.ai/docs/trends#list-regions) | | **400** `unknown_event_type` | A webhook names an event that doesn't exist | See [supported events](https://dev.virlo.ai/docs/webhooks#supported-events) | | **401** `missing_api_key` | No `Authorization` header. A key in `x-api-key` or the URL doesn't count. | Send `Authorization: Bearer YOUR_API_KEY` | | **401** `invalid_api_key` | The key is wrong, revoked, or badly formatted | Use `Bearer` (capital B) and paste the key once; it already starts with `virlo_tkn_`. If revoked, create a new one in [API keys](https://dev.virlo.ai/dashboard/api-keys) | | **402** `insufficient_credits` | Your balance is below the price (1 credit = $0.01) | Add funds in [Billing](https://dev.virlo.ai/dashboard/billing), then retry | | **403** `forbidden` | You won't see this with an API key. Only dashboard team settings return it (wrong role, or invite for another email). A failure inside Virlo is never reported as 403. | Ask a team owner or admin | | **404** `not_found` | The ID doesn't exist or belongs to another team, or the URL is wrong | Check the ID, and that the path starts with `/v1/` | | **409** `conflict` | Something is in the way, for example you're already collecting that creator's past posts, or your team has 5 active webhooks (the max). | Wait for the job to finish, or delete a webhook or set one inactive, then retry | | **429** `rate_limit_exceeded` | Your key sent too many requests | Wait `retry_after` seconds, then retry | | **429** `too_many_requests` | Your key is missing or malformed, and your IP address sent too many requests. No `retry_after`. | Fix the `Authorization` header, then slow down | | **502** `upstream_error` | A Virlo service behind the API failed or rejected Virlo's own request. Not caused by your request, and not charged. | Wait a few seconds and retry | | **503** `service_unavailable` | Virlo is briefly unavailable or overloaded (for example, a database timeout). Not charged. | Wait a few seconds and retry, with backoff | | **504** `request_timeout` | The request took longer than 27 seconds and was stopped. You are not charged for it. | Wait a few seconds and retry | | **500** `internal_error` | Something went wrong on our side | Retry once or twice, then email with the request time | ## What to do Retry automatically in two cases only: - `rate_limit_exceeded`: wait `retry_after` seconds. - Any 500-range error: wait 2 seconds, then 4, then stop. For other errors, fix the cause the table names, then retry. **Handle errors by code:** ```js const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) async function callVirlo(url, attempt = 1) { const res = await fetch(url, { headers: { Authorization: 'Bearer YOUR_API_KEY' }, }) if (res.ok) return res.json() // A few 500-range errors arrive as a web page, not JSON. // Treat those as a server-side error so they are retried too. const err = await res.json().catch(() => ({ code: res.status >= 500 ? 'service_unavailable' : 'unknown_error', message: res.statusText, })) const canRetry = attempt < 3 if (err.code === 'rate_limit_exceeded' && canRetry) { // Wait as long as the API asks, then try again. await sleep((err.retry_after ?? 60) * 1000) return callVirlo(url, attempt + 1) } const serverSide = ['upstream_error', 'service_unavailable', 'request_timeout', 'internal_error'] if (serverSide.includes(err.code) && canRetry) { // Wait 2 seconds, then 4, then give up. await sleep(2 ** attempt * 1000) return callVirlo(url, attempt + 1) } if (err.code === 'insufficient_credits') { throw new Error( `Add funds: this request needs ${err.required_credits} credits, you have ${err.remaining_credits}`, ) } // Anything else: fix the cause first (see the table). // message can be one sentence or a list of them. throw new Error(`${err.code}: ${[].concat(err.message).join('; ')}`) } ``` --- # Workflow recipes > Step-by-step code for common research questions: what works in a niche, why a creator's video took off, what is trending, which hashtags are big, and what works in another language or country. Each recipe lists its cost and how long it takes. Source: https://dev.virlo.ai/docs/recipes Code for five research questions, with the cost and time of each. Replace `YOUR_API_KEY` with [your key](https://dev.virlo.ai/docs/quickstart). **At a glance** - **What it does:** Answers one research question per recipe, call by call. - **You send:** A topic, a creator's handle, or a hashtag. - **You get back:** An AI report, top videos, ads, standout creators, trends, or hashtag stats. - **Cost:** $0.15 to $1.25 per recipe as written, before add-ons. A recurring agent then costs $0.50 per run until you pause or delete it. Reading an agent's videos, ads, creators, and report is free; hashtag and trend lists charge every time. A successful response's `X-Cost` header shows its cost. An **agent** (Content Research Agent) collects matching videos and writes an AI report, once or on a schedule. A **lookup** checks one creator, video, sound, or hashtag. - [What works in my niche?](https://dev.virlo.ai/docs/recipes#full-niche-analysis) - [Why did a creator's video take off?](https://dev.virlo.ai/docs/recipes#creator-deep-dive) - [What is trending, and can I keep watching it?](https://dev.virlo.ai/docs/recipes#trend-monitoring-setup) - [Which hashtags are biggest?](https://dev.virlo.ai/docs/recipes#hashtag-research) - [What works in Spanish, or in Mexico?](https://dev.virlo.ai/docs/recipes#global-non-english-research) --- ## Full niche analysis A one-time agent searches TikTok, YouTube, Instagram, and Meta ads, then reports what is working. **At a glance** - **Cost:** $0.50, charged when you create the agent, even if the run fails. $1.50 with [Data Intelligence](https://dev.virlo.ai/docs/intelligence): AI notes on hook and format for many, not all, videos. - **How long:** Half of all runs are done in under 8 minutes, and 9 in 10 in under 20. Broad runs like this take longer. ### 1. Create a one-time agent ($0.50) Describe what you want (`intent`) and add keywords, or get them free from [Suggest keywords](https://dev.virlo.ai/docs/agents#suggest-keywords). **cURL:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": false, "intent": "Find what is working in Jeep Wrangler mods content right now", "name": "Jeep Wrangler Mods Research", "keywords": ["jeep wrangler mods", "jeep wrangler accessories", "jeep upgrades"], "platforms": ["youtube", "tiktok", "instagram"], "meta_ads_enabled": true }' ``` **Python:** ```python import requests, time # is_recurring=False: run once. name is a label for you. # meta_ads_enabled also collects Meta ads (Facebook and Instagram ads). # No min_views or date range here: filter for free when you read results (step 3). response = requests.post( 'https://api.virlo.ai/v1/agents', headers={'Authorization': 'Bearer YOUR_API_KEY'}, json={ 'is_recurring': False, 'intent': 'Find what is working in Jeep Wrangler mods content right now', 'name': 'Jeep Wrangler Mods Research', 'keywords': ['jeep wrangler mods', 'jeep wrangler accessories', 'jeep upgrades'], 'platforms': ['youtube', 'tiktok', 'instagram'], 'meta_ads_enabled': True } ) agent_id = response.json()['data']['id'] # returned right away; the research runs in the background ``` ### 2. Wait until it's done (free) [Check again](https://dev.virlo.ai/docs/async-data) every 30 seconds until `finalized` is `true` (report written) and `last_run_at` is set (creators scored). A `partial_failure` run (one keyword or platform failed) is still usable; a `failed` run is not. **Python:** ```python deadline = time.time() + 60 * 60 # stop waiting after an hour (rare) while True: agent = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}', headers={'Authorization': 'Bearer YOUR_API_KEY'} ).json()['data'] if agent['latest_run']['status'] == 'failed': raise Exception('The run failed.') # finalized: videos collected and the AI report written # (latest_run.status "completed" alone only means videos are collected). # last_run_at: set about a minute later, when creator scoring is done. if agent['finalized'] and agent['last_run_at']: break if time.time() > deadline: # Check again later. Creating a new agent would charge you again. raise TimeoutError('Still running after an hour.') print(f"{agent['stage']}: {agent['progress_pct']}%") # e.g. "collecting: 40%" time.sleep(30) # analysis is only the headline. The full report (themes, tactics, posting times, # top 10 videos) is in analysis_data, also at GET /v1/agents/:id/analysis/latest. report = agent['analysis_data'] or {} print(agent['analysis']) # the main takeaway, in one or two sentences for theme in report.get('themes', []): print(f"- {theme['name']}: {theme['why_it_works']}") # Example output, from a latte art agent: # The most impactful insight for creators is the overwhelming success of 'progress journey' content, ... # - Progress Journey Latte Art: Documenting daily or incremental improvements creates a relatable narrative, ... ``` Or use the `content_research_agent.run.completed` [webhook](https://dev.virlo.ai/docs/webhooks#supported-events). It can arrive up to a minute before creator scoring ends, so check `last_run_at` first. ### 3. Read the results (free) Standout creators get far more views than their follower count predicts. **Python:** ```python # Free, including the filters (min_views, start_date, end_date, platforms, region). # The 50 most-viewed videos videos = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}/videos', headers={'Authorization': 'Bearer YOUR_API_KEY'}, params={'limit': 50, 'order_by': 'views', 'sort': 'desc'} ).json()['data']['videos'] # Meta ads (needs meta_ads_enabled at create time) ads = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}/ads', headers={'Authorization': 'Bearer YOUR_API_KEY'}, params={'limit': 50} ).json()['data']['ads'] # Standout creators ("creator outliers"), best Virality Score first. # weighted_score = ln(avg_views / followers) x ln(followers) # 35+ exceptional, 25-35 very strong, 18-25 strong, 10-18 promising outliers = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}/creators/outliers', headers={'Authorization': 'Bearer YOUR_API_KEY'}, params={'limit': 20, 'order_by': 'weighted_score', 'sort': 'desc'} ).json()['data']['outliers'] for creator in outliers[:5]: print(f"{creator['platform']}: {creator['follower_count']:,} followers, " f"{creator['avg_views']:,.0f} avg views, score {creator['weighted_score']}") print(f" {creator['creator_url']}") # Example output: # instagram: 1,791 followers, 475,807 avg views, score 41.81 # https://www.instagram.com/... ``` ### 4. Optional: profile a creator ($0.50 to $1.00) Run the [Creator deep dive](https://dev.virlo.ai/docs/recipes#creator-deep-dive) on their handle ($0.50, or $1.00 with its step 2). TikTok and Instagram `creator_url`s end with the handle; on YouTube, open the link and copy the @handle. **Python:** ```python for creator in outliers[:3]: handle = creator['creator_url'].rstrip('/').split('/')[-1].lstrip('@') # On YouTube this is often a channel ID (/channel/UC...), not a handle. print(creator['platform'], handle) # tiktok outside.kev ``` --- ## Creator deep dive Size up a creator, then see if their top recent video is a real breakout. **At a glance** - **Cost:** $1.00: $0.50 per lookup, charged when it starts. A failed creator lookup, such as for a handle that doesn't exist, is refunded; a failed video check isn't. - **How long:** About a minute. Each lookup usually takes 20 to 40 seconds. ### 1. Look up the creator ($0.50) **cURL:** ```bash curl "https://api.virlo.ai/v1/satellite/creator/tiktok/khaby.lame?include=videos,outliers&max_videos=20" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Python:** ```python import requests, time response = requests.get( 'https://api.virlo.ai/v1/satellite/creator/tiktok/khaby.lame', headers={'Authorization': 'Bearer YOUR_API_KEY'}, # include adds their recent videos and standout ones. max_videos: how many to read (up to 100). params={'include': 'videos,outliers', 'max_videos': 20} ) creator_job_id = response.json()['data']['job_id'] # Check the job every 10 seconds until it is completed or failed. while True: result = requests.get( f'https://api.virlo.ai/v1/satellite/creator/status/{creator_job_id}', headers={'Authorization': 'Bearer YOUR_API_KEY'} ).json()['data'] if result['status'] == 'completed': break if result['status'] == 'failed': raise Exception(result['error']) # e.g. the handle has no public posts time.sleep(10) creator = result['result'] print(f"Followers: {creator['profile']['followers']}") print(f"Avg views: {creator['stats']['avg_views']}") print(f"Engagement rate: {creator['stats']['engagement_rate']:.0%}") # 0.06 means 6% ``` ### 2. Check their top recent video ($0.50) This checks the most-viewed of the 20 recent videos the lookup read, not necessarily their all-time top. **Python:** ```python # The most-viewed of the recent videos the lookup read top_video = max(creator['videos'], key=lambda v: v['views']) response = requests.post( 'https://api.virlo.ai/v1/satellite/video-outlier', headers={'Authorization': 'Bearer YOUR_API_KEY'}, json={'url': top_video['url'], 'platform': 'tiktok'} ) video_job_id = response.json()['data']['job_id'] while True: result = requests.get( f'https://api.virlo.ai/v1/satellite/video-outlier/status/{video_job_id}', headers={'Authorization': 'Bearer YOUR_API_KEY'} ).json()['data'] if result['status'] == 'completed': break if result['status'] == 'failed': raise Exception(result['error']) time.sleep(10) analysis = result['result']['analysis'] print(f"Outlier score: {analysis['outlier_score']}") # views / the creator's median views print(f"Percentile: {analysis['percentile']}") # % of their recent videos it beat print(f"Label: {analysis['performance_label']}") # average | above_average | viral | mega_viral # Both results are saved. Reopen either for free with GET /v1/satellite/runs/{job_id}, # so keep both IDs. A new paid lookup of the same creator gets its own ID; old runs stay. ``` `outlier_score` is views divided by the creator's median views (up to 100 recent videos): `mega_viral` is 10x or more, `viral` 3 to 10x, `above_average` 1.5 to 3x, `average` under 1.5x. --- ## Trend monitoring setup Find today's trends, research one, then keep watching it with a recurring agent. **At a glance** - **Cost:** $1.25 to start ($0.25 trends, $0.50 one-time agent, $0.50 first recurring run), or $0.75 without the one-time agent. Then $0.50 per run. - **How long:** Seconds, plus one agent run (up to about 20 minutes). ### 1. Get today's trends ($0.25) Worldwide trends, unless you add `region=us` (or `gb`, `au`, `sg`). **Python:** ```python import requests API = 'https://api.virlo.ai/v1' headers = {'Authorization': 'Bearer YOUR_API_KEY'} digest = requests.get(f'{API}/trends/digest', headers=headers).json()['data'] for group in digest: print(group['local_date']) # the list's date. Don't use 'title', which can be a day off. for item in group['trends']: print(f" #{item['ranking']}: {item['trend']['name']}") ``` ### 2. Research one trend ($0.50) Get free keyword ideas and start a one-time agent, then read results as in the [Full niche analysis](https://dev.virlo.ai/docs/recipes#full-niche-analysis). **Python:** ```python trend_name = digest[0]['trends'][0]['trend']['name'] # or pick one from the list intent = f'Find what is working in short videos about {trend_name}' # Keyword ideas (free) suggestion = requests.post( f'{API}/agents/suggest-keywords', headers=headers, json={'intent': intent} ).json()['data'] # Before you pay for a run, check that suggestion['quality']['passes'] is True. # One-time agent ($0.50). Optional: skip it to start at $0.75, but keep the keyword ideas. agent = requests.post( f'{API}/agents', headers=headers, json={ 'is_recurring': False, 'intent': intent, 'keywords': suggestion['keywords'], 'exclude_keywords': suggestion['exclude_keywords'], # words that point to other topics } ).json()['data'] agent_id = agent['id'] ``` ### 3. Keep watching it ($0.50 per run) Create a new agent with `is_recurring: true` and a `cadence`: `daily`, `weekly`, `monthly`, or a custom schedule (cron format), at most once a day. Creating it is free, but its first run starts at once. Each run costs $0.50 when it finishes. A `partial_failure` run is charged; a `failed` run isn't. These charges aren't in any header, so [check your balance](https://dev.virlo.ai/docs/credits#check-your-balance). To stop them, pause ([Update agent](https://dev.virlo.ai/docs/agents#update-agent), `active: false`) or [delete](https://dev.virlo.ai/docs/agents#delete-agent) the agent. **Python:** ```python # Your balance must cover the first run ($0.50), or this returns a 402 error. monitor = requests.post( f'{API}/agents', headers=headers, json={ 'is_recurring': True, 'cadence': 'weekly', # same day and time each week; next_run_at says when 'name': f'Weekly watch: {trend_name}', 'intent': intent, 'keywords': suggestion['keywords'], 'exclude_keywords': suggestion['exclude_keywords'], } ).json()['data'] print(f"Agent ID: {monitor['id']}") print(f"Next scheduled run: {monitor['next_run_at']}") # the first run has already started ``` --- ## Hashtag research See which hashtags had the most-viewed videos posted in the last 30 days. **At a glance** - **Cost:** $0.05 per call: $0.15 for steps 1 to 3, or $0.25 adding YouTube and Instagram. Empty lists are billed; a hashtag with no videos (`404`) is free. - **How long:** A few seconds per call. Numbers count only videos Virlo has collected, so compare [hashtags](https://dev.virlo.ai/docs/hashtags) with each other, not with platform totals. ### 1. Top hashtags ($0.05) Top lists need both dates (`YYYY-MM-DD`, at most 90 days apart). Views are the current totals of videos posted in that range. **Python:** ```python import requests from datetime import date, timedelta headers = {'Authorization': 'Bearer YOUR_API_KEY'} end = date.today().isoformat() start = (date.today() - timedelta(days=30)).isoformat() hashtags = requests.get( 'https://api.virlo.ai/v1/hashtags', headers=headers, params={'start_date': start, 'end_date': end, 'limit': 20, 'order_by': 'views', 'sort': 'desc'} ).json()['data'] # Generic tags like #fyp lead. Hashtags come back lowercase, without the #. for tag in hashtags[:5]: print(f"#{tag['hashtag']}: {tag['count']:,} videos, {tag['total_views']:,} views") ``` ### 2. One hashtag's numbers ($0.05) Skip the dates for all-time numbers. **Python:** ```python perf = requests.get( 'https://api.virlo.ai/v1/hashtags/fyp/performance', # no # in the path headers=headers, params={'start_date': start, 'end_date': end} ).json()['data'] print(f"#fyp: {perf['video_count']:,} videos, {perf['total_views']:,} views, avg {perf['avg_views']:,.0f} views/video") ``` ### 3. Top hashtags on one platform ($0.05) **Python:** ```python # YouTube and Instagram: /v1/youtube/hashtags and /v1/instagram/hashtags tiktok = requests.get( 'https://api.virlo.ai/v1/tiktok/hashtags', headers=headers, params={'start_date': start, 'end_date': end, 'limit': 10, 'order_by': 'views', 'sort': 'desc'} ).json()['data'] for tag in tiktok[:3]: print(f"#{tag['hashtag']}: {tag['count']:,} videos, {tag['total_views']:,} views") # Example output (videos posted Aug 25 to Sep 23, 2026): # #fyp: 31,049 videos, 5,162,705,739 views # #viral: 9,971 videos, 1,693,054,907 views # #funny: 4,276 videos, 1,242,478,023 views ``` For a hashtag's videos, use a [hashtag lookup](https://dev.virlo.ai/docs/satellite/hashtags) ($0.50 to $2.50). --- ## Research in another language or country Research a niche in any language, then narrow results to one country. **At a glance** - **Cost:** $0.50, charged when you create the agent ($1.50 with Data Intelligence). Country filters are free. - **How long:** The same as a [Full niche analysis](https://dev.virlo.ai/docs/recipes#full-niche-analysis). 1. **Set `english_only: false`** when you create the agent, or it keeps only English. Write the name, intent, and keywords in your language; Virlo builds its search terms in that language. English may still appear (there's no language filter). 2. **`region`** when you read results: a two-letter code like `MX`, any case. `Mexico` returns no results, not an error. > **Note:** **Country data is in beta.** Items with no known country are left out of filtered results. TikTok slideshows (swipeable photo posts) almost always have one; Instagram videos rarely do. ### 1. Create an all-languages agent ($0.50) **cURL:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": false, "intent": "Investigar el contenido viral de fútbol en Latinoamérica", "name": "Fútbol LATAM", "keywords": ["goles de fútbol", "jugadas de fútbol", "resumen de partido"], "platforms": ["tiktok", "instagram"], "english_only": false }' ``` **Python:** ```python import requests, time resp = requests.post( 'https://api.virlo.ai/v1/agents', headers={'Authorization': 'Bearer YOUR_API_KEY'}, json={ 'is_recurring': False, 'intent': 'Investigar el contenido viral de fútbol en Latinoamérica', 'name': 'Fútbol LATAM', 'keywords': ['goles de fútbol', 'jugadas de fútbol', 'resumen de partido'], 'platforms': ['tiktok', 'instagram'], 'english_only': False, # keep every language }, ) agent_id = resp.json()['data']['id'] ``` ### 2. Wait, then filter by country (free) Videos carry the country as `upload_region`, slideshows as `region`; both are `null` when unknown. **cURL:** ```bash # Videos uploaded from Mexico curl -G https://api.virlo.ai/v1/agents/{agent_id}/videos \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=MX \ -d order_by=views \ -d sort=desc \ -d limit=50 # Slideshows uploaded from Argentina curl -G https://api.virlo.ai/v1/agents/{agent_id}/slideshows \ -H "Authorization: Bearer YOUR_API_KEY" \ -d region=AR \ -d limit=50 ``` **Python:** ```python # First wait until finalized, with the Full niche analysis polling code # (you can skip its last_run_at check, since this recipe doesn't read creators). headers = {'Authorization': 'Bearer YOUR_API_KEY'} mx_videos = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}/videos', headers=headers, params={'region': 'MX', 'order_by': 'views', 'sort': 'desc', 'limit': 50}, ).json()['data']['videos'] ar_slideshows = requests.get( f'https://api.virlo.ai/v1/agents/{agent_id}/slideshows', headers=headers, params={'region': 'AR', 'limit': 50}, ).json()['data']['slideshows'] for v in mx_videos: print(v['upload_region'], v['views'], v['url']) # videos: upload_region for s in ar_slideshows: print(s['region'], s['views'], s['url']) # slideshows: region ``` --- # API playground > Try every Virlo API endpoint and see real replies drawn as videos, creators, sounds and charts. No API key needed. Source: https://dev.virlo.ai/docs/playground See what the API returns before you write code. **At a glance** - **What it does:** Runs any endpoint and draws the reply: video grids, creator cards, sounds, hooks, audience splits and growth charts. - **You send:** Pick a question or an endpoint. The fields come filled in, so **Send request** works right away. - **You get back:** Replies recorded from the real API, the raw JSON, and the same request as cURL, JavaScript or Python. - **Cost:** Free here, no key needed. Each endpoint shows its live price, and the [Billing page](https://dev.virlo.ai/docs/credits#prices) lists them all. ## How it works - Start with a question, or pick any endpoint from the list. The list is read from the API's own spec, so it always matches what the API offers. - Results lead to the next call. From a video, ask why it popped or look up its creator. From a sound, open the videos using it. From an agent, read its videos, trends and hooks. - **Result** draws the reply, **JSON** shows it raw, and **Code** gives you the request to paste into your app. - Agents and deep-dive lookups run in the background. The playground shows how waiting works: it checks the status endpoint until the job is done. ## Try it The interactive playground runs in the browser: . Every endpoint it shows is documented on the other pages. ## Run it on your own data > **Note:** Sign in and open the [dashboard playground](https://dev.virlo.ai/dashboard/playground). It sends real requests with your team's API key, fills ids from your own agents and tracked creators, and shows what each call cost. Paid endpoints spend your balance. --- # MCP server > Connect Claude, Cursor, VS Code, Codex, Windsurf, or any other MCP app to Virlo. 55 tools for Content Research Agents, creator, video, sound and hashtag lookups, tracking, sounds, hooks, audience data, and trends. Source: https://dev.virlo.ai/docs/mcp Let your AI assistant use Virlo for you. Ask Claude, Cursor, or another assistant to research a niche or find trending sounds, and it fetches the data itself. **At a glance** - **What it does:** Gives your AI assistant 55 Virlo tools. MCP (Model Context Protocol) is the standard way AI apps use outside tools. - **You send:** Questions in plain words to your assistant, after a one-time setup. - **You get back:** Answers built on real Virlo data, such as this week's top TikTok hooks for food content, each with its source video. - **Cost:** Connecting and re-reading results are free. Quick data (trends, trending videos, hashtags, sounds, hooks) costs $0.05 to $0.25 a call. New research or a lookup starts at $0.50. - **How long:** Most tools answer in seconds. A Content Research Agent can take about 20 minutes; ask the assistant to check back. > **Note:** **Need an API key?** Not in **Claude Desktop, claude.ai, or Claude Code**. They sign you in to Virlo and create one for you. Other apps (**Cursor, VS Code, Windsurf, Codex**) need a key from your [dashboard](https://dev.virlo.ai/dashboard/api-keys), unless they offer to sign you in to Virlo. Keys start with `virlo_tkn_`. ## Quick setup Pick your app's tab. If it uses an API key, paste the key first so the setup code includes it. Cursor then gets a one-click install button. **Server URL:** `https://dev.virlo.ai/api/mcp/mcp`. Transport: streamable HTTP. Auth: `Authorization: Bearer YOUR_API_KEY`. **Cursor** (\~/.cursor/mcp.json): ```json { "mcpServers": { "virlo": { "url": "https://dev.virlo.ai/api/mcp/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` **VS Code** (.vscode/mcp.json): ```json { "servers": { "virlo": { "type": "http", "url": "https://dev.virlo.ai/api/mcp/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` **Claude Code:** ```bash claude mcp add --transport http virlo https://dev.virlo.ai/api/mcp/mcp --header "Authorization: Bearer YOUR_API_KEY" ``` ## Any other MCP app Any app that supports remote MCP servers can connect with: | Setting | Value | | - | - | | Server URL | `https://dev.virlo.ai/api/mcp/mcp` | | Transport | Streamable HTTP (some apps say "HTTP") | | Header | `Authorization: Bearer YOUR_API_KEY` | If your app supports MCP sign-in (OAuth), skip the header and sign in. If it only runs local servers, use the `mcp-remote` bridge on the Windsurf and Codex tabs. **Details for developers** - The server is stateless. Use `POST` (a `GET` returns `405`) and send `Accept: application/json, text/event-stream` (or get `406`). - `401` means a missing `Authorization` header or a key without the `virlo_tkn_` prefix. `403 Origin not allowed` means a browser app sent an unapproved `Origin`. Native apps should send none. - Tool errors come back as normal replies with the error text inside. - OAuth discovery: `https://dev.virlo.ai/.well-known/oauth-protected-resource` (dynamic client registration, PKCE `S256`). ## If something goes wrong - **Connected, but every tool says "Invalid or inactive API key".** Setup only checks the `virlo_tkn_` prefix. The real check comes on the first call. Copy the key again from your [dashboard](https://dev.virlo.ai/dashboard/api-keys). - **Paid tools fail with error 402, free ones work.** Your balance is too low. Add funds on the [Billing page](https://dev.virlo.ai/dashboard/billing). - **A filter does nothing.** Tools silently ignore settings they don't know, such as a misspelled name. Ask the assistant which settings it sent. ## Content Research Agents A **Content Research Agent** collects and analyzes short-form videos on a topic you describe. It runs once, or on a daily, weekly, or monthly **cadence**. Each collection is a **run**. For historical reasons, one-time agent tools say "keyword search" and recurring ones say "niche monitor". Both create [Content Research Agents](https://dev.virlo.ai/docs/agents). ### Getting good results from an agent 1. **Describe the goal in one sentence.** This **intent** is required. Say who it's for and what you want to learn, like "beginner skincare routines that name drugstore products." See the [Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook). 2. **Get free keywords** from `suggest_keywords`: usually 7 to 12 phrases, plus words to exclude. If it flags the intent as weak, sharpen it. 3. **Start the agent** with the same intent and both lists (up to 50 keywords, same price). 4. **Wait for `finalized: true`**, then read `data_type: "summary"` first. Reading results is free. Ask to filter by views, dates, platform, sort order, or country (beta). **Data Intelligence** (+$1.00 per run): AI tags collected videos with 79 details (70 for slideshows), such as hook, format, and tone. Turn it on when you start the agent (`data_intelligence_enabled: true`), or later with `update_niche_monitor`. It never applies to past runs. See [Data Intelligence](https://dev.virlo.ai/docs/intelligence). **More options** - **Other languages.** Set `english_only: false`, and write the keywords and intent in that language. - **Meta ads.** `enable_meta_ads: true` (`search_keywords`) or `meta_ads_enabled: true` (`create_niche_monitor`) adds Meta ads at no extra cost. Runs can then take longer, up to about 45 minutes. - **Partial results.** Status `partial_failure` is usable: one platform or keyword failed. ## Available tools Prices are per call. ### Trends and hashtags (instant) - `search_hashtags` ($0.05): top hashtags by uses or views. - `get_hashtag_performance` ($0.05): views, likes, and comments for one hashtag. - `get_trending_videos` ($0.25): the most-viewed videos from the last 48 hours. - `get_trends` ($0.25): trending topics with example videos, by date and region. - `get_trends_digest` ($0.25): today's curated trend list for a region. - `get_emerging_trends` ($0.25): early trends ranked by momentum. Trend regions are `global` (the default), `us`, `gb`, `au`, and `sg`. ### One-time agents - `suggest_keywords` (free): turns an intent into graded keywords and exclude words. Creates nothing. - `search_keywords` ($0.50, or $1.50 with Data Intelligence): starts a one-time agent. Needs `intent`. - `list_keyword_searches` (free): lists them. - `get_keyword_search_results` (free): reads results by `data_type`: `status` (the default), `summary`, `videos`, `slideshows`, `ads`, `outliers`, `analysis`, `trends`, or `sounds`. **Outliers** are creators whose videos get far more views than their follower count predicts. ### Recurring agents - `create_niche_monitor` ($0.50 per run, or $1.50 with Data Intelligence): runs `daily`, `weekly`, or `monthly`. Needs `intent`. The first run starts right away and is billed. - `list_niche_monitors` (free): lists them. - `get_niche_monitor_data` (free): reads results. Uses `overview` (the default) instead of `status`, and adds `hashtags`, `benchmarks`, `affinity` (related topics, beta), and `similar` (look-alike creators, beta). - `update_niche_monitor` (free): changes settings. `is_active: false` pauses the agent, and `true` resumes it. - `delete_niche_monitor` (free): deletes the agent for good and stops billing. It drops off your list, but past results stay readable by ID, so save it. To stop for a while, pause instead. - `set_niche_monitor_autonomy` (free): turns autopilot on or off with `autopilot: true` or `false`. - `get_niche_monitor_activity` (free): the change log. What the agent noticed, what autopilot changed, and why. - `get_niche_monitor_events` (free): breaking stories in the niche, scored 0 to 10. - `get_niche_monitor_proposals` and `review_niche_monitor_proposal` (free, deprecated): list and act on suggested changes. Autopilot now applies changes itself, so read the activity log instead. **How autopilot works** After each run, a recurring agent rewords its searches and adds keywords so it keeps finding new videos. Autopilot is on by default, with nothing to unlock. It never removes the keywords or exclude terms you set, never adds charges, and never changes the schedule. To turn it off, pass `autopilot: false` to `create_niche_monitor`, `update_niche_monitor`, or `set_niche_monitor_autonomy`. [More on autopilot](https://dev.virlo.ai/docs/agents/autopilot). ### Creator, video, sound, and hashtag lookups A **lookup** is a one-time deep dive on one creator, video, sound, or hashtag, saved as a run you can re-read for free. Tool names say `satellite`, the internal name for lookups. - `lookup_creator` ($0.50, up to $1.75): profile, recent videos, and top performers. Add-ons: `trend_analysis` (+$0.50), `data_intelligence` (+$0.25), and audience data (+$0.50). - `batch_lookup_creators` ($0.50 per creator, free if looked up in the last 6 hours with at least the same options): up to 25 creators at once. - `analyze_video` ($0.50): how far one video beat the creator's usual numbers. - `lookup_sound` ($0.50, or $1.00 with trend analysis): up to 100 videos using a TikTok or Instagram sound, or about 300 with trend analysis. Takes the sound's `music_id` (the number at the end of its link) or a Virlo sound ID. - `lookup_hashtag` ($0.50, up to $2.50 with [more videos and trend analysis](https://dev.virlo.ai/docs/satellite/hashtags)): videos and stats for a TikTok, Instagram, or YouTube hashtag. Repeats within 6 hours are free. - `get_satellite_run` (free): re-reads a past lookup by `run_id`. Each paid lookup has its own run, so older ones stay. - `list_satellite_runs` (free): past lookups. Check here before paying again. ### Tracking Tracking checks a creator or video on a schedule (daily by default, from every 6 hours to monthly) and writes a short AI report. - `track_creator` and `track_video` ($0.25 per check): start tracking. `collection_depth` also pulls a creator's older posts once, for $0.50 to $2.00. - `list_tracked_items` (free): what you track. - `get_tracking_report` (free): the latest AI report and snapshots. - `get_tracking_signals` (free): alerts such as outlier videos and follower spikes. - `list_creator_posts` (free): a tracked creator's posts and the sounds used. - `collect_creator_posts` ($0.50, $1.00, or $2.00): pulls up to 50, 200, or 500 more posts now. Refunded if it finds none. - `get_posting_cadence` (free): how often, and on which days, the creator posts. - `update_tracking_settings` (free): pauses, resumes, or changes how often it checks. Paused items stay listed and stop costing. - `untrack_creator` and `untrack_video` (free): stop tracking. Tracking a creator again restores the history. A video can't be tracked again. ### Audience Audience data describes who comments on a creator's videos: age, gender, language, and location. It covers creators you track. For others, add it to `lookup_creator`. - `refresh_creator_audience` ($0.50): makes a new snapshot, or returns one under 30 days old, or one still being made, for free. Refunded if data is too thin. - `get_creator_audience_demographics` (free): age, gender, and language. - `get_creator_audience_geography` (free): top countries and cities. ### Sounds - `get_trending_sounds` ($0.25): sounds used in the most new videos in the last 7 days. - `get_breakout_sounds` ($0.25): the fastest-growing sounds from a small start. - `search_sounds` ($0.10): title search. Spelling must match; case doesn't. - `get_sound_details` ($0.05): details and totals. `resolve: true` finds the original song for $0.10 more, the first time only. - `get_sound_videos` ($0.25): videos using a sound, by views or date. - `get_sound_usage_history` ($0.05): a daily snapshot of the sound's total use count and its daily growth. - `get_creator_sounds` ($0.25): sounds a creator owns, and how many others use them. `get_sound_details`, `get_sound_videos`, and `get_sound_usage_history` take Virlo's sound ID from `search_sounds` or `get_trending_sounds`. Agent-collected sounds are free with `data_type: "sounds"`. ### Hooks A **hook** is a video's opening line or shot. They're ranked by **Virality Score**: how far a video beat its creator's follower count. See [Hooks](https://dev.virlo.ai/docs/hooks). - `get_trending_hooks` ($0.25): the best opening lines right now, with source videos. - `search_hooks` ($0.25): finds hooks by phrase or type. - `get_hook_types` (free): which hook styles win most often. - `get_hook_library` ($0.10): fill-in-the-blank hook templates. - `get_agent_hooks` ($0.25, free with Data Intelligence or while the agent has no hooks): top hooks from your agent. Without Data Intelligence there are few or none, so use `get_trending_hooks`. ### Job status and balance - `check_job_status` (free): checks any background job. - `get_credit_balance` (free): your balance in dollars and credits (1 credit = $0.01), plus prices. ## Long-running jobs Agents, lookups, post collection, and audience snapshots run in the background. - `search_keywords`, `lookup_creator`, `analyze_video`, `lookup_sound`, and `lookup_hashtag` wait up to 25 seconds, checking every 2.5 seconds. If the job isn't done, you get `status: "processing"` and a job ID to check later with `check_job_status`, not in a loop. - `create_niche_monitor`, `batch_lookup_creators`, `collect_creator_posts`, and `refresh_creator_audience` return an ID right away. A job is done when `finalized: true`. `completed` can mean the AI report is still being written, so empty analysis fields mean "not yet". A video with `intelligence_status: "skipped"` is the exception: it was left out on purpose. Half of agent runs finish collecting in under 8 minutes, and 9 in 10 in under 20. The AI report follows within minutes. Lookups take under a minute to about 3 minutes. In a chat app, ask the assistant to check back in 10 to 20 minutes. Developers can use the `content_research_agent.run.completed` [webhook](https://dev.virlo.ai/docs/webhooks) instead. ## Guides your assistant can read The server has four read-only guides (MCP resources). Many apps don't read them unprompted, so start big research with "Read Virlo's agent playbook and intent cookbook first." - `virlo://docs/intent-cookbook`: how to write a good intent. - `virlo://docs/agent-playbook`: which tool fits which question, and how to avoid wasted spend. - `virlo://docs/api-overview`: the tool catalog. - `virlo://docs/credit-costs`: the price of each tool. The first two are also plain text for AI assistants ([`/intent-cookbook.txt`](https://dev.virlo.ai/intent-cookbook.txt), [`/agent-playbook.txt`](https://dev.virlo.ai/agent-playbook.txt)) and pages for people ([Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook), [Research playbook](https://dev.virlo.ai/docs/agent-playbook)). ## Workflow prompts These are built-in, multi-step research plans. Pick one from your app's menu or ask by name: "Use the full_niche_analysis prompt for viral cooking content." - **`full_niche_analysis`** (you give `niche`): drafts keywords with `suggest_keywords` (free), runs a one-time agent, looks up the top 2 or 3 creators, and sets up a weekly agent. About $2.00 to $2.50, then $0.50 a week. - **`creator_deep_dive`** (`platform`, `username`): looks up the creator and writes a profile, then asks before tracking them. $0.50, plus $0.25 per check if you track them. - **`trend_scout`** (optional `platform`: `youtube`, `tiktok`, `instagram`, or `all`): turns emerging trends, today's digest, viral videos, and hashtag momentum into a trend report. About $0.90 to $1.25. - **`genre_monitor`** (`genre`): drafts keywords with `suggest_keywords` (free), creates a weekly TikTok agent, and writes a genre brief. $0.50 a week until you stop it. - **`competitor_landscape`** (`subject`, a creator or niche; optional `platform`): runs an agent with Data Intelligence, shortlists 3 to 5 competitors, looks each up after you confirm, and writes a positioning brief. About $3.00 to $4.50. ## What it costs MCP tools cost the same as the Virlo API and draw from your prepaid API balance. It's separate from any Virlo app plan. Recurring agents and tracking bill every run until you pause or stop them. MCP replies don't show what a call cost. Run `get_credit_balance` before and after, or open [Usage](https://dev.virlo.ai/dashboard/usage). [Billing and pricing](https://dev.virlo.ai/docs/credits) --- # Connect an AI assistant > Connect Cursor, Claude, OpenClaw, and other AI assistants to Virlo with the MCP server, plain-text docs files, the ClawHub skill, or the OpenAPI spec. Source: https://dev.virlo.ai/docs/ai-agents Let Claude, Cursor, or another AI assistant use Virlo for you. **At a glance** - **What it does:** Gives the assistant tools or docs to use Virlo correctly. - **You send:** An API key from your [dashboard](https://dev.virlo.ai/dashboard/api-keys) and a funded balance. - **You get back:** An assistant that does research or writes working code. - **Cost:** Setup is free. Quick data costs $0.05 to $0.25; new research and lookups start at $0.50. Reopening results is free. API responses show each charge in the `X-Cost` header; in chat, check your balance. - **You choose:** The [MCP server](https://dev.virlo.ai/docs/ai-agents#mcp-configuration) for chat research, the [files](https://dev.virlo.ai/docs/ai-agents#llm-optimized-documentation) or [OpenAPI spec](https://dev.virlo.ai/docs/ai-agents#openapi-specification) for code, or the [OpenClaw skill](https://dev.virlo.ai/docs/ai-agents#openclaw-skill) in OpenClaw. > **Note:** **No code needed:** use the [MCP server](https://dev.virlo.ai/docs/mcp) inside Claude, or the [Virlo web app](https://app.virlo.ai), a separate product with its own plans. ## MCP server Add `https://dev.virlo.ai/api/mcp/mcp` as a connector in your AI app for 55 Virlo tools. Claude apps sign you in; others use your API key. [Setup per app](https://dev.virlo.ai/docs/mcp). ## Plain-text files Paste one into the chat for help writing code. They cover the API, not MCP. - [`/llms.txt`](https://dev.virlo.ai/llms.txt): a short overview and prices. - [`/llms-full.txt`](https://dev.virlo.ai/llms-full.txt): every endpoint, parameter, and response field. - [`/agent-playbook.txt`](https://dev.virlo.ai/agent-playbook.txt): which request answers which question, and how to judge results. Always include it. ## OpenClaw skill Install [Virlo's skill](https://clawhub.ai/virlo-ai/skills/short-form-market-research-brain) from ClawHub, OpenClaw's skill store. `@virlo-ai` marks the official copy. **cURL:** ```bash openclaw skills install @virlo-ai/short-form-market-research-brain ``` Add your key to `~/.openclaw/openclaw.json` as below, or the skill won't appear. ```json5 { skills: { entries: { "short-form-market-research-brain": { env: { VIRLO_API_KEY: "YOUR_API_KEY" } } } } } ``` ## OpenAPI spec For code generators and Postman. - **Spec file:** [`https://api.virlo.ai/openapi.json`](https://api.virlo.ai/openapi.json) - **Browse it:** [`https://api.virlo.ai/api`](https://api.virlo.ai/api). Ignore the internal `/admin` routes. ## Tips - **Ask for your balance first** (free: `GET /v1/account/balance`). - **Use a [Content Research Agent](https://dev.virlo.ai/docs/agents)** for "what's working in this niche?" Most runs finish within 20 minutes. Ask the assistant to check back, or have code wait for `finalized: true` ([how results load](https://dev.virlo.ai/docs/async-data)). --- # Research playbook > How people and AI assistants pick the right Virlo workflow for a research question and read the results correctly: costs, timings, virality scoring, platform benchmarks, AI fields, and audience data. Source: https://dev.virlo.ai/docs/agent-playbook How to pick the right Virlo workflow for a research question, in any niche, and how to read what comes back. **At a glance** - **What it does:** A field guide for researching short-form video, yourself or through an AI assistant. - **You get back:** What to run, how long it takes, how to rank viral videos, and how to read AI fields. - **Cost:** Reading this guide is free. Each workflow lists its price. > **Note:** Working through an AI assistant? Give it the plain-text guide [`/agent-playbook.txt`](https://dev.virlo.ai/agent-playbook.txt). If it's connected to Virlo by MCP, [Available tools](https://dev.virlo.ai/docs/mcp#available-tools) names the tool behind each workflow. ## Pick the right workflow Find your question. Swap `:id`-style parts for real values. An **agent** (Content Research Agent) collects TikTok, YouTube, and Instagram videos for your keywords. **Data Intelligence** (+$1.00 per run) adds an AI breakdown of the videos worth studying. A creator lookup's $0.25 AI read is separate and covers fewer fields. ### Research a niche | Question | Run this | Cost | | - | - | - | | "What's working in X right now?" | **One-time agent**: `POST /v1/agents` with `is_recurring: false` | $0.50 (+$1.00 with Data Intelligence) | | "Keep watching X for me." | **Recurring agent**: `POST /v1/agents` with `is_recurring: true` and a `cadence` | $0.50 per run, first run included; it starts right away (+$1.00 per run with Data Intelligence). | | "What's working in X in Spanish or Thai?" | **Agent, all languages**: `POST /v1/agents` with `english_only: false` | Same as any agent | | "Only videos uploaded from Canada." | **Country filter (beta)**: `GET /v1/agents/:id/videos?region=CA` | Free | | "What do these videos actually say?" | **Transcripts**: `GET /v1/agents/:id/videos?include_transcript=true` | Free | ### Look up or track one thing | Question | Run this | Cost | | - | - | - | | "Tell me about this creator." | **Creator lookup**: `GET /v1/satellite/creator/:platform/:username` | $0.50 (+$0.50 trend analysis, +$0.25 for the lookup's own AI read of each video) | | "Who watches this creator?" | **Audience**: `audience_demographics=true` on a creator lookup, or `POST /v1/tracking/creators/:id/audience-refresh` | $0.50 per new snapshot | | "Why did this video pop?" | **Video outlier check** (one video against the creator's usual numbers): `POST /v1/satellite/video-outlier` | $0.50 | | "What's happening with this sound?" | **Sound lookup** (TikTok, Instagram): `GET /v1/satellite/sounds/:platform/:music_id` | $0.50 to $1.00 | | "What's working under #X?" | **Hashtag lookup** (TikTok, Instagram, YouTube Shorts): `GET /v1/satellite/hashtags/:platform/:hashtag` | $0.50 to $2.50 | | "Track this creator or video." | **Tracking**: `POST /v1/tracking/creators` or `/v1/tracking/videos` | $0.25 per check, daily by default | ### Trends, sounds, and hooks | Question | Run this | Cost | | - | - | - | | "What's trending today?" | **Digests**: `GET /v1/trends/digest`, `/v1/videos/digest`, `/v1/hashtags` | $0.05 to $0.25 | | "What's starting to take off?" | **Emerging trends**: `GET /v1/trends/emerging` | $0.25 | | "What's trending in the US, UK, Australia, or Singapore?" | **Regional trends**: `GET /v1/trends?region=us` (or `gb`, `au`, `sg`) | $0.25 | | "Find me a sound." | **Sounds**: `GET /v1/sounds/trending`, `/breakout`, `/search` | $0.10 to $0.25 | | "What hooks work in X?" | **Hooks**: `GET /v1/agents/:id/hooks` for your agent's niche, or `GET /v1/hooks/trending` | $0.25 (free on an agent with Data Intelligence, or while it has no hooks yet) | | "Which kinds of hooks work best?" | **Hook types**: `GET /v1/hooks/types?sort=strong_hit_rate` | Free | | "Where is this hook from?" | **Hook search**: `GET /v1/hooks/search?q=...` | $0.25 | | "Write me hooks for X." | **Hook templates**: `GET /v1/hooks/library` | $0.10 (+$0.25 if you also call `/v1/hooks/trending`) | Most research follows one path: a one-time agent finds outlier creators (creators whose views far beat their follower count), a creator lookup checks the best ones, and tracking follows the winners. **Coming from Orbit or Comet?** `POST /v1/agents` replaces **Orbit** (now `is_recurring: false`) and **Comet** (now `is_recurring: true`). Both are deprecated and scheduled for removal. The announced date was August 3, 2026, and they still respond today, so migrate now. Old IDs work at `/v1/agents/:id`. See the [migration guide](https://dev.virlo.ai/docs/agents#migrating-from-orbit-comet). ## How long things take Agents and lookups run in the background. Start a job, check back at the pace in the table below until it's done (polling), then read the results. Or get a [webhook](https://dev.virlo.ai/docs/webhooks) when it finishes. More in [How results load](https://dev.virlo.ai/docs/async-data). - **Done means `finalized: true`.** `status: completed` alone can mean the AI report is still being written. Empty (`null`) AI fields mean "not yet," not "no data." The exception is a video with `intelligence_status: "skipped"`: its `intelligence` will never arrive. - **`partial_failure` is usable** (about 2% of runs). One platform or keyword failed, and the rest is there. Only `failed` (rare) means no data. - Each status reply says when to check next (`pending_jobs[].retry_after_seconds`). | Job | Typical | Plan for | Check every | | - | - | - | - | | Agent run | About 8 min (9 in 10 within 20 min) | 45 min | 15 to 60 s | | Creator lookup | 15 to 40 s | 2 min | 10 to 15 s | | Sound lookup | 1 to 2 min | 5 min | 10 to 15 s | | Hashtag lookup | 1 to 3 min | 5 min | 10 to 15 s | | Video outlier check | 20 to 60 s | 3 min | 10 s | Trend analysis makes sound and hashtag lookups a little slower. The job status check stops working after 24 hours. ## Setting up an agent **Keywords matter most.** Use specific phrases of two or more words ("jeep wrangler mods", not "jeep"). Send 7 to 12, the number `suggest-keywords` returns (50 at most); the price is the same. Cover synonyms of the _same_ idea: each keyword is its own search on each platform. **Write a real intent.** One required sentence on what you want to find. With Data Intelligence on, AI checks each video against it, and `intent_match=true` keeps only the matches when you read videos. See the [Intent cookbook](https://dev.virlo.ai/docs/intent-cookbook). **Exclude words that pull in the wrong topic**, like apple the fruit when you mean Apple the company. A few exclude words remove more junk than an extra keyword adds. - Each is matched as a whole word against captions and hashtags. `exclude_keywords_strict` also checks what's said in the video. - Use single words from the other meaning (`recipe`, `fruit`, `orchard`). Phrases like `apple the fruit` filter out almost nothing, and excluding your own topic (`apple`) removes everything. - The free keyword helper (`POST /v1/agents/suggest-keywords`) returns a ready `exclude_keywords` list. Pass it straight through. **Other settings:** - **Don't send `time_period` or `min_views`.** `POST /v1/agents` rejects them with a `400` error (MCP tools ignore them). Filter when you read instead, for free: the videos endpoint takes `min_views`, `platforms`, `start_date`, `end_date`, `region`, and `order_by`. - **`english_only`** is `true` by default. For non-English niches, set it to `false` and write the keywords and intent in that language, or you get almost nothing. - **`meta_ads_enabled: true`** also collects matching ads from Meta's ad library at no extra cost, but a run can take up to about 45 minutes. Use it when a brand or product is involved. - **`data_intelligence_enabled: true`** (+$1.00 per run) when you want to _understand_ content (hooks, formats, messaging), not just count views. You can't add it to a run afterward. ## Reading what you collected **Read everything you paid for.** Reading is free. A one-time run keeps about 360 videos (later recurring runs about 200), plus slideshows, ads, outlier creators, sounds, an AI report, and trends. Once it's finished, start with `GET /v1/agents/:id/summary`, then read the full lists. **Filter by upload country (beta).** Add `region` with a two-letter code (`US`, `CA`, any case) to the agent's `videos` or `slideshows` endpoint. A country name returns nothing, so convert it yourself. Coverage is partial: items with no known country drop out. **Creator lookups:** add `include=videos,outliers` and raise `max_videos` to 50 to 100 (the default is 20; sound and hashtag lookups already default to 50). The price stays $0.50. `cross_links=true` finds the creator's profiles on other platforms. **All lookups** (video outlier checks included) are saved. Re-read one free at `GET /v1/satellite/runs/:run_id`; the `run_id` is the job ID. Each paid lookup is saved as a new run with its own `run_id`, so a later lookup of the same creator, sound, or hashtag never overwrites an earlier one. **Refunds.** Virlo refunds a failed creator, sound, or hashtag lookup in full, for example a handle that doesn't exist, or a lookup that stalls and is closed out as failed. The trend analysis surcharge comes back when no trends come out, the audience surcharge when no new snapshot was needed, and the Data Intelligence surcharge when the lookup had no posts to enrich or its analysis failed. The result or failed status then shows `credits_refunded`, in credits, and so does the saved run at `GET /v1/satellite/runs/:run_id`, even for a failed lookup. The hashtag depth surcharge comes back only when the whole lookup fails. ## How recurring agents improve themselves After each run, a recurring agent tunes its own keywords so it keeps finding new videos. This is **autopilot**. It is on by default for every new agent, whether you make it through the API, over MCP, or in the Virlo app. - **What it does:** rewords searches, adds keywords (up to 15 in all), adds short-lived keywords for breaking stories in the niche, and collects more widely after a thin run. Keyword changes must pass a quality check first. - **What it never does:** remove a keyword or exclude term you set (returned as `pinned_keywords`), add charges, change the schedule, or pause the agent. Price per run is unchanged. - **What changed and why:** `GET /v1/agents/:id/activity` (free) is the change log. - **Breaking stories:** `GET /v1/agents/:id/events` (free). Each has a title, summary, importance score (0 to 10), and the keywords added. - **Turn it off:** send `autopilot: false` when you create or update the agent, or `PUT /v1/agents/:id/autonomy` with `{ "autopilot": false }`. The agent then keeps its setup exactly as you set it. `cognition_enabled: false` stops self-review entirely, and the agent keeps collecting. - **Proposals are deprecated.** `GET /v1/agents/:id/proposals` and its apply, dismiss, and revert calls still work for now. Read the activity log instead. One-time agents don't change themselves. See [Autopilot](https://dev.virlo.ai/docs/agents/autopilot#autonomy). ## Spotting the most viral Raw views mislead. 500,000 views is routine for an account with 10 million followers and huge for one with 3,000. So rank by the **Virality Score** (`weighted_score`), as Virlo does for outlier creators and hooks. It rewards beating the follower count by a wide margin, weighted by audience size so one lucky video from a tiny account doesn't top every list. That 3,000-follower video (167x its followers) scores 41: exceptional. 3,000,000 views on a 1,000,000-follower account (3x) scores 15: promising. Below 1x there's no score (the hooks endpoints show a negative one). | Virality Score | Read it as | | - | - | | 35 or more | Exceptional: study it frame by frame | | 25 to 35 | Very strong | | 18 to 25 | Strong | | 10 to 18 | Promising | | Below 10 | Emerging or routine | **How the score is calculated** ``` ratio = views / followers (only when followers > 0 and ratio > 1) weighted_score = ln(ratio) × ln(followers) exceptional: weighted_score ≥ 35 ``` `ln` is the natural logarithm (`Math.log` in JavaScript, `math.log` in Python). To find the real winners: 1. Rank all videos by this score. The videos list can't sort by it, so compute it from `views` and `author.followers`. 2. Check the plain multiplier: 20 times the follower count is notable, 100 times exceptional, 1,000 times a mega-outlier. 3. Compute `engagement_rate = (likes + comments + shares) / views`. Above 5% on a big video means people really reacted. Below 1% means passive spread (a popular sound, reposts, or paid promotion). 4. Compare with the AI's picks (`analysis_data.top_10_breakdown`), which also weigh quality and topic fit. Videos on both lists are the real winners. 5. For whole accounts, sort **creator outliers** (`GET /v1/agents/:id/creators/outliers`) by `weighted_score` (`ln(outlier_ratio) × ln(follower_count)`), not the raw ratio. They're the best source of rising talent and partnership leads. 6. Check `publish_date`: a high score this week is a live trend; from months ago, it's history. ## Platform benchmarks Never compare raw views across platforms. | Platform | Median views | Top 10% start at | Top 1% start at | Videos with a transcript | | - | - | - | - | - | | TikTok | \~39K | \~1M | \~6.6M | \~63% | | Instagram Reels | \~3.8K | \~175K | \~3M | Almost none, \~40% with Data Intelligence | | YouTube Shorts | \~1K | \~32K | \~2.5M | \~38% | A 100K-view Reel is a bigger deal than a 100K-view TikTok. Spoken insights (hooks, messaging) come mostly from TikTok and YouTube. Instagram publishes no transcripts, so a Reel only has one when Data Intelligence transcribed its audio. Without it, lean on descriptions, hashtags, and the AI's visual analysis. To read the words themselves, add `include_transcript=true` to [Get videos](https://dev.virlo.ai/docs/agents#agent-video-transcripts). TikTok and YouTube transcripts are mostly plain text (`source: "platform"`). Timestamps always come with the ones Virlo transcribed (`source: "transcribed"`), which includes every Instagram transcript, and with a small share of platform ones, so check `segments`. ## Reading Data Intelligence fields With Data Intelligence on, each analyzed video gets AI-written fields ([full list](https://dev.virlo.ai/docs/intelligence)). Off-topic, year-old and underperforming videos are skipped. The ones that matter most: - **Hooks.** `hook_text` is the exact opening line. `hook_type` is its style (`question`, `bold_claim`, `pov_setup`, and more). Use the [hooks endpoints](https://dev.virlo.ai/docs/hooks), or group your top videos by `hook_type` and quote real `hook_text` lines as templates. - **`summary`** is on every analyzed item. Reading 100 summaries is the fastest way to "watch" 100 videos. - **`content_format`** includes free-text values, so group close ones (`skit`, `comedy_skit`, `skit_sketch`). - **Commercial signals** (`is_sponsored`, `brands_mentioned[]`, `cta_usages[]`) show paid posts, organic brand mentions, and the niche's calls to action. - **Trust checks.** Use an item's fields only when `intelligence_status` is `"ready"`. `"skipped"` items were left out on purpose and never get fields. Discount anything in `low_confidence_fields[]`. `transcript_word_count: 0` with visual fields filled in means a silent video, not missing data. **Find the winners' pattern.** Count each `hook_type`, `content_format`, and `emotional_tone` in the top 25% of videos by Virality Score, then in the bottom 25%. The differences are the niche's real playbook. ## Slideshows TikTok image carousels (slideshows) come back separately from `/slideshows`, with their own AI fields. In product, education, and lifestyle niches they often beat video. - `panel_texts[]` and `panel_text_full` hold every slide's text in order. That's usually the whole content, so read it like a script you can adapt. - `narrative_arc` is how the slides progress. `text_density` says whether text or images dominate. - The hook is slide 1. Read `hook_text` as you would for a video. - Video-only fields (`visual_format`, `camera_perspective`, captions, transcript) are absent by design. ## Reading the AI report and trends `GET /v1/agents/:id/analysis/latest` returns the AI report in `analysis_data`: - `key_highlight` is the top finding, in one paragraph. `overview` holds the counts (`total_videos` analyzed, `avg_virality`). - `themes[]` are groups of what works, each with `why_it_works`, `tactics[]`, a `confidence` from 0 to 1, and `evidence_video_ids[]`. Trust 0.7 and up, check 0.5 to 0.7 against their videos, and call anything lower tentative. Match evidence IDs to the video list so every claim points to real videos. - `timing_analysis` shows posting-time patterns, `excluded_videos` what the AI judged off topic, and `connecting_thread` the pattern tying the themes together. `GET /v1/agents/:id/trends/latest` returns trends, each with a `status`: `new` (just appeared, the biggest opportunity), `rising` (growing, act now), `steady` (holding), or `fading` (skip it). On recurring agents, `stable_key` follows one trend across runs, and the `prev_*` fields show the change since the last run. A trend moving from `new` to `rising` with a growing `video_count` is the strongest "post about this now" signal. ## Trusting audience data Audience snapshots are built mainly from the people who **comment** on a creator's videos, not from the follower count. Before you share the numbers: - **`confidence_level`:** `high` or `medium` is usable. For `low`, say "limited signal" and don't present the percentages as fact. - **`data_source`,** strongest first: `comments`, `comments_extended`, `mixed`, `followers` (TikTok only, never above medium confidence), then `profile_only`. `profile_only` means no real signal and is refunded automatically. Treat it as "no data." - **Cost:** a snapshot is reused free for 30 days (`freshness_days`). Only a new one costs $0.50. Asking again while one is being built returns that job for free. ## Spending wisely - Starting work costs money, and so do instant data requests (trends, hashtags, sounds, hooks). Re-reading is free, so first check what your team already has: `GET /v1/agents`, `/v1/satellite/runs`, `/v1/tracking/creators`. - One well-scoped agent beats three vague ones. Put the effort into keywords, not retries. - Add up the cost before a plan with several paid steps, and check your balance first (`GET /v1/account/balance`, free). Top up if it's under $10. The `X-Balance-Remaining` header only appears on paid responses. - Vetting many creators? `POST /v1/satellite/creators/batch` takes up to 25, at $0.50 for each creator it looks up. A creator looked up in the last 6 hours with at least the same options comes back from cache for free. - `X-Cost` always shows the full charge. A refund for a failed lookup or an empty post collection arrives after the job ends, as its own row in your usage history. --- # Orbit (deprecated) > Deprecated. Orbit is the old address for a one-time Content Research Agent. How to move from /v1/orbit to /v1/agents. Source: https://dev.virlo.ai/docs/orbit Orbit is the old address for one-time [Content Research Agents](https://dev.virlo.ai/docs/agents). Use `/v1/agents`. Your Orbit IDs work there. **At a glance** - **What it does:** Starts a one-time agent. Past its August 3, 2026 removal date, so it can stop anytime. - **Cost:** $0.50 per run, or $1.50 with [Data Intelligence](https://dev.virlo.ai/docs/intelligence) (AI breakdowns of the videos), charged up front on either address. Results are free, except [agent hooks](https://dev.virlo.ai/docs/agents#get-agent-hooks) ($0.25 without Data Intelligence). - **Limits:** On Pro, Orbit allows 2 new runs a minute (50 a day, errors count) and 50 a minute for reading lists, agent details, videos, ads or outliers. `/v1/agents` has only the [daily limit](https://dev.virlo.ai/docs/rate-limits). ## Old address to new address | Old | New | | - | - | | `POST /v1/orbit` | `POST /v1/agents` | | `GET /v1/orbit` or `/v1/orbit/list` | `GET /v1/agents?is_recurring=false` | | `GET /v1/orbit/:id/...` | `GET /v1/agents/:id/...` | ## Change what you send `/v1/agents` rejects unknown fields (error `400`), so: - Add the required `is_recurring: false` and [`intent`](https://dev.virlo.ai/docs/agents#writing-a-good-intent) (one sentence on your goal). - Rename `enable_meta_ads` to `meta_ads_enabled`. - `keywords`: up to 50 (was 10). `name` is optional. - Remove `min_views`, `time_period` and `run_analysis` (Orbit ignored them). Filter views and dates on [`/videos`](https://dev.virlo.ai/docs/agents#get-agent-videos). **POST /v1/agents:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": false, "intent": "Find viral home espresso videos to plan content for a coffee gear brand", "name": "Home espresso", "keywords": ["home espresso", "espresso machine"], "platforms": ["tiktok", "youtube"], "meta_ads_enabled": true }' ``` **Filter when you read:** ```bash curl "https://api.virlo.ai/v1/agents/{agent_id}/videos?min_views=10000&start_date=2026-09-01T00:00:00Z&end_date=2026-09-30T23:59:59Z" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ## Update how you read results - `data.orbit_id` is now `data.id`. - List: `orbits`, `total` become `agents`, `page`, `count` (this page only). `createdAt` becomes `created_at`; status and video count moved to `GET /v1/agents/:id`. - `GET /v1/agents/:id` has a `latest_run` summary instead of `results` and `intent_summary`. Get videos from `/videos`. - `orbit_id`, `orbit_name` and `hasMore` become `agent_id`, `agent_name` and `has_more`. - Done means `finalized: true`. If `latest_run.status` is `partial_failure`, one platform or keyword failed but the rest is usable (Orbit reported `completed`). ## Webhooks Each run sends `orbit.run.completed` and `content_research_agent.run.completed`. Subscribe to both (an [update](https://dev.virlo.ai/docs/webhooks#update-webhook) replaces the whole `enabled_events` list), then drop the old one. The new event has the same `orbit` object, plus `is_recurring` (failed runs use `content_research_agent` instead). --- # Comet (deprecated) > Deprecated. Comet is the old name for a recurring Content Research Agent. How to move from /v1/comet to /v1/agents. Source: https://dev.virlo.ai/docs/comet Comet is the old name for a recurring [Content Research Agent](https://dev.virlo.ai/docs/agents). `/v1/comet` still works but is past its August 3, 2026 removal date and may stop any time. **At a glance** - **What it does:** Runs a Content Research Agent on a schedule. - **You send:** `POST /v1/agents` with `is_recurring: true`, a schedule (`cadence`) and a goal (`intent`). - **You get back:** The same results at `/v1/agents/:id/...`, usually within 20 minutes of each run. - **Cost:** $0.50 per run ($1.50 with [Data Intelligence](https://dev.virlo.ai/docs/intelligence)) until you pause or delete it. ## Move to agents Your comets keep running and already live at `/v1/agents/:id`, same ID. Swap `/v1/comet` for `/v1/agents` everywhere, except: - `POST /v1/comet` becomes `POST /v1/agents` with `is_recurring: true`. - `GET /v1/comet` or `/v1/comet/list` becomes `GET /v1/agents?is_recurring=true`. `include_inactive=true` adds paused ones. New recurring agents start with [autopilot](https://dev.virlo.ai/docs/agents/autopilot#autonomy) on. After each run it rewords searches and adds keywords, and it never removes the ones you set. To turn it off, send `autopilot: false` to `PUT /v1/agents/:id`. Read an agent's `autopilot` field from `/v1/agents`; `/v1/comet` doesn't show it. ## Change your request - Add `is_recurring: true` and `intent`, a sentence on what you want to find. - Drop `min_views`, `time_range` (Comet ignored both) and `is_active`, or you get `400`. Pause with `PUT /v1/agents/:id` and `active: false`. Filter [videos](https://dev.virlo.ai/docs/agents#get-agent-videos) by `min_views`, `start_date` and `end_date` instead. - Up to 50 `keywords` (Comet: 20). Optional: `name`, and `platforms` (default: `youtube`, `tiktok`, `instagram`). - `cadence` (unchanged): `daily`, `weekly`, `monthly`, or a cron schedule like `0 9 * * 1` (Mondays 09:00 UTC). More often than daily returns `400`. **Before: POST /v1/comet:** ```bash curl -X POST https://api.virlo.ai/v1/comet \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Cold brew watch", "keywords": ["cold brew concentrate", "#coldbrew"], "platforms": ["tiktok", "youtube"], "cadence": "weekly", "min_views": 10000, "time_range": "this_week" }' ``` **After: POST /v1/agents:** ```bash curl -X POST https://api.virlo.ai/v1/agents \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "is_recurring": true, "cadence": "weekly", "intent": "Track viral cold brew videos to plan content for a coffee brand", "name": "Cold brew watch", "keywords": ["cold brew concentrate", "#coldbrew"], "platforms": ["tiktok", "youtube"] }' ``` ## Renamed fields - The list is paged (`data.agents`), not one `data.configurations` array. - `is_active` is now `active`. `min_views`, `time_range` and `intent_summary` are gone. - `custom_niche_id` (videos, slideshows, ads) and `comet_id` (outliers) are now `agent_id`, and their `_name` fields are now `agent_name`. ## Webhooks Runs send both `comet.run.completed` and `content_research_agent.run.completed`. Subscribe your [webhook](https://dev.virlo.ai/docs/webhooks#update-webhook) to the new one, then drop the old. The new event keeps agent details in `comet` and adds `data.is_recurring`. Failed runs put them in `content_research_agent`. ## Cost - Creating costs $0 (`X-Cost: 0.00`), but the first run starts at once, billed when done. Failed runs are free. - Creating fails with `402` unless your balance covers one run.