Content Research Agents
A Content Research Agent (CRA) is the unified, forward-facing way to run research on Virlo. One resource, /v1/agents, replaces the split Orbit (one-shot keyword search) and Comet (recurring niche monitor) endpoints — a single is_recurring flag decides the mode. One-shot agents cost $0.50 at creation; recurring agents are free to create and bill $0.50 per scheduled run (including the first); all reads are free.
Collection scope is system-managed. You no longer choose a min_views floor or a time_range window at creation — agents collect the widest relevant net (cadence-aware) so nothing is missed. You filter at read time on the videos endpoint (min_views, start_date, end_date, platforms, order_by), for free, as many ways as you like without re-running the job.
All /v1/agents endpoints use snake_case for parameters and response fields, and wrap responses in a { "data": { ... } } envelope. Poll the agent resource and rely on finalized: true (not wall-clock time) to know a run is fully settled.
Migrating from Orbit / Comet
Orbit and Comet remain available during a migration window and will be removed on August 3, 2026. Migrating is mostly a rename — the data shapes are the same.
Endpoint mapping
| Legacy | Unified |
|---|---|
POST /v1/orbit | POST /v1/agents with is_recurring: false |
POST /v1/comet | POST /v1/agents with is_recurring: true |
GET /v1/orbit/:id · GET /v1/comet/:id | GET /v1/agents/:id |
GET /v1/{orbit,comet}/:id/videos | GET /v1/agents/:id/videos |
GET /v1/{orbit,comet}/:id/slideshows | GET /v1/agents/:id/slideshows |
GET /v1/{orbit,comet}/:id/ads | GET /v1/agents/:id/ads |
GET /v1/{orbit,comet}/:id/creators/outliers | GET /v1/agents/:id/creators/outliers |
GET /v1/{orbit,comet}/:id/analysis[/latest] | GET /v1/agents/:id/analysis[/latest] |
GET /v1/{orbit,comet}/:id/trends[/latest] | GET /v1/agents/:id/trends[/latest] |
GET /v1/{orbit,comet}/:id/sounds | GET /v1/agents/:id/sounds |
GET /v1/comet/:id/hashtags | GET /v1/agents/:id/hashtags |
GET /v1/comet/:id/benchmarks | GET /v1/agents/:id/benchmarks |
GET /v1/comet/:id/affinity | GET /v1/agents/:id/affinity |
GET /v1/comet/:id/creators/:cid/similar | GET /v1/agents/:id/creators/:cid/similar |
PUT /v1/comet/:id | PUT /v1/agents/:id |
DELETE /v1/comet/:id | DELETE /v1/agents/:id |
Every read is a same-path rename (/v1/orbit/:id/… or /v1/comet/:id/… → /v1/agents/:id/…) and the IDs are identical, so you can migrate one call site at a time.
What changed
min_views/time_range/time_periodare removed from creation (collection is system-managed). Move any view/date filtering to thevideosendpoint query params.intentis required on agents — it drives keyword quality, intent-match filtering, and the agent's self-optimization.- Webhook: subscribe to
content_research_agent.run.completed(carriesis_recurring) instead oforbit.run.completed/comet.run.completed. The legacy events keep firing until removal. - New: an autonomy surface (activity log + change proposals) — see below.
During migration the legacy endpoints still accept min_views/time_range/time_period, but they are ignored (accepted for backward compatibility only). /v1/agents itself rejects these fields with 400 validation_error. Filtering must move to read time either way.
Suggest keywords
Turns an intent into a quality-graded keyword set you can pass straight to Create agent. Free, synchronous, and creates nothing — call it as many times as you like until the quality score looks right, then spend the $0.50 on an agent that starts with good keywords.
Keyword quality is the single biggest driver of how useful an agent's results are, so this is the recommended first call for any new agent.
- Name
intent- Type
- string
- Required
- *
- Description
What the agent should research, in one sentence — the same string you'll pass to
POST /v1/agents. Max 500 characters.
- Name
topic_hint- Type
- string
- Description
Short topic or agent-name hint to steer generation. Example:
"Protein Recipes".
- Name
platforms- Type
- string[]
- Description
Platforms the agent will collect from — phrasing differs by platform, so passing this sharpens the output.
- Name
existing_keywords- Type
- string[]
- Description
The agent's current keywords. Pass these for
refreshandopportunitymodes so the engine avoids repeating what you already have.
- Name
mode- Type
- string
- Description
create(default) — build a fresh set from the intent.refresh— replace stale keywords on an existing agent.opportunity— find under-covered angles adjacent to the current set.
- Name
desired_count- Type
- integer
- Description
How many keywords to aim for. The engine clamps this to the data-backed 7–12 sweet spot: beyond ~15 keywords the off-target ratio climbs sharply, and single bare generic words cause 50–60% intent-filter loss.
- Name
use_web_grounding- Type
- boolean
- Description
Ground generation in current web results so timely phrasing is picked up. Slower — leave off for evergreen niches. Default
false.
Reading the quality grade
quality.score is 0–100; quality.passes is true when the score clears the threshold and there are no critical issues. If it's false, sharpen your intent and call again — it costs nothing.
Each entry in quality.issues has a code, a severity (critical / warning / info), a human message, and often the offenders that triggered it. Codes: too_few_keywords, too_many_keywords, single_word_keywords, overly_long_keywords, duplicate_keywords, low_cluster_cohesion, empty_set.
quality.stats.core_token_coverage is the fraction (0–1) of keywords sharing the dominant core token in core_token. Low coverage means the set is semantically scattered — tight clusters around one core entity collect far less off-target content.
Request
curl -X POST https://api.virlo.ai/v1/agents/suggest-keywords \
-H "Authorization: Bearer {token}" \
-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
{
"data": {
"keywords": [
"high protein recipe",
"protein meal prep",
"protein snack ideas",
"high protein dessert",
"protein overnight oats"
],
"exclude_keywords": ["protein powder review", "supplement haul"],
"reasoning": "Focused on preparation-style phrasing rather than product terms, which skew toward supplement marketing rather than recipe content.",
"quality": {
"score": 86,
"passes": true,
"issues": [
{
"code": "single_word_keywords",
"severity": "warning",
"message": "Single bare words collect a high share of off-topic content.",
"offenders": ["protein"]
}
],
"stats": {
"count": 5,
"avg_words_per_keyword": 3.2,
"single_word_count": 0,
"long_keyword_count": 0,
"duplicate_count": 0,
"core_token_coverage": 1.0,
"core_token": "protein"
}
},
"timely_context_used": false
}
}
Create agent
Creates a one-shot (is_recurring: false) or recurring (is_recurring: true) agent and dispatches its first run immediately.
- Name
is_recurring- Type
- boolean
- Required
- *
- Description
false= one-shot (Orbit-equivalent).true= recurring monitor (Comet-equivalent).
- Name
intent- Type
- string
- Required
- *
- Description
What you want this agent to research, in one sentence. Drives keyword quality,
intent_matchfiltering, and the agent's self-optimization. Example:"Track viral protein-recipe content for a fitness brand".
- Name
keywords- Type
- string[]
- Required
- *
- Description
1–50 keywords. 3–7 specific multi-word phrases covering synonyms of the same concept work best. Hashtag tokens are normalized (
#melodichouse==melodic house).
- Name
platforms- Type
- string[]
- Description
youtube,tiktok,instagram. Defaults to all three.meta_adsis also accepted here, but prefer themeta_ads_enabledflag below — it's the supported way to turn ad collection on and keepsplatformsmeaning organic content.
- Name
cadence- Type
- string
- Description
Required when
is_recurring: true."daily","weekly","monthly", or a cron expression (must run at most once per day — sub-daily crons are rejected). Rejected for one-shot agents.
- Name
name- Type
- string
- Description
Human-friendly name. Defaults to a generated label.
- Name
exclude_keywords- Type
- string[]
- Description
Keywords to filter out of results.
- Name
exclude_keywords_strict- Type
- boolean
- Description
Also match
exclude_keywordsagainst transcripts. Defaultfalse.
- Name
meta_ads_enabled- Type
- boolean
- Description
Also collect Meta ads. Default
false.
- Name
data_intelligence_enabled- Type
- boolean
- Description
Enable per-video AI intelligence (43 structured fields). Adds $1.00 to the base cost. Cannot be applied retroactively. Default
false.
- Name
english_only- Type
- boolean
- Description
Restrict collection to English-language content. Default
true. Setfalseto collect content in every language — the way to research non-English or global niches. Write yourintentandkeywordsin the target language when you opt out: the keyword engine adapts to the language of your input. On a recurring agent this applies to future runs; it never re-filters already-collected content.
There is no min_views or time_range here — collection scope is system-managed. Filter at read time on GET /v1/agents/:id/videos. Sending them anyway returns 400 validation_error (property min_views should not exist) — /v1/agents rejects unknown fields rather than ignoring them. Only the legacy /v1/orbit and /v1/comet endpoints still accept-and-ignore them during the migration window.
Going global. Leave english_only unset (or true) for English-only research — the default that suits most US/UK/AU audiences. To open the gates to other languages, set english_only: false and phrase your intent + keywords in that language (e.g. Spanish keywords for Spanish TikTok). This pairs naturally with the region filter at read time to slice results by upload country.
Cadence shortcuts ("daily" / "weekly" / "monthly") work exactly like a cron expression — both are valid. A raw cron must run at most once per day; sub-daily crons are rejected.
Team-level autopilot unlock. New agents default to autonomy_level: "suggest" and autopilot_unlocked: false. But once anyone on your team has unlocked autopilot (by manually applying one proposal), newly created agents come back with autonomy_level: "autopilot" and autopilot_unlocked: true by default — this is expected, not a bug. See Autonomy.
Request
curl -X POST https://api.virlo.ai/v1/agents \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"is_recurring": true,
"intent": "Track viral protein-recipe content for a fitness brand",
"keywords": ["high protein recipe", "protein meal prep", "protein snack ideas"],
"platforms": ["youtube", "tiktok", "instagram"],
"cadence": "weekly",
"name": "Protein Recipes",
"meta_ads_enabled": true
}'
Response
{
"data": {
"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": [],
"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",
"autonomy_level": "suggest",
"autopilot_unlocked": false,
"cognition_enabled": true,
"cadence": "weekly",
"next_run_at": "2026-07-10T14:00:00Z",
"last_run_at": null,
"is_processing": true,
"created_at": "2026-07-03T14:00:00Z",
"updated_at": "2026-07-03T14:00:00Z",
"latest_run": {
"id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90",
"status": "processing"
},
"job_id": "job_7f2a9c1e4b6d"
},
"message": "Agent created"
}
List agents
Lists your agents.
- Name
is_recurring- Type
- boolean
- Description
Filter to recurring (
true) or one-shot (false) agents.
- Name
include_inactive- Type
- boolean
- Description
Include deactivated agents. Default
false.
- Name
page- Type
- integer
- Description
1-indexed page. Default
1.
- Name
limit- Type
- integer
- Description
Results per page (max 100). Default
50.
Each row returns the agent's full config — keywords, platforms, intent, intent_keywords, exclude_keywords, source, team_id, autonomy fields, and timestamps — not just the summary fields shown in the trimmed example. No per-agent follow-up fetch is needed to read config.
Request
curl -G https://api.virlo.ai/v1/agents \
-H "Authorization: Bearer {token}" \
-d is_recurring=true \
-d limit=50
Response
{
"data": {
"limit": 50,
"page": 1,
"count": 2,
"agents": [
{
"id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"name": "Protein Recipes",
"is_recurring": true,
"active": true,
"cadence": "weekly",
"next_run_at": "2026-07-10T14:00:00Z",
"last_run_at": "2026-07-03T14:18:00Z",
"is_processing": false
},
{
"id": "b2e4d9f3-1c8e-4b02-8d3f-3a6e7c9f2b55",
"name": "Jeep Wrangler Mods",
"is_recurring": false,
"active": true,
"cadence": null,
"next_run_at": null,
"last_run_at": "2026-07-02T09:31:00Z",
"is_processing": false
}
]
}
}
Get agent
Returns the agent config, autonomy state, latest run, and async status. finalized: true means the latest run (scrape + AI analysis) is fully settled; while false, inspect pending_jobs[].
Alongside the config you sent, the response includes:
- Name
intent_keywords- Type
- string[]
- Description
The expanded keyword set the agent actually searched, derived from your
intent+keywords. Null until the first run resolves it. Read this to see how the agent interpreted your intent — if the expansion drifted, sharpenintentrather than adding keywords.
- Name
latest_run- Type
- object
- Description
The most recent run, same shape as Get run.
- Name
pending_jobs- Type
- object[]
- Description
Secondary work still in flight, each with a
typeandretry_after_secondsto use as your poll interval. Empty whenfinalizedistrue.
- Name
autonomy_level / autopilot_unlocked / cognition_enabled- Type
- —
- Description
Self-optimization state. See Autonomy.
Collection scope (min_views, time_range) is system-managed and not returned — there is nothing for you to read or set. Filter at read time on GET /v1/agents/:id/videos instead.
Request
curl https://api.virlo.ai/v1/agents/{agent_id} \
-H "Authorization: Bearer {token}"
Response
{
"data": {
"id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"name": "Protein Recipes",
"is_recurring": true,
"active": true,
"cadence": "weekly",
"next_run_at": "2026-07-10T14:00:00Z",
"last_run_at": "2026-07-03T14:18:00Z",
"is_processing": false,
"finalized": true,
"latest_run": {
"id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90",
"status": "completed",
"videos_linked": 283,
"outliers_identified": 12,
"started_at": "2026-07-03T14:00:00Z",
"completed_at": "2026-07-03T14:18:00Z"
},
"pending_jobs": [],
"analysis": "Protein desserts and no-cook overnight recipes are driving the most outsized reach this week...",
"analysis_data": { "key_highlight": "...", "themes": [] },
"analysis_batch_start": "2026-06-26T14:00:00Z",
"analysis_batch_end": "2026-07-03T14:00:00Z",
"autonomy_level": "suggest",
"autopilot_unlocked": false,
"cognition_enabled": true
}
}
While a run is still settling, finalized is false and pending_jobs[] lists what's in flight (each with a type and retry_after_seconds you can use as your poll interval). status: "completed" on latest_run alone is not the done signal — wait for finalized: true. partial_failure is a usable terminal state.
Update agent
Updates mutable config. Only fields you pass change. Collection scope (min_views/time_range) is system-managed and cannot be set here.
- Name
name- Type
- string
- Description
- New name.
- Name
active- Type
- boolean
- Description
- Activate or pause the agent.
- Name
keywords- Type
- string[]
- Description
- Replace the keyword set.
- Name
platforms- Type
- string[]
- Description
- Replace the platform set.
- Name
cadence- Type
- string
- Description
- New cadence (recurring only).
- Name
exclude_keywords- Type
- string[]
- Description
- Replace excludes.
- Name
exclude_keywords_strict- Type
- boolean
- Description
- Toggle transcript-level exclusion.
- Name
meta_ads_enabled- Type
- boolean
- Description
- Toggle Meta ads collection (future runs).
- Name
intent- Type
- string
- Description
- Update the agent's intent.
- Name
data_intelligence_enabled- Type
- boolean
- Description
- Toggle per-video intelligence for future runs.
- Name
english_only- Type
- boolean
- Description
- Toggle English-only collection for future runs.
true= English only,false= all languages. Does not re-filter content already collected.
Request
curl -X PUT https://api.virlo.ai/v1/agents/{agent_id} \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{ "keywords": ["high protein recipe", "protein meal prep", "protein desserts"] }'
Delete agent
Soft-deletes the agent (stops future runs; previously collected data remains accessible until purged). Returns 204 No Content.
Request
curl -X DELETE https://api.virlo.ai/v1/agents/{agent_id} \
-H "Authorization: Bearer {token}"
Get videos
Videos collected by the agent. This is where you filter the broad collection — for free, any way you like.
- Name
min_views- Type
- integer
- Description
Minimum view count (read-time filter). Example:
100000.
- Name
platforms- Type
- string[]
- Description
Filter by platform(s).
- Name
start_date- Type
- string
- Description
Only videos published on/after this ISO date. Example:
2026-01-01T00:00:00Z.
- Name
end_date- Type
- string
- Description
Only videos published on/before this ISO date.
- Name
order_by- Type
- string
- Description
views|publish_date|created_at.
- Name
sort- Type
- string
- Description
asc|desc.
- Name
intent_match- Type
- boolean
- Description
When the agent used
data_intelligence_enabled, return only intent-matching (true) or non-matching (false) videos.
- Name
region- Type
- string
- Description
Beta. Filter by resolved upload region — an ISO-3166-1 alpha-2 country code (case-insensitive), e.g.
US,CA,GB,RU,AU,JP,BR. Only videos whose region resolved to that country are returned; videos without a resolved region are excluded. See the Upload region note below.
- Name
page- Type
- integer
- Description
- 1-indexed page. Default
1.
- Name
limit- Type
- integer
- Description
- Results per page (max 100). Default
50.
Upload region (beta). Every video carries an upload_region field — the uploader's country as an ISO-3166-1 alpha-2 code (e.g. US, CA, RU, AU), or null when it couldn't be resolved. Region is derived deterministically where the platform provides it (TikTok video/creator region, YouTube channel country) and AI-inferred otherwise, so coverage is partial and still improving. Pass region=US (case-insensitive) to return only that country's uploads.
A companion upload_region_source field tells you how the region was resolved, so you can decide how much to trust it:
upload_region_source | Meaning | Precision |
|---|---|---|
tiktok_region | TikTok's own region field on the video | Exact |
youtube_channel_country | Country declared on the YouTube channel | Exact |
instagram_location_tag | Location tag attached to the post | Exact, but opt-in per post |
inferred_normalize | Derived from other signals when the platform gave none | Directional |
null | Could not be resolved — upload_region is null too | — |
Filter to the first three client-side when you need deterministic regions only.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/videos \
-H "Authorization: Bearer {token}" \
-d min_views=100000 \
-d start_date=2026-01-01T00:00:00Z \
-d order_by=views \
-d sort=desc \
-d limit=50
Response
{
"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-06-28T18:22:00Z",
"author": {
"username": "fitcoachjen",
"avatar_url": "https://.../avatar.jpg",
"followers": 48200,
"country": "US",
"verified": false
},
"hashtags": ["proteinrecipe", "highprotein", "healthydessert"],
"thumbnail_url": "https://.../thumb.jpg",
"keyword_found_by": "high protein recipe",
"upload_region": "US",
"upload_region_source": "tiktok_region",
"intent_match": { "matches": true, "reasoning": "Recipe tutorial matches the stated intent." },
"intelligence": { "hook_type": "problem_solution", "content_format": "tutorial" },
"intelligence_status": "ready",
"is_duet": false,
"is_stitch": false,
"sound": { "id": "8f6e5c50-1451-4007-a120-92744e632dad", "title": "original sound - fitcoachjen" }
}
]
}
}
The author object carries followers — which is what makes per-video virality scoring possible without a second call. The Agent Playbook ranks by ln(views / author.followers) × ln(author.followers) rather than raw views; everything you need for that is on the row. author.country is the creator's country (distinct from the video's upload_region).
The author object is { username, avatar_url, followers, country, verified }. It intentionally has no id — to pivot to similar creators you need an author_id, which comes from the creator outliers endpoint, not from a video row.
Pagination for videos / slideshows / ads / outliers uses total + limit + offset nested inside data (alongside the result array). Sounds and hashtags use a different top-level pagination envelope — see those endpoints.
Get slideshows
Slideshows (image carousels) collected by the agent. Same read-time filters as videos, including region.
Each slideshow carries a region field (ISO-3166-1 alpha-2). Slideshow region comes straight from TikTok, so it's the highest-coverage region signal Virlo exposes — a slideshow-heavy agent is the best surface for reliable regional filtering.
- Name
region- Type
- string
- Description
Beta. Filter by upload region — an ISO-3166-1 alpha-2 country code (case-insensitive), e.g.
TH,US,BR. Only slideshows uploaded from that country are returned.
- Name
min_views- Type
- integer
- Description
- Minimum view count (read-time filter).
- Name
start_date- Type
- string
- Description
- Only slideshows published on/after this ISO date.
- Name
end_date- Type
- string
- Description
- Only slideshows published on/before this ISO date.
- Name
order_by- Type
- string
- Description
views|publish_date|created_at.
- Name
sort- Type
- string
- Description
asc|desc.
- Name
page- Type
- integer
- Description
- 1-indexed page. Default
1.
- Name
limit- Type
- integer
- Description
- Results per page (max 100). Default
50.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/slideshows \
-H "Authorization: Bearer {token}" \
-d limit=50
Get ads
Meta ads collected when meta_ads_enabled was set. Supports order_by (created_at | page_like_count), sort, page, limit.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/ads \
-H "Authorization: Bearer {token}" \
-d limit=50
Get creator outliers
Creators who significantly outperform their follower count within the agent's corpus. Prefer order_by=weighted_score. Also supports platform, page, limit, sort, plus follower_tier (nano / micro / mid / macro) and category (topic substring) filters.
order_by=rising ranks by true run-over-run velocity (follower/view growth) and adds growth_followers / growth_views / growth_video_count fields per row; on a young agent without two snapshots it transparently falls back to the weighted-outlier ranking (the response echoes ranking: "outlier_fallback"). Pass a row's author_id to similar creators to explore adjacent creators.
Each row carries more than the ranking fields — enough to qualify a creator without a second call:
- Name
weighted_score- Type
- number
- Description
The ranking signal:
ln(views/followers) × ln(followers). ≥ 35 exceptional, 25–35 very strong, 18–25 strong, 10–18 promising.
- Name
outlier_ratio- Type
- number
- Description
Average views divided by follower count.
- Name
content_angle- Type
- string
- Description
AI-derived description of what this creator's angle actually is — the fastest way to judge fit without watching their videos.
- Name
creator_topics- Type
- string[]
- Description
Topics this creator covers across their catalogue.
- Name
matching_topics- Type
- string[]
- Description
The subset of
creator_topicsthat overlaps your agent's intent. A creator with many topics but one match is a tangential hit; several matches means they live in your niche.
- Name
median_views- Type
- integer
- Description
Median views — more robust than
avg_viewswhen one video went viral.
- Name
breakout_video_count- Type
- integer
- Description
How many of their videos in this corpus were outliers, not just one lucky hit.
- Name
posts_per_week- Type
- number
- Description
Posting cadence, derived from the publish-date spread.
- Name
avg_engagement_rate- Type
- number
- Description
Mean engagement rate across analyzed videos.
- Name
videos- Type
- object[]
- Description
The specific videos that qualified this creator as an outlier.
- Name
platform- Type
- string
- Description
youtube|tiktok|instagram.
- Name
identified_at- Type
- string
- Description
When this creator was first flagged as an outlier.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/creators/outliers \
-H "Authorization: Bearer {token}" \
-d order_by=weighted_score \
-d limit=25
Response
{
"data": {
"agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"agent_name": "Protein Recipes",
"total": 12,
"limit": 25,
"offset": 0,
"has_more": true,
"outliers": [
{
"author_id": "c3d4e5f6-a7b8-4901-9c2d-3e4f5a6b7c8d",
"platform": "tiktok",
"creator_url": "https://www.tiktok.com/@fitcoachjen",
"creator_avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/….jpg",
"follower_count": 48200,
"avg_views": 512000,
"median_views": 431000,
"outlier_ratio": 10.6,
"weighted_score": 25.4,
"videos_analyzed": 14,
"breakout_video_count": 4,
"posts_per_week": 5.2,
"avg_engagement_rate": 0.081,
"content_angle": "Three-ingredient high-protein desserts filmed in one unbroken take.",
"creator_topics": ["fitness", "recipes", "meal prep"],
"matching_topics": ["recipes", "meal prep"],
"identified_at": "2026-07-03T14:16:00Z"
}
]
}
}
Get sounds
Top sounds across the agent's collected videos, ranked by usage. Free. Use sort=rising (or growth_7d) to rank by run-over-run momentum — rows carry growth_video_count, growth_views, and a new / rising / steady / fading lifecycle label (growth fields are null until the agent has two run snapshots). Pivot to GET /v1/sounds/:sound_id/usage-history for a sound's full time-series.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/sounds \
-H "Authorization: Bearer {token}" \
-d sort=rising \
-d limit=20
Response
{
"data": [
{
"id": "8f6e5c50-1451-4007-a120-92744e632dad",
"title": "Saxophones getting louder",
"platform": "tiktok",
"usage_count": 138106,
"video_count": 42,
"avg_views": 612000,
"growth_video_count": 18,
"growth_views": 240000,
"lifecycle": "rising"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 63,
"total_pages": 4,
"has_next_page": true,
"has_prev_page": false
}
}
Get hashtags
Per-hashtag analytics computed over the agent's collected videos: volume, total/average views, average engagement, run-over-run growth with a new / rising / steady / fading lifecycle label, and the most active creators per hashtag. Free. Sort with sort=volume (default), growth, or avg_views.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/hashtags \
-H "Authorization: Bearer {token}" \
-d sort=growth \
-d limit=25
Response
{
"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://.../avatar.jpg",
"video_count": 8
}
]
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 118,
"total_pages": 5,
"has_next_page": true,
"has_prev_page": false
}
}
Get benchmarks
Median engagement rate, follower count, niche video count, and posting frequency across the agent's creators, bucketed by follower tier. Free. Answers "how does this creator compare to the genre norm?".
Returns one row per tier — data is a plain array with no pagination.
- Name
follower_tier- Type
- string
- Description
nano|micro|mid|macro.
- Name
creator_count- Type
- integer
- Description
Creators in this tier within the agent's corpus. Treat a tier with a small
creator_countas directional — a median over a handful of creators is noisy.
- Name
median_followers- Type
- integer
- Description
Median follower count for the tier.
- Name
median_engagement_rate- Type
- number
- Description
Median engagement rate (0–1). Compare a candidate creator against their own tier — engagement falls predictably as follower count rises, so cross-tier comparison is misleading.
- Name
median_videos_in_niche- Type
- integer
- Description
Median number of the agent's collected videos per creator in the tier — a proxy for how committed the tier is to this niche versus passing through it.
- Name
median_posting_frequency_days- Type
- number
- Description
Median gap in days between posts.
Request
curl https://api.virlo.ai/v1/agents/{agent_id}/benchmarks \
-H "Authorization: Bearer {token}"
Response
{
"data": [
{
"follower_tier": "micro",
"creator_count": 48,
"median_followers": 42000,
"median_engagement_rate": 0.071,
"median_videos_in_niche": 6,
"median_posting_frequency_days": 1.4
}
]
}
Get affinity
Beta — directional signal; methodology may evolve. Genre-adjacency derived from the agent's corpus: dominant creator topics, co-occurring hashtags, and co-occurring sounds. Not a follow-graph. Free.
Request
curl https://api.virlo.ai/v1/agents/{agent_id}/affinity \
-H "Authorization: Bearer {token}"
Get similar creators
Beta — directional signal; methodology may evolve. Other creators in the agent's corpus ranked by shared hashtags and shared sounds with the target creator (co-occurrence; use the author_id from creator outliers). Free. Supports limit (default 20, max 100).
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/creators/{author_id}/similar \
-H "Authorization: Bearer {token}" \
-d limit=20
Get latest analysis
The full structured AI analysis from the most recent completed cycle — analysis is a short human-readable highlight, analysis_data carries the full object (themes, connecting_thread, viral_tactics, timing_analysis, top_10_breakdown, …). Free.
The latest analysis fields are also merged into GET /v1/agents/:id (analysis, analysis_data, analysis_batch_start, analysis_batch_end). For every past batch, see analysis history.
Request
curl https://api.virlo.ai/v1/agents/{agent_id}/analysis/latest \
-H "Authorization: Bearer {token}"
Response
{
"data": {
"id": "d5e6f7a8-b9c0-4d12-8e34-5f6a7b8c9d01",
"insight_type": "content_research_agent",
"reference_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"batch_start": "2026-06-26T14:00:00Z",
"batch_end": "2026-07-03T14:00:00Z",
"analysis": "Protein desserts and no-cook overnight recipes are driving the most outsized reach this week...",
"analysis_data": {
"key_highlight": "No-cook 'overnight' protein recipes are the breakout format.",
"overview": "...",
"themes": [
{
"why_it_works": "Zero-effort framing lowers the barrier to trying the recipe.",
"tactics": ["show the jar in the first frame", "call out the protein grams in text"],
"confidence": 0.82,
"evidence_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"]
}
],
"top_10_breakdown": []
},
"video_count": 283,
"analyzed_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"],
"model_used": "claude-sonnet",
"created_at": "2026-07-03T14:17:00Z"
}
}
Get analysis history
Paginated list of every completed analysis batch for this agent, newest first. Same item shape as latest analysis. Free.
- Name
page- Type
- integer
- Description
1-indexed page. Default
1.
- Name
limit- Type
- integer
- Description
Results per page, 1–100. Default
50.
- Name
start_date- Type
- string
- Description
Optional window start (
YYYY-MM-DD).
- Name
end_date- Type
- string
- Description
Optional window end (
YYYY-MM-DD).
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/analysis \
-H "Authorization: Bearer {token}" \
-d limit=20
Get latest trends
Every trend detected in the most recent completed analysis cycle. Each trend carries evidence videos, aggregate engagement stats, a stable_key for time-series joins, and a new / rising / steady / fading status versus the previous cycle. Free.
For every past trend row (or one trend's trajectory), see trends history.
Request
curl https://api.virlo.ai/v1/agents/{agent_id}/trends/latest \
-H "Authorization: Bearer {token}"
Response
{
"data": {
"reference_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"insight_type": "content_research_agent",
"viral_insight_id": "f1a2b3c4-d5e6-4f78-9a01-2b3c4d5e6f70",
"batch_start": "2026-06-26T14:00:00Z",
"batch_end": "2026-07-03T14:00:00Z",
"total": 6,
"trends": [
{
"id": "aa11bb22-cc33-4d44-8e55-6f778899aa00",
"rank": 1,
"stable_key": "overnight-protein-oats",
"name": "No-cook overnight protein oats",
"why_it_works": "Zero-effort framing + high protein payoff in one scroll.",
"tactics": ["show the jar first", "text-overlay the protein grams"],
"confidence": 0.86,
"evidence_video_ids": ["e7c2a1b4-9f3d-4e88-8a12-5b6c7d8e9f01"],
"video_count": 38,
"total_views": 18400000,
"total_likes": 2100000,
"total_comments": 54000,
"total_shares": 210000,
"avg_virality_score": 22.7,
"platform_breakdown": { "tiktok": 24, "instagram": 9, "youtube": 5 },
"top_creators": ["fitcoachjen"],
"peak_hour_utc": 18,
"status": "rising",
"first_seen_at": "2026-06-19T14:00:00Z",
"prev_video_count": 21,
"prev_total_views": 9200000,
"created_at": "2026-07-03T14:17:00Z"
}
]
}
}
Get trends history
Paginated list of every trend row produced for this agent, newest first. Filter by stable_key to follow one trend across runs — a trend moving new → rising with growing video_count is the strongest "post about this now" signal on recurring agents. Free.
- Name
page- Type
- integer
- Description
1-indexed page. Default
1.
- Name
limit- Type
- integer
- Description
Results per page, 1–100. Default
50.
- Name
stable_key- Type
- string
- Description
Optional. Isolate a single recurring trend's trajectory.
- Name
start_date- Type
- string
- Description
Optional window start (
YYYY-MM-DD).
- Name
end_date- Type
- string
- Description
Optional window end (
YYYY-MM-DD).
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/trends \
-H "Authorization: Bearer {token}" \
-d stable_key=overnight-protein-oats \
-d limit=50
List runs
Lists the agent's runs (one for a one-shot agent, one per cycle for a recurring agent), newest first. Free. Fetch a single run with Get run.
Each run is a collection report — it tells you not just how many videos landed, but how many were discarded and why. When an agent returns less than you expected, this is the endpoint that explains it.
- Name
status- Type
- string
- Description
pending|processing|completed|failed|partial_failure.partial_failureis a usable terminal state — one platform or keyword failed, the rest succeeded.
- Name
videos_linked- Type
- integer
- Description
Videos attached to the agent by this run.
slideshows_linkedandmeta_ads_linkedare the equivalents for those content types.
- Name
youtube_count / tiktok_count / instagram_count- Type
- integer
- Description
Per-platform contribution for this run — the quickest way to spot one platform underdelivering.
- Name
intent_filtered- Type
- integer
- Description
Videos collected but dropped for not matching your
intent. A high number relative tovideos_linkedmeans your keywords are pulling in off-topic content — tighten them or sharpen the intent.
- Name
language_filtered_count- Type
- integer
- Description
Videos dropped by the
english_onlyfilter. If this is large, the niche is mostly non-English — setenglish_only: false.
- Name
exclude_keywords_filtered- Type
- integer
- Description
Videos dropped by your
exclude_keywords.
- Name
duplicates_dropped- Type
- integer
- Description
Videos already collected by an earlier run of this agent.
- Name
keyword_breakdown- Type
- object[]
- Description
Per-keyword collection detail:
keyword,videos_inserted,videos_updated, and per-platform counts. The single most useful field for tuning an agent — it shows exactly which keywords are producing and which are dead weight.
- Name
outliers_identified- Type
- integer
- Description
Creator outliers detected in this run's corpus. See creator outliers.
- Name
trends_detected- Type
- string[]
- Description
Trend names detected in this cycle. Full rows via trends.
- Name
execution_time_ms- Type
- integer
- Description
Wall-clock duration of the run.
Run objects do not carry finalized — that flag lives on the agent resource (GET /v1/agents/:id) because it also accounts for secondary AI jobs still in flight. A run reaching status: "completed" means the scrape finished; use the agent's finalized: true to know everything is settled.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/runs \
-H "Authorization: Bearer {token}" \
-d limit=50
Response
{
"data": {
"agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"limit": 50,
"offset": 0,
"count": 2,
"runs": [
{
"id": "5c8b1f20-3a4d-4e7f-9b12-6d2a1c7e8f90",
"agent_id": "a1f3c8e2-0b7d-4a91-9c2e-2f5d6b8e1a44",
"status": "completed",
"videos_linked": 283,
"slideshows_linked": 41,
"meta_ads_linked": 0,
"youtube_count": 62,
"tiktok_count": 178,
"instagram_count": 43,
"total_videos_inserted": 283,
"total_videos_updated": 61,
"total_videos_failed": 0,
"intent_filtered": 172,
"language_filtered_count": 0,
"exclude_keywords_filtered": 14,
"duplicates_dropped": 0,
"outliers_identified": 12,
"trends_detected": ["No-cook overnight protein oats"],
"keyword_breakdown": [
{
"keyword": "high protein recipe",
"videos_inserted": 41,
"videos_updated": 12,
"tiktok_count": 27,
"youtube_count": 12,
"instagram_count": 2
}
],
"execution_time_ms": 1080000,
"created_at": "2026-07-03T14:00:00Z",
"started_at": "2026-07-03T14:00:00Z",
"completed_at": "2026-07-03T14:18:00Z"
}
]
}
}
This list uses count (items on this page), not total — the runs list does not report a grand total. Page through with limit + page until a page returns fewer than limit items.
Get run
Fetch one run by UUID. Returns the same object as List runs — status, collection counts, filter counts, keyword_breakdown, and timing. 404 if the run is not on this agent. Free.
Request
curl https://api.virlo.ai/v1/agents/{agent_id}/runs/{run_id} \
-H "Authorization: Bearer {token}"
Autonomy overview
Agents don't just collect — they reflect on their own performance and propose safe changes to keep finding content without manual babysitting. When an agent's yield drops, it can propose refreshing stale keywords or widening a starved collection window (and, as a last resort, dropping the view floor).
Self-optimization applies to recurring agents only — a one-shot runs once, so there's no future run to improve. One-shot agents won't produce proposals or activity.
How it works
- Every proposed change is surfaced as a proposal with a human-readable rationale and a before/after
diff. autonomy_level: "suggest"(default) — proposals wait for your approval via apply/dismiss.autonomy_level: "autopilot"— safe changes auto-apply. Autopilot must be unlocked first by approving one proposal manually, and it only ever widens collection — it never restricts what you see.cognition_enabled: falsepauses self-optimization entirely (the agent keeps collecting).- Everything the agent decides is recorded in its activity log, and any applied change is revertible.
Autopilot is intentionally conservative: it can broaden a starved agent's window or lower an over-restrictive floor, but it can never narrow collection or change your keywords without a proposal you can see and revert.
Team-level unlock. The "unlock autopilot by approving one proposal" step happens once per team, not once per agent. After your team has unlocked it, newly created agents come back already at autonomy_level: "autopilot" with autopilot_unlocked: true — expected behavior. Set any agent back to suggest via Set autonomy if you want manual approval.
Get activity
The agent's decision log — reflections, milestones, and the changes it has made over time. Free. Supports limit (max 100).
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/activity \
-H "Authorization: Bearer {token}" \
-d limit=20
List proposals
Self-optimization proposals. Filter with status (pending | applied | auto_applied | dismissed | reverted). Each proposal includes its type (keyword_refresh | filter_change), a rationale, and a diff.
Request
curl -G https://api.virlo.ai/v1/agents/{agent_id}/proposals \
-H "Authorization: Bearer {token}" \
-d status=pending
Apply / dismiss / revert a proposal
POST /v1/agents/:id/proposals/:proposal_id/apply— approve and apply a pending proposal. The first manual apply unlocks autopilot for the agent.POST /v1/agents/:id/proposals/:proposal_id/dismiss— reject a pending proposal.POST /v1/agents/:id/proposals/:proposal_id/revert— roll an applied change back to the prior config.
Request
curl -X POST https://api.virlo.ai/v1/agents/{agent_id}/proposals/{proposal_id}/apply \
-H "Authorization: Bearer {token}"
Set autonomy
Choose how much the agent may change on its own.
- Name
autonomy_level- Type
- string
- Description
suggest(changes wait for approval) orautopilot(safe widenings auto-apply; must be unlocked first).
- Name
cognition_enabled- Type
- boolean
- Description
Master switch for self-optimization.
falsepauses all reflection + proposals.
Request
curl -X PUT https://api.virlo.ai/v1/agents/{agent_id}/autonomy \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{ "autonomy_level": "autopilot" }'
