# Virlo API v1 — Complete Reference > Social media intelligence API for TikTok, YouTube Shorts, and Instagram Reels. Track viral content, hashtags, trends, and creators with real-time analytics across 4M+ indexed creators and 8.7M+ videos. This document is the complete Virlo API reference for LLM and AI agent consumption. **Primary research surface: Content Research Agents (`POST /v1/agents`)**. Satellite, Tracking, Sounds, Trends, Hashtags, and digests are also covered. `/v1/orbit` and `/v1/comet` are DEPRECATED shims for Agents (removed August 3, 2026) — do not start new integrations on them. --- ## Authentication All requests require a Bearer token in the Authorization header. ``` Authorization: Bearer virlo_tkn_ ``` Get your API key at https://dev.virlo.ai/dashboard. Keys start with `virlo_tkn_`. Keep keys secret — never expose them client-side or commit them to version control. ## Base URL ``` https://api.virlo.ai/v1 ``` Parameter names and response fields use snake_case. Satellite responses additionally carry deprecated camelCase aliases next to a few canonical keys (`publishDate` → `publish_date`, `hasMore` → `has_more`, …) — read the snake_case form; the aliases are removed August 3, 2026. `author_id` on digest videos may be null when the author row is not linked. Sound `cover_url` may be a relative storage path or full CDN URL. All responses are wrapped in a `{ "data": { ... } }` envelope. For lightweight agent status polling, use `GET /v1/agents/:id` and read only status/finalized/pending_jobs — pull heavy arrays from the dedicated sub-paths (`/videos`, `/slideshows`, etc.). Legacy `GET /v1/orbit/:orbit_id` can return full inline videos/slideshows/ads when complete (hundreds of KB); MCP `get_keyword_search_results` with `data_type=status` strips those arrays. ## Pagination Pagination via `limit` (1-100, values >100 are clamped to 100, default 50) and `page` (1-indexed, default 1). The metadata SHAPE varies by endpoint: content sub-resources (videos/slideshows/ads/outliers) nest `total`/`limit`/`offset` inside `data`; sounds/hashtags/tracking use a top-level `pagination` object; `GET /v1/agents` and `/runs` return `count` (this page) with no grand total. See https://dev.virlo.ai/docs/pagination. ## Rate Limits Rate limits are per API key, tracked by sliding time windows. Response headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. On 429, check `Retry-After` header. Default limits: - Global: 10,000 requests/day (applies to all endpoints without specific rules) - Satellite creator lookup: 5/min, 100/hour, 1,000/day - Satellite sound lookup: 5/min, 100/hour, 1,000/day - Satellite hashtag lookup: 5/min, 100/hour, 1,000/day - Satellite video outlier: 5/min, 100/hour, 1,000/day - Video digest, trends, hashtags: 50/min, 500/hour, 5,000/day - Tracking endpoints (posts, cadence, collection): global limit only (10,000/day) - Status polling endpoints: global limit only (10,000/day) Tip: Use batch endpoints (POST /v1/satellite/creators/batch) to process up to 25 creators in 1 request. Contact info@virlo.ai for custom rate limits. ## Error Format Errors return: `{ "statusCode": number, "code": string, "error": string, "message": string | string[] }`. **Branch on `code`** — a stable machine-readable identifier — never on `message` (human-facing, may be reworded). `message` is an array on validation errors. Codes by status: - 400 `validation_error` (also `invalid_date_range`, `unknown_region`, `unknown_event_type`): invalid parameters. - 401 `missing_api_key` (no Authorization header) or `invalid_api_key` (malformed/revoked). - 402 `insufficient_credits`: carries `required_credits` + `remaining_credits`. Do NOT retry; add funds at https://dev.virlo.ai/dashboard/billing. - 403 `forbidden`. 404 `not_found`. 409 `conflict`. - 429 `rate_limit_exceeded`: carries `limit`, `remaining`, `reset_at`, `retry_after`. Not billed. - 5xx `upstream_error` / `service_unavailable` / `internal_error`: retry with backoff. Not billed. Only 2xx responses are billed. ## Billing Pay-as-you-go prepaid balance. Add funds in Billing (minimum shown there; A/B tested via api-min-topup-amount), use the API, auto top-up keeps you running. No subscriptions. Balance never expires. Response headers: - `X-Cost`: Dollar cost of this request, "0.00" for free reads (e.g. "0.25"). Present on **every** response. - `X-Credits-Used`: Internal credits consumed, "0" for free reads (1 credit = $0.01). Present on **every** response. - `X-Credits-Remaining`: Internal credits remaining. Present **only on charged responses** (cost > 0). - `X-Balance-Remaining`: Dollar balance remaining (e.g. "47.50"). Present **only on charged responses** (cost > 0). So free reads carry `X-Cost: 0.00` and `X-Credits-Used: 0` but omit the two remaining-balance headers — only charged calls report your remaining balance. ### Pricing Per Endpoint **Free ($0.00):** - GET /v1/agents — list agents - GET /v1/agents/:id — poll agent status/results (finalized signal) - GET /v1/agents/:id/videos — retrieve agent videos (filter min_views/dates/platforms here) - GET /v1/agents/:id/slideshows — retrieve agent slideshows - GET /v1/agents/:id/ads — retrieve agent Meta ads - GET /v1/agents/:id/creators/outliers — retrieve agent creator outliers - GET /v1/agents/:id/sounds — top sounds (sort=rising/growth_7d; rows carry a lifecycle label — filter client-side) - GET /v1/agents/:id/hashtags — per-hashtag analytics + growth + top creators - GET /v1/agents/:id/benchmarks — genre norms by follower tier - GET /v1/agents/:id/affinity — genre adjacency (BETA) - GET /v1/agents/:id/creators/:creator_id/similar — similar creators (BETA) - GET /v1/agents/:id/analysis/latest — latest AI analysis - GET /v1/agents/:id/analysis — AI analysis history - GET /v1/agents/:id/trends/latest — latest AI-detected trends - GET /v1/agents/:id/trends — trends history (stable_key time-series) - GET /v1/agents/:id/runs — list runs - GET /v1/agents/:id/runs/:run_id — get a single run - GET /v1/agents/:id/activity — autonomy activity (recurring) - GET /v1/agents/:id/events — breaking events/stories detected in the niche (recurring); title/summary/salience/status/keywords/evidence - GET /v1/agents/:id/proposals — self-optimization proposals (recurring) - POST /v1/agents/:id/proposals/:pid/{apply,dismiss,revert} — act on a proposal - PUT /v1/agents/:id/autonomy — set autonomy_level (suggest|autopilot) + cognition_enabled - PUT /v1/agents/:id — update agent config - DELETE /v1/agents/:id — delete agent (204) - POST /v1/agents/suggest-keywords — FREE pre-flight keyword generation from an intent. Returns keywords, exclude_keywords, reasoning, and a quality score. Creates nothing and costs nothing — call this BEFORE POST /v1/agents so the paid run starts with a good keyword set. Modes: create (default), refresh (replace stale keywords on an existing agent), opportunity (find under-covered adjacent angles). - GET /v1/satellite/creator/status/:job_id — poll Satellite status - GET /v1/satellite/creators/batch/:batch_id — poll batch lookup status - GET /v1/satellite/video-outlier/status/:job_id — poll Video Outlier status - GET /v1/satellite/sounds/status/:job_id — poll sound lookup status - GET /v1/satellite/hashtags/status/:job_id — poll hashtag lookup status - GET /v1/satellite/runs/:run_id — re-read any past creator, sound, hashtag, or batch run (video-outlier persistence not shipped yet) - GET /v1/satellite/runs — list every satellite run owned by your team - GET /v1/satellite/runs/:run_id/videos — paginated videos sub-resource for any past run - GET /v1/account/balance — check credit balance and pricing - DEPRECATED (removed Aug 3, 2026): all `GET /v1/orbit/*` and `GET /v1/comet/*` reads — same data as the matching `GET /v1/agents/:id/*` path; IDs are interchangeable. Do not use for new integrations. **$0.25 (tracking):** - POST /v1/tracking/creators — start tracking a creator - POST /v1/tracking/videos — start tracking a video **$0.50 – $2.00 (post collection, varies by depth):** - POST /v1/tracking/creators/:id/posts/collect — trigger deep post collection. Depth tiers: standard (50 videos, $0.50), deep (200 videos, $1.00), full (500 videos, $2.00) **Free (tracking retrieval):** - GET /v1/tracking/creators — list tracked creators - GET /v1/tracking/creators/:id — get tracked creator details (includes AI category slug + content_tags) - GET /v1/tracking/creators/:id/report — get latest creator AI report (auto-generated each cycle) - GET /v1/tracking/creators/:id/snapshots — get creator metric snapshots (supports start_date, end_date, limit) - GET /v1/tracking/creators/:id/posts — list creator's collected posts with is_duet/is_stitch TikTok flags and a nested sound object (sound.external_id = platform-native sound id; null for YouTube) - GET /v1/tracking/creators/:id/posts/:post_id — get single post detail (includes sound) - GET /v1/tracking/creators/:id/posts/collect/:collection_id — poll collection job status - GET /v1/tracking/creators/:id/posting-cadence — get posting frequency analytics - PATCH /v1/tracking/creators/:id — update creator tracking settings - DELETE /v1/tracking/creators/:id — stop tracking creator - GET /v1/tracking/videos — list tracked videos - GET /v1/tracking/videos/:id — get tracked video details - GET /v1/tracking/videos/:id/report — get latest video AI report (auto-generated each cycle) - GET /v1/tracking/videos/:id/snapshots — get video metric snapshots (supports start_date, end_date, limit) - PATCH /v1/tracking/videos/:id — update video tracking settings - DELETE /v1/tracking/videos/:id — stop tracking video - GET /v1/tracking/creators/:id/audience-demographics — age/gender breakdown for a tracked creator - GET /v1/tracking/creators/:id/audience-geography — country/region breakdown for a tracked creator - GET /v1/tracking/creators/:id/audience-refresh/:jobId — poll a tracking-scoped audience refresh job - GET /v1/audience/snapshot/:job_id — generic audience-snapshot poll; works for ANY audience job (satellite-path or tracking-path). Prefer this when you only have a job_id. - POST /v1/agents/suggest-keywords — quality-graded keyword suggestions for an intent (free, synchronous, creates nothing) **$0.05:** - GET /v1/hashtags — cross-platform hashtag stats - GET /v1/hashtags/:hashtag/performance — detailed hashtag analytics - GET /v1/youtube/hashtags — YouTube-specific hashtag data - GET /v1/tiktok/hashtags — TikTok-specific hashtag data - GET /v1/instagram/hashtags — Instagram-specific hashtag data - GET /v1/sounds/:sound_id — sound details + aggregate stats + track_resolution (resolve=true adds $0.10 on a fresh resolution) - GET /v1/sounds/:sound_id/usage-history — daily usage snapshots with deltas **$0.10:** - GET /v1/sounds/search?q=... — fuzzy search sounds by title **$0.25:** - GET /v1/videos/digest — top videos from last 48 hours (all platforms) - GET /v1/youtube/videos/digest — YouTube videos digest - GET /v1/tiktok/videos/digest — TikTok videos digest - GET /v1/instagram/videos/digest — Instagram videos digest - GET /v1/trends — trend groups with rankings (+ live momentum) - GET /v1/trends/digest — today's trends digest - GET /v1/trends/emerging — momentum-ranked early-stage (new/rising) trends - GET /v1/sounds/trending — sounds currently trending (default sort videos_7d) - GET /v1/sounds/breakout — sounds with high recent-burst ratio (rapidly rising from a small baseline) - GET /v1/sounds/:sound_id/videos — videos using a specific sound - GET /v1/sounds/by-creator/:platform/:handle — creator's sounds with UGC metrics **$0.50:** - POST /v1/agents with is_recurring: false — create a one-shot research agent (recommended; includes AI analysis + trends). Recurring agents (is_recurring: true) are FREE to create and bill $0.50 per scheduled run instead. - POST /v1/orbit — (DEPRECATED) queue keyword search (most valuable: includes AI analysis) - POST /v1/comet — (DEPRECATED) create automated niche monitoring - GET /v1/satellite/creator/:platform/:username — start creator lookup (profile includes AI category slug + content_tags; videos include is_duet/is_stitch flags, thumbnail_url, duration (seconds), and is_pinned (TikTok only; deprecated camelCase isPinned alias until Aug 3, 2026); +$0.50 with trend_analysis=true to also run LLM trend detection over the creator's body of work) - POST /v1/satellite/creators/batch — batch creator lookup (up to 25 handles, per-handle pricing) - POST /v1/satellite/video-outlier — start video outlier analysis - GET /v1/satellite/sounds/:platform/:music_id — start sound lookup (TikTok + Instagram; YouTube not supported; $0.50 base, +$0.50 with trend_analysis=true) - GET /v1/satellite/hashtags/:platform/:hashtag — start hashtag lookup (tiktok | instagram | youtube; $0.50 base, +$0.50 with trend_analysis=true; depth tiers: deep ~300 videos +$0.50 (waived when trend_analysis=true), full ~500 videos +$1.50 — TikTok + YouTube only, Instagram is depth=standard only; repeating the same lookup within 6 hours returns the cached run free) **+$1.00 add-on (Data Intelligence):** - POST /v1/agents with data_intelligence_enabled: true — adds 43 AI fields per video and slideshow (one-shot: $0.50 + $1.00 = $1.50 total; recurring: +$1.00 per scheduled run). Add intent for intent-based filtering with intent_match per video ({matches, reasoning} object; filter with ?intent_match=true). Aggregate intent_summary arrives in the content_research_agent.run.completed webhook payload (GET /v1/agents/:id does not return it — count matches via the filtered videos endpoint's total) - POST /v1/orbit with data_intelligence_enabled: true — (DEPRECATED) adds 43 AI fields per video ($1.50 total). Add intent for intent-based filtering with intent_match per video and intent_summary - POST /v1/comet with data_intelligence_enabled: true — (DEPRECATED) adds 43 AI fields per video ($1.50 total). Add intent for intent-based filtering with intent_match per video and intent_summary ### Funding - Add funds at https://dev.virlo.ai/dashboard/billing (minimum shown there; A/B tested via api-min-topup-amount) - Balance never expires. Auto top-up enabled by default. - When balance runs out: API returns 402, auto top-up triggers if enabled. --- ## Async Workflow State Machines Several Virlo endpoints use asynchronous processing. You queue a job, then poll for results. CRITICAL FOR AI AGENTS: Response times vary significantly based on keyword count, whether meta_ads is enabled, and current server load. NEVER set hard timeouts. ALWAYS continue polling until you receive a terminal status. The canonical "done" signal is `finalized: true` (see the Async Data Model guide at https://dev.virlo.ai/docs/async-data) — `status: completed` alone means the main scrape is done but secondary AI jobs may still be running. ### Content Research Agent Lifecycle (recommended) Status flow: pending → processing → completed | partial_failure | failed. `partial_failure` is a USABLE terminal state (one platform/keyword failed, the rest is collected) — treat it like `completed`. (Only the legacy /keyword-search shim's POST acknowledgment says `status: "queued"` — run rows never do.) Workflow: POST /v1/agents → save the returned `id` (and `job_id`) → GET /v1/agents/:id (repeat until `finalized: true`) → GET videos, slideshows, ads, creators/outliers, sounds, hashtags, analysis/latest, trends/latest. - One-shot agents (`is_recurring: false`): a single run. Typical completion ~15-20 minutes, up to 45 minutes with meta_ads_enabled. Poll every 60 seconds; never hardcode a timeout. - Recurring agents (`is_recurring: true`): the first run kicks off immediately at create; subsequent runs fire on the `cadence`. Results accumulate across runs. Follow each completion via the `content_research_agent.run.completed` webhook (carries `is_recurring`) instead of polling. - `finalized: true` is the canonical done signal — `status: completed` alone may still have secondary AI jobs (analysis, trends, per-video intelligence) running, listed in `pending_jobs[]` with `retry_after_seconds`. ### Orbit / Comet Lifecycles (DEPRECATED) > DEPRECATED — removed August 3, 2026. Use the Content Research Agent lifecycle above. > - Orbit → `POST /v1/agents` with `is_recurring: false` > - Comet → `POST /v1/agents` with `is_recurring: true` + `cadence` > IDs are interchangeable; every `/v1/orbit/:id/*` and `/v1/comet/:id/*` read maps 1:1 to `/v1/agents/:id/*`. Prefer webhook `content_research_agent.run.completed` over legacy `orbit.run.completed` / `comet.run.completed`. ### Satellite (Creator Lookup) Lifecycle Status flow: processing → completed | failed Typical execution time: - Default lookup (max_videos=20): 20-60 seconds (can be longer under high traffic) - trend_analysis=true (100 videos + LLM): 60-120 seconds, can run longer under load Results in the poll cache expire after 24 hours. The durable satellite_run row never expires — re-read via GET /v1/satellite/runs/:run_id. Rate limits: 5/min, 100/hour, 1,000/day. Recommended polling interval: every 10-15 seconds. Polling is free. Workflow: GET /v1/satellite/creator/:platform/:username → save job_id → GET /v1/satellite/creator/status/:job_id (repeat until completed). Save the run_id from the completed payload for free re-reads later. ### Batch Creator Lookup Lifecycle Submit multiple creators at once. Each creator is processed as an independent lookup job. Workflow: POST /v1/satellite/creators/batch → save batch_id → GET /v1/satellite/creators/batch/:batch_id (repeat until all completed/failed) The batch poll returns aggregate counts (completed, failed, processing) and per-creator statuses. Individual creator results can also be polled via GET /v1/satellite/creator/status/:job_id. Recommended polling interval: every 15-30 seconds Batch results expire after 24 hours. ### Video Outlier Analysis Lifecycle Status flow: processing → completed | failed Typical execution time: 20-60 seconds (can be longer under high traffic) Status poll cache lasts 24 hours. NOTE: unlike creator/sound/batch lookups, video-outlier results are NOT yet persisted to the durable runs ledger — store the result within 24h or re-run. Rate limits: 5/min, 100/hour, 1,000/day. Recommended polling interval: every 10-15 seconds Workflow: POST /v1/satellite/video-outlier → save job_id → GET /v1/satellite/video-outlier/status/:job_id (repeat until completed) ### Sound Lookup Lifecycle (TikTok + Instagram) Status flow: processing → completed | failed Typical execution time (measured in production): - Default (max_videos=50): 1-3 minutes - trend_analysis=true (~300 videos + LLM): ~8 minutes on average, plan for up to 20 minutes - Recommended polling interval: every 30 seconds for trend_analysis lookups Results in the status poll cache expire after 24 hours. The durable satellite_run row never expires — re-read via GET /v1/satellite/runs/:run_id. Recommended polling interval: every 10-15 seconds. Polling is free. Workflow: GET /v1/satellite/sounds/{tiktok|instagram}/:music_id → save job_id → GET /v1/satellite/sounds/status/:job_id (repeat until completed). Save the run_id from the completed payload for free re-reads later. ### Hashtag Lookup Lifecycle (TikTok + Instagram + YouTube) Status flow: processing → completed | failed Typical execution time: - Default (max_videos=50): 1-3 minutes - trend_analysis=true (~300-video deep fetch + LLM): ~8 minutes on average, plan for up to 20 minutes - Recommended polling interval: every 30 seconds for trend_analysis lookups Repeating the same lookup within 6 hours returns the cached run for FREE — the acknowledgment carries `cached: true`. The cache only satisfies a request with the same `sort`, (when `trend_analysis=true`) trends already analyzed, and an equal-or-deeper stored `depth` (a deeper cached run satisfies a shallower request for free; a shallower one re-scrapes and bills); changing those parameters re-scrapes and bills normally. Results in the status poll cache expire after 24 hours. The durable satellite_run row never expires — re-read via GET /v1/satellite/runs/:run_id. Recommended polling interval: every 10-15 seconds. Polling is free. Workflow: GET /v1/satellite/hashtags/{tiktok|instagram|youtube}/:hashtag → save job_id → GET /v1/satellite/hashtags/status/:job_id (repeat until completed). Save the run_id from the completed payload for free re-reads later. --- ## Webhook Events Subscribe at POST /v1/webhooks (see the Tracking/Account section for the management endpoints). Every delivery is a POST with envelope `{ id, event, created_at, data }`; the outcome (`success` | `partial_failure` | `failed`) is inside `data.status` — do NOT split success/failure into separate subscriptions. Management responses are NOT `{data}`-enveloped (bare array/object). - `content_research_agent.run.completed` — **preferred.** Fires for every agent run (one-shot AND recurring); payload carries `is_recurring`. Team-scoped. - `content_research_agent.event.detected` — a recurring agent confirmed a breaking event in its niche (corpus burst or news scan). Fires BETWEEN scheduled runs. Payload: `title`, `summary`, `salience`, `source`, `event_date` (nullable — null today; dated event themes appear in the run's analysis instead), `keywords_added[]`, `evidence[]` (corroborating items — news `title`/`url`/`domain` for news_scan, top video titles for corpus_burst), `action_taken` (`timely_keywords`|`collecting_early`|`breaking_ingest`|`none`), `next_run_at`. Read the full list any time via GET /v1/agents/:id/events. Team-scoped. (Internal/copilot name: `cra.event.detected`.) - `orbit.run.completed` / `comet.run.completed` — legacy aliases for the above (DEPRECATED, still fire during the migration window). Team-scoped. - `satellite.lookup.completed` — a creator/sound/hashtag/video/batch lookup finished; payload has a `type` discriminator (`data.type: "hashtag_lookup"` for hashtag lookups). Team-scoped. - `tracking.cycle.completed` — a tracked creator/video cycle finished (new snapshot + AI report). Team-scoped. - `tracking.outlier_video.detected` — a tracked creator posted a video far above their median. Team-scoped. - `tracking.paused` — tracking auto-paused after repeated scrape failures. Team-scoped. - `audience.snapshot.completed` — an audience demographics/geography snapshot finished. Team-scoped. - `trends.daily.completed` — the daily GLOBAL trend run finished (~01:00 UTC); payload carries `region: "global"`. Broadcast. - `trends.region.completed` — a per-region trend run finished (us/gb/au/…); same payload shape plus the `region` that completed. One delivery per region. Broadcast. Prefer webhooks over polling for high volume. Each `pending_jobs[]` entry on an async GET names the exact `webhook_event` to subscribe to. --- ## Endpoint Reference **Read order for research tasks:** start at [Content Research Agents (/v1/agents)](#content-research-agents-v1agents--unified-api) below. Hashtags / trends / digests / Satellite / Tracking follow. Skip the Orbit and Comet sections unless you are migrating a legacy integration. ### GET /v1/hashtags Cross-platform hashtag statistics with date filtering. Cost: $0.05 Query parameters: - start_date (string, required): Start date YYYY-MM-DD. Max range 90 days. - end_date (string, required): End date YYYY-MM-DD. - limit (integer): Results per page, 1-100, default 50. - order_by (string): "count" or "views". Default "count". - sort (string): "asc" or "desc". Default "desc". Response schema: ```json { "data": [ { "hashtag": "#fyp", "count": 6124, "total_views": 7670661488 } ] } ``` ### GET /v1/hashtags/:hashtag/performance Detailed performance analytics for a specific hashtag. Cost: $0.05 Path parameters: - hashtag (string, required): The hashtag to look up (without # prefix). Query parameters: - start_date (string, optional): Start date YYYY-MM-DD. - end_date (string, optional): End date YYYY-MM-DD. Response schema: ```json { "data": { "hashtag": "fyp", "video_count": 434, "total_views": 1159398224, "avg_views": 2671424.48, "total_likes": 76534113, "avg_likes": 176345.88, "total_comments": 443567, "avg_comments": 1022.04 } } ``` ### GET /v1/trends Trend groups with ranked trends. Cost: $0.25 Query parameters: - start_date (string, optional): Default last 24h. - end_date (string, optional): Default now. - limit (integer): Results per page, default 50. - region (string, optional): Trend region code, default "global" — the worldwide feed (preserves historical behavior). Supported today: "global", "us" (United States), "gb" (United Kingdom), "au" (Australia); more regions will be added over time — discover codes at runtime via GET /v1/trends/regions (free) rather than hard-coding. Unknown codes return a 400 listing the available regions. Response schema: ```json { "data": [ { "id": "uuid", "title": "Trends for Mar 17th", "region": "global", "local_date": "2026-03-17", "trends": [ { "id": "uuid", "trend_id": "uuid", "trend_group_id": "uuid", "ranking": 1, "detected_at": "2026-03-17T11:12:03Z", "last_seen_at": "2026-03-17T23:04:41Z", "origin_region_codes": null, "global_confidence": null, "momentum": { "status": "rising", "score": 0.74, "views_per_hour": 18400, "updated_at": "2026-03-17T22:00:00Z" }, "trend": { "id": "uuid", "name": "Trend name — human readable title", "description": "Detailed description of the trend with context and creator activity.", "trend_type": "content" } } ] } ] } ``` Notes: - Each region has its own curated source battery and runs three times per day at 7:00, 13:00 and 19:00 in its own local timezone; all runs for a region-local day merge into ONE trend group per region per day (re-detections dedupe, they don't duplicate). - detected_at is when the trend first entered its day container; last_seen_at is the last intra-day run that re-confirmed it. Compare them to tell brand-new trends from sustained ones. - origin_region_codes and global_confidence are only set on synthesis-promoted trends in the "global" region — trends independently detected in multiple country regions (e.g. ["gb","us"] with a 0-1 confidence). Native global detections carry null. - momentum is a live signal refreshed by a background loop (~every 2h, configurable per region) that re-reads only exemplar view/like counts — no re-clustering. status is one of new/rising/steady/fading; score is 0-1 heat; views_per_hour is the instantaneous exemplar growth. momentum is null until the first snapshot runs for a trend, at which point its lifecycle is seeded from discovery-time signals. Use GET /v1/trends/emerging for a pre-filtered, momentum-ranked view. ### GET /v1/trends/emerging What's emerging right now for a region: a flat, momentum-ranked list of early-stage trends (lifecycle status new or rising) from the current daily container. Composable with region (e.g. region=gb → "what's emerging in the UK right now"). Reads maintained momentum state (no inline scraping) so it responds fast. Cost: free (rate-limited per plan; intended for user-triggered, in-product use) Query parameters: - region (string, optional): Trend region code, default "global". Same codes as GET /v1/trends. - limit (integer, optional): Max emerging trends, ranked by momentum. Default 20 (1-50). - top_exemplars (integer, optional): Top N exemplar videos per trend (0-20). Default 5. Response schema: ```json { "region": "gb", "generated_at": "2026-03-17T22:05:00Z", "data": [ { "id": "uuid", "trend_id": "uuid", "ranking": 3, "region": "gb", "status": "rising", "momentum_score": 0.74, "views_per_hour": 18400, "detected_at": "2026-03-17T11:12:03Z", "last_seen_at": "2026-03-17T21:00:00Z", "exemplar_count": 42, "top_exemplars": [], "trend": { "id": "uuid", "name": "Trend name — human readable title", "description": "Detailed description of the trend with context and creator activity.", "trend_type": "content" } } ] } ``` Notes: - Only trends with status new or rising are returned, sorted by momentum_score DESC. Before the first momentum snapshot runs, status/score are seeded from discovery-time signals, so the endpoint is useful immediately. - Momentum is maintained by the background snapshot loop, so this is a cheap read of already-computed state — safe to call per user request. ### GET /v1/trends/digest Today's trends digest for a region (default "global"), based on the current date in the region's timezone. Cost: $0.25 Query parameters: - limit (integer): Results per page, default 50. - region (string, optional): Trend region code, default "global". Same codes as GET /v1/trends. Response schema: Same as GET /v1/trends. "Today" is resolved in the region's own timezone — at 9pm in London the gb digest still returns the UK's current day. ### GET /v1/trends/regions Lists trend regions available for the region parameter on /v1/trends and /v1/trends/digest. More regions (including sub-regions like major city hubs) will be added over time — poll this endpoint to discover them instead of hard-coding codes. Cost: free Response schema: ```json { "data": [ { "code": "global", "name": "Global", "timezone": "America/New_York" }, { "code": "us", "name": "United States", "timezone": "America/New_York" }, { "code": "gb", "name": "United Kingdom", "timezone": "Europe/London" }, { "code": "au", "name": "Australia", "timezone": "Australia/Sydney" } ] } ``` ### GET /v1/videos/digest Top performing videos from the last 48 hours across all platforms. Cost: $0.25 Query parameters: - limit (integer): 1-100, default 50. Response schema: ```json { "data": [ { "id": "uuid", "url": "https://www.tiktok.com/@user/video/123", "publish_date": "2026-03-17T00:08:41", "views": 6665489, "number_of_likes": 229785, "number_of_shares": 4102, "number_of_comments": 8088, "description": "Full video description with hashtags and text...", "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/uuid.jpg", "hashtags": ["donaldtrump", "funny"], "type": "tiktok", "niche": "unknown", "author_id": "uuid", "bookmarks": 22834, "external_id": "7618009747375017219", "region": "US", "duration": 30, "transcript_raw": "Full video transcript text if available, null otherwise", "is_eligible_for_commission": false, "is_duet": false, "is_stitch": false, "sound": null } ] } ``` Video data includes: URL, publish date, views (nullable), like/share/comment counts, bookmarks, full description, thumbnail, hashtags array (nullable), platform type (youtube/tiktok/instagram), niche classification, author reference (nullable), platform-native ID, region code (nullable), duration in seconds (nullable), raw transcript text when available (nullable), TikTok Shop commission eligibility (`is_eligible_for_commission`), TikTok `is_duet`/`is_stitch` flags, and an additive `sound` object (nullable). Transcript data is especially valuable for content analysis and keyword extraction. ### GET /v1/youtube/videos/digest, GET /v1/tiktok/videos/digest, GET /v1/instagram/videos/digest Platform-specific video digests. Same parameters and response schema as GET /v1/videos/digest, filtered to a single platform. Cost: $0.25 each. ### GET /v1/youtube/hashtags, GET /v1/tiktok/hashtags, GET /v1/instagram/hashtags Platform-specific hashtag statistics. Same parameters and response schema as GET /v1/hashtags, filtered to a single platform. Cost: $0.05 each. --- ## Content Research Agents (/v1/agents) — Unified API **This is the primary, recommended research API.** A Content Research Agent unifies keyword search (formerly Orbit) and recurring niche monitoring (formerly Comet) behind one resource. A single flag decides the shape: - `is_recurring: false` → one-shot run (the Orbit replacement). Runs once, bills once. - `is_recurring: true` → recurring agent (the Comet replacement). Runs on a cadence, bills per scheduled run. Agent IDs are interchangeable with the legacy Orbit/Comet id everywhere — every `/v1/agents/:id/*` sub-path is a superset of the old `/v1/orbit/:id/*` and `/v1/comet/:id/*` reads. Orbit and Comet are DEPRECATED and removed on **August 3, 2026**; migrate all new integrations to `/v1/agents`. **Collection scope is system-managed.** You do NOT set `min_views` or `time_range` at creation — the system collects broadly and continuously. Filter at read time on the videos/slideshows/ads endpoints (`min_views`, `start_date`, `end_date`, `platforms`, `order_by`). ### POST /v1/agents — Create an Agent Creates a one-shot or recurring research agent. Returns immediately with the agent plus the first run's `job_id` for polling. Cost: **$0.50 for one-shot** (`is_recurring: false`). **Recurring agents are free to create** (`is_recurring: true`) and bill per scheduled run. Add `data_intelligence_enabled: true` for +$1.00. Reads are always free. Request body (JSON): - is_recurring (boolean, required): `false` = one-shot (Orbit-style); `true` = recurring (Comet-style). - intent (string, required): One-sentence natural-language description of what you're really after (e.g. "find UGC-style skincare ads targeting Gen Z"). Powers intent_match filtering when intelligence is enabled. - keywords (string[], required): 1-50 keywords. Use specific multi-word phrases. Hashtag-style tokens are normalized (`#progressivehouse` == `progressive house`). - name (string, optional): Descriptive name for the agent. - platforms (string[], optional): Any of `youtube`, `tiktok`, `instagram`. Defaults to all three. - cadence (string, required if is_recurring=true): `daily`, `weekly`, `monthly`, or a valid cron expression that runs at most once per day. The named shortcuts are verified working. Rejected (400) if is_recurring=false. - exclude_keywords (string[], optional): Keywords to filter out of results. - exclude_keywords_strict (boolean, optional): Also match transcripts for exclusions. Default false. - meta_ads_enabled (boolean, optional): Collect related Meta ads. Default false. - data_intelligence_enabled (boolean, optional): Add 43 AI fields per video/slideshow. +$1.00. Default false. Cannot be added retroactively. - english_only (boolean, optional): Default `true` (English-only collection). Set `false` to collect content in EVERY language — the switch for non-English/global niches. When false, write `keywords` and `intent` in the target language (the keyword engine adapts to your input language). Applies to future runs on recurring agents; never re-filters already-collected content. Note: there is no `min_views` or `time_range` at create time — collection is system-managed; filter at read time. Response (201 Created): ```json { "data": { "id": "uuid", "name": "Agent name", "is_recurring": false, "intent": "…", "keywords": ["keyword1", "keyword2"], "platforms": ["youtube", "tiktok", "instagram"], "cadence": null, "meta_ads_enabled": false, "data_intelligence_enabled": false, "english_only": true, "autonomy_level": "suggest", "autopilot_unlocked": false, "cognition_enabled": false, "job_id": "uuid", "latest_run": { "id": "uuid", "status": "pending", "created_at": "ISO8601" } }, "message": "Agent created" } ``` ### GET /v1/agents — List Agents Cost: Free. Query parameters: `is_recurring` (boolean filter), `include_inactive` (boolean), `limit`, `page`. Response: ```json { "data": { "limit": 50, "page": 1, "count": 12, "agents": [ { "id": "uuid", "name": "…", "is_recurring": true, "keywords": ["…"], "platforms": ["tiktok"], "cadence": "weekly", "created_at": "ISO8601" } ] } } ``` ### GET /v1/agents/:id — Get Agent (Poll) Cost: Free. Returns the agent config plus `analysis` (short summary text), `analysis_data` (full structured object), `pending_jobs[]` (in-flight secondary jobs when not finalized), and `finalized` (boolean). **`finalized: true` is the canonical "this run won't change" signal** — `status: completed` alone can still have AI analysis in flight, so `null` analysis/intelligence means "not yet", not "no data". ```json { "data": { "id": "uuid", "name": "Agent name", "is_recurring": false, "status": "completed", "finalized": true, "keywords": ["…"], "platforms": ["youtube", "tiktok", "instagram"], "analysis": "Short summary text", "analysis_data": { "themes": [], "viral_tactics": [], "overview": {} }, "pending_jobs": [ { "type": "video_intelligence", "poll_url": "…", "result_path": "…", "webhook_event": "content_research_agent.run.completed", "retry_after_seconds": 60 } ] } } ``` ### GET /v1/agents/:id/videos — Get Paginated Videos Cost: Free. This is where you filter the system-managed collection. Query parameters: `page`, `limit`, `min_views`, `platforms` (comma-separated), `start_date`, `end_date`, `order_by` (`views` | `publish_date` | `created_at`), `sort`, `intent_match` (boolean — when the agent used intent + data_intelligence_enabled), `region` (ISO-3166-1 alpha-2 upload-country filter, case-insensitive, e.g. `US`/`CA`/`RU`/`AU` — BETA, see below). Response: ```json { "data": { "agent_id": "uuid", "agent_name": "Agent name", "total": 214, "limit": 50, "offset": 0, "videos": [ { "id": "uuid", "url": "https://www.tiktok.com/@user/video/123", "description": "Full description…", "platform": "tiktok", "views": 6665489, "likes": 229785, "shares": 4102, "comments": 8088, "bookmarks": 22834, "publish_date": "2026-03-17T00:08:41", "author": { "username": "user", "avatar_url": "url", "followers": 281560, "country": "US", "verified": false }, "hashtags": ["donaldtrump", "funny"], "thumbnail_url": "https://auth.virlo.ai/storage/v1/object/public/thumbnails/uuid.jpg", "keyword_found_by": "keyword1", "upload_region": "US", "upload_region_source": "tiktok_region", "intent_match": null, "intelligence": null, "intelligence_status": "disabled", "is_duet": false, "is_stitch": false, "sound": { } } ] } } ``` `intent_match` is present only when the agent used intent + data_intelligence_enabled. `intelligence` / `intelligence_status` (`ready` | `pending` | `disabled` | `failed` | `skipped`) are present when data_intelligence_enabled was set. `is_duet` / `is_stitch` are TikTok-only (null elsewhere). `sound` is the additive audio metadata object. Each video also carries `upload_region` — the resolved uploader country as an ISO-3166-1 alpha-2 code (e.g. `US`, `CA`, `RU`, `AU`), or `null` when unresolved. Pass `region=` (case-insensitive) to return only that country's uploads. Region detection is BETA: deterministic where the platform provides it (TikTok video/creator region, YouTube channel country) and AI-inferred otherwise, so coverage is partial and improving — items without a resolved region are excluded when you filter. Slideshows expose the same filter via their deterministic `region` field (highest coverage, straight from TikTok). ### GET /v1/agents/:id/slideshows — Get Slideshows (Image Carousels) Cost: Free. Same envelope and query parameters as /videos; items live under the `slideshows` key. Slideshow intelligence uses the per-panel schema (panel_texts[], panel_text_full, image_count, narrative_arc, text_density) instead of a transcript. ### GET /v1/agents/:id/ads — Get Meta Ads Cost: Free. Only populated when `meta_ads_enabled` was true. Same envelope; items under the `ads` key. `order_by` accepts `created_at` | `page_like_count`. ### GET /v1/agents/:id/creators/outliers — Get Creator Outliers Cost: Free. Under-followed creators consistently outperforming their follower baseline. Query parameters: `order_by` (`weighted_score` default | `rising`), `sort`, `platform`, `follower_tier` (`nano` | `micro` | `mid` | `macro`), `category` (topic substring). Response (same flat shape as Orbit/Comet — `author_id` / `creator_url` / `creator_avatar_url`; avatar URLs are always fetchable HTTPS): ```json { "data": { "agent_id": "uuid", "total": 4, "limit": 50, "offset": 0, "has_more": false, "outliers": [ { "author_id": "uuid", "creator_url": "https://www.tiktok.com/@user", "creator_avatar_url": "https://auth.virlo.ai/storage/v1/object/public/avatars/….jpg", "follower_count": 3200, "avg_views": 210000, "outlier_ratio": 65.6, "weighted_score": 33.7, "videos_analyzed": 12 } ] } } ``` ### GET /v1/agents/:id/sounds — Get Sounds Cost: Free. Query parameters: `sort` (`rising` | `growth_7d` | `video_count` | `usage_count`), `limit`, `page`. There is NO `lifecycle` query param (passing one returns 400) — each row carries a `lifecycle` label (`new` | `rising` | `steady` | `fading`); filter client-side. Response (this endpoint uses the `pagination` envelope, not offset/total inline). Each sound carries `id` (Virlo UUID) + `external_id` (platform-native), `title`, `platform`, `video_count`, `avg_views`, run-over-run `growth_video_count` / `growth_views`, and a `lifecycle` label: ```json { "data": [ { "id": "…", "external_id": "7247…", "title": "…", "platform": "tiktok", "video_count": 42, "avg_views": 612000, "growth_video_count": 18, "growth_views": 240000, "lifecycle": "rising" } ], "pagination": { "page": 1, "limit": 50, "total": 120, "total_pages": 3, "has_next_page": true, "has_prev_page": false } } ``` ### GET /v1/agents/:id/hashtags — Get Hashtags Cost: Free. Same `pagination` envelope as /sounds. Query parameters: `sort` (`volume` | `growth` | `avg_views`), `limit`, `page`. Returns per-hashtag video_count, total_views, avg_views, run-over-run growth, a lifecycle label, and top_creators[]. ### GET /v1/agents/:id/benchmarks — Genre Benchmarks Cost: Free. Genre norms bucketed by follower tier (nano/micro/mid/macro): median_engagement_rate, median_followers, median_videos_in_niche, median_posting_frequency_days. ### GET /v1/agents/:id/affinity — Genre Affinity (BETA) Cost: Free. BETA, directional. Genre adjacency from the corpus: creator_topics, related_hashtags, related_sounds. Not a follow-graph. ### GET /v1/agents/:id/creators/:creator_id/similar — Similar Creators (BETA) Cost: Free. BETA, directional. Creators ranked by shared hashtags + shared sounds (co-occurrence). Query parameter: `limit`. ### GET /v1/agents/:id/analysis/latest — Latest AI Analysis Cost: Free. The pre-computed research report for the most recent batch. ```json { "data": { "id": "uuid", "insight_type": "content_research_agent", "reference_id": "agent_id", "batch_start": "ISO8601", "batch_end": "ISO8601", "analysis": "Short summary text", "analysis_data": { "overview": {}, "key_highlight": "…", "themes": [], "viral_tactics": [], "top_10_breakdown": [], "timing_analysis": {}, "excluded_videos": [], "connecting_thread": "…" }, "video_count": 214, "analyzed_video_ids": ["uuid"], "model_used": "google/gemini-2.5-flash", "created_at": "ISO8601" } } ``` ### GET /v1/agents/:id/analysis — Analysis History Cost: Free. Paginated list of every completed analysis batch, newest first. Query parameters: `page`, `limit`, `start_date`, `end_date` (YYYY-MM-DD). Same item shape as `/analysis/latest`. Uses the `pagination` envelope. ### GET /v1/agents/:id/trends/latest — Latest AI-Detected Trends Cost: Free. ```json { "data": { "reference_id": "agent_id", "insight_type": "content_research_agent", "viral_insight_id": "uuid", "batch_start": "ISO8601", "batch_end": "ISO8601", "total": 3, "trends": [ { "id": "uuid", "rank": 1, "stable_key": "trend-slug", "name": "Trend name", "why_it_works": "Explanation", "tactics": ["tactic1"], "confidence": 0.91, "evidence_video_ids": ["uuid"], "video_count": 48, "total_views": 12500000, "total_likes": 410000, "total_comments": 28000, "total_shares": 4100, "avg_virality_score": 26.4, "platform_breakdown": { "tiktok": 40, "youtube": 8 }, "top_creators": [{ "username": "user", "video_count": 5 }], "peak_hour_utc": 21, "status": "rising", "first_seen_at": "ISO8601", "prev_video_count": 30, "prev_total_views": 8100000, "created_at": "ISO8601" } ] } } ``` Lifecycle `status`: `new` (just emerged — highest opportunity) | `rising` (act now) | `steady` | `fading` (avoid). `stable_key` lets you follow one trend across runs; `prev_*` give run-over-run deltas. ### GET /v1/agents/:id/trends — Trends History Cost: Free. Paginated list of every trend row for this agent, newest first. Query parameters: `page`, `limit`, `stable_key` (follow one trend across runs), `start_date`, `end_date`. Uses the `pagination` envelope. A trend moving `new → rising` with growing `video_count` is the strongest "post about this now" signal on recurring agents. ### GET /v1/agents/:id/runs — List Runs Cost: Free. Query parameters: `page`, `limit`. Each scheduled or one-shot execution is a run (status, finalized, videos_linked, outliers_identified, started_at, completed_at). ### GET /v1/agents/:id/runs/:run_id — Get Run Cost: Free. Single run by UUID. 404 if the run is not on this agent. ### PUT /v1/agents/:id — Update Agent Cost: Free. Update agent fields (name, keywords, platforms, cadence, exclude_keywords, meta_ads_enabled, etc.). Note: collection scope stays system-managed — you cannot set min_views/time_range here. ### DELETE /v1/agents/:id — Delete Agent Cost: Free. Returns 204. ### Autonomy (recurring agents) Recurring agents self-optimize by proposing keyword refreshes and filter changes. One-shot agents expose these fields but generate no proposals or activity. - GET /v1/agents/:id/activity — Free. Timeline of autonomy actions. - GET /v1/agents/:id/events — Free. Breaking events/stories the agent detected in its niche (recurring). Active (confirmed, unexpired) first, most salient first. Each: `title`, `summary`, `source` (`corpus_burst` | `news_scan`), `salience` (0-10), `status` (`candidate` | `confirmed` | `dismissed` | `expired`), `keywords`, `evidence` ([{ url, views, description }]), `detected_at`/`confirmed_at`/`expires_at`. Supports `limit` (max 100). - GET /v1/agents/:id/proposals — Free. Proposed changes. `status`: `pending` | `applied` | `auto_applied` | `dismissed` | `reverted`. `type`: `keyword_refresh` | `filter_change`. - POST /v1/agents/:id/proposals/:pid/apply — Free. Apply a proposal. The first manual apply unlocks autopilot for the team. - POST /v1/agents/:id/proposals/:pid/dismiss — Free. - POST /v1/agents/:id/proposals/:pid/revert — Free. - PUT /v1/agents/:id/autonomy — Free. Body: `{ "autonomy_level": "suggest" | "autopilot", "cognition_enabled": boolean }`. Autonomy defaults: `autonomy_level` defaults to `suggest`. But once a team unlocks autopilot (by manually applying a proposal once), newly created agents default to `autonomy_level: "autopilot"` with `autopilot_unlocked: true`. ### Webhook Subscribe to `content_research_agent.run.completed` (carries `is_recurring`) — the preferred event for both one-shot and recurring completions. Legacy `orbit.run.completed` / `comet.run.completed` still fire during the deprecation window. Recurring agents also emit `content_research_agent.event.detected` between runs when they confirm a breaking event in their niche (payload: title/summary/salience/keywords_added/evidence/action_taken) — the push companion to GET /v1/agents/:id/events. --- ### POST /v1/orbit — Queue Keyword Search > **DEPRECATED — removed August 3, 2026.** Migrate to `POST /v1/agents` with `is_recurring: false`. Agent and Orbit IDs are interchangeable; every `/v1/orbit/:id/*` read has an identical `/v1/agents/:id/*` equivalent. The Orbit endpoints below remain functional only during the deprecation window. Queues an asynchronous keyword-based video discovery job across YouTube, TikTok, and/or Instagram. Returns immediately with an orbit_id for polling. Cost: $0.50 Request body (JSON): - name (string, required): Descriptive name for this search. - keywords (string[], required): 1-10 keywords to search for. Use specific multi-word phrases for best results (e.g., "jeep wrangler mods" not "jeep"). - time_period (string, required): "today", "this_week", "this_month", "this_year". - platforms (string[]): ["youtube", "tiktok", "instagram"]. Defaults to all. - min_views (integer): Minimum view count threshold. Default 0. - enable_meta_ads (boolean): Collect Meta ads related to keywords. Default false. - data_intelligence_enabled (boolean): Enable Data Intelligence to get 43 AI-analyzed fields per video. Adds $1.00 to total cost ($1.50 total). Default false. See "Data Intelligence" section for details. - english_only (boolean): Default true (English-only). Set false to collect content in EVERY language — write keywords + intent in the target language when you do. Filter results by upload country with the region param on GET /v1/orbit/:orbit_id/videos (and /slideshows). - run_analysis (boolean): DEPRECATED — analysis is now always generated for every Orbit at no extra cost. This parameter is accepted for backwards compatibility but has no effect. Use GET /v1/orbit/:orbit_id/analysis/latest and GET /v1/orbit/:orbit_id/trends/latest to retrieve AI insights. - exclude_keywords (string[]): Keywords to filter out from results. - exclude_keywords_strict (boolean): Also check video transcripts for exclusions. Default false. Response: ```json { "data": { "orbit_id": "d18109f3-e933-4c06-a4e6-9c0c05b276ac", "status": "queued", "message": "Keyword search job queued with max priority." } } ``` ### GET /v1/orbit — List Searches List all Orbit searches for this account. Paginated. Cost: Free Query parameters: limit, page. Response includes array of orbits with: id, name, keywords, platforms, status, totalVideos, createdAt, executionTimeMs. ### GET /v1/orbit/:orbit_id — Get Search Results (Poll) Poll for status and retrieve results when complete. When status is "processing", results is null. When "completed", results contains the full dataset. Cost: Free The response includes an "analysis" field with a short human-readable summary and "analysis_data" with the full structured object. For complete AI analysis and trends, use the dedicated sub-endpoints: GET /v1/orbit/:orbit_id/analysis/latest and GET /v1/orbit/:orbit_id/trends/latest. Response when completed: ```json { "data": { "id": "uuid", "status": "completed", "name": "Search name", "keywords": ["keyword1", "keyword2"], "analysis": "# Markdown intelligence report...\n\n## Top Trends\n\n...", "results": { "total_videos": 46, "youtube_count": 7, "tiktok_count": 37, "instagram_count": 2, "videos": [...], "ads": [...], "keyword_breakdown": [ { "keyword": "keyword1", "tiktokCount": 21, "youtubeCount": 4, "instagramCount": 0, "metaAdsCount": 0, "videosInserted": 25 } ], "execution_time_ms": 201128 }, "started_at": "2026-03-17T17:27:01.348745" } } ``` ### GET /v1/orbit/:orbit_id/videos — Get Paginated Videos Cost: Free Query parameters: limit, page, min_views, platforms (comma-separated), start_date, end_date, order_by (publish_date/views/created_at), sort, intent_match (boolean — filter by intent match when search used intent + data_intelligence_enabled), region (ISO-3166-1 alpha-2 upload-country filter, case-insensitive — BETA; each video carries upload_region, slideshows carry a deterministic region with the highest coverage). Each video includes: id, url, description, platform, views, likes, shares, comments, bookmarks, publish_date, author (username, verified, followers, avatar_url), hashtags, thumbnail_url, keyword_found_by, duration, region, is_duet (TikTok only, null for others), is_stitch (TikTok only, null for others). ### GET /v1/orbit/:orbit_id/slideshows — Get Slideshows (Image Carousels) Cost: Free Query parameters: limit, page, order_by, sort. Slideshows are TikTok image carousels discovered alongside videos. Each slideshow includes: url, description, platform, views, likes, shares, comments, bookmarks, publish_date, hashtags, thumbnail_url, images (array of { image_url, position }), author (username, verified, followers, avatar_url), keyword_found_by, is_eligible_for_commission, region. When the parent orbit was queued with data_intelligence_enabled=true, each slideshow also carries `intelligence_status` (ready | pending | disabled | failed | skipped) plus an `intelligence` payload. Slideshow intelligence uses a separate schema from video intelligence — it analyses per-panel text (panel_texts[], panel_text_full, panel_text_word_count, panel_text_character_count, image_count, narrative_arc, text_density) instead of a transcript, and drops video-only fields like visual_format, camera_perspective, scene_changed, transcript_*. See /docs/intelligence#slideshow-intelligence for the full schema. ### GET /v1/orbit/:orbit_id/ads — Get Meta Ads Cost: Free. Only returns results if enable_meta_ads was true when queuing. Each ad includes: id, ad_archive_id, page_id, page_profile_url, is_active, start_date, end_date, url (Facebook Ad Library link), caption, body, cta_type, page_like_count, title, video_url, keyword_found_by. ### GET /v1/orbit/:orbit_id/creators/outliers — Get Creator Outliers Cost: Free Creators significantly outperforming their follower count. Each outlier includes: creator_url, creator_avatar_url, follower_count, avg_views, outlier_ratio (views/followers), videos_analyzed, creator_topics, matching_topics, platform, identified_at, and videos array with full video data. ### GET /v1/orbit/:orbit_id/analysis/latest — Get Latest AI Analysis Cost: Free Returns the most recent AI-generated analysis for this Orbit search. Includes structured themes with confidence scores, viral tactics, timing analysis, and a human-readable overview. The analysis is automatically generated after job completion at no extra cost. Response schema: ```json { "data": { "id": "uuid", "insight_type": "orbit", "reference_id": "orbit_id", "batch_start": "ISO8601", "batch_end": "ISO8601", "analysis": "Short summary text", "analysis_data": { "themes": [ { "stable_key": "theme-slug", "name": "Theme Name", "why_it_works": "Explanation of why this content pattern resonates", "video_count": 48, "evidence_video_ids": ["vid-001", "vid-002"], "tactics": ["tactic1", "tactic2"], "confidence": 0.91 } ], "viral_tactics": ["tactic1", "tactic2"], "timing_analysis": { "peak_hours": [19, 21], "pattern": "evening spike" }, "overview": { "total_videos": 347, "platforms": { "tiktok": 310, "youtube": 37 } } } } } ``` ### GET /v1/orbit/:orbit_id/analysis — Get Analysis History Cost: Free. Paginated list of all AI analyses generated for this Orbit. Query parameters: page, limit. ### GET /v1/orbit/:orbit_id/trends/latest — Get Latest AI-Detected Trends Cost: Free Returns the most recent AI-detected trend themes for this Orbit search. Each theme includes view counts, video counts, tactics, and confidence scores. Response schema: ```json { "data": { "orbit_id": "uuid", "orbit_name": "Search Name", "themes": [ { "stable_key": "theme-slug", "name": "Theme Name", "why_it_works": "Explanation", "video_count": 48, "total_views": 12500000, "avg_views": 260416, "evidence_video_ids": ["vid-001"], "tactics": ["tactic1"], "confidence": 0.91 } ], "aggregate_stats": { "total_themes": 3, "total_videos_across_themes": 112, "total_views_across_themes": 27600000 } } } ``` ### GET /v1/orbit/:orbit_id/trends — Get Trends History Cost: Free. Paginated list of trend snapshots. Query parameters: limit. --- ### POST /v1/comet — Create Custom Niche > **DEPRECATED — removed August 3, 2026.** Migrate to `POST /v1/agents` with `is_recurring: true`. Agent and Comet IDs are interchangeable; every `/v1/comet/:id/*` read has an identical `/v1/agents/:id/*` equivalent. The Comet endpoints below remain functional only during the deprecation window. Creates an automated monitoring configuration that runs on a recurring cadence. Cost: $0.50 Request body (JSON): - name (string, required): Name for this niche monitor. - keywords (string[], required): 1-20 keywords to track. - platforms (string[], required): At least one of: ["youtube", "tiktok", "instagram"]. - cadence (string, required): "daily", "weekly", "monthly", or a valid cron expression that runs at most once per day. - min_views (integer, required): Minimum views threshold (0 or higher). - time_range (string, required): "today", "this_week", "this_month", "this_year". - is_active (boolean): Whether monitoring is active. Default true. - meta_ads_enabled (boolean): Track Meta ads. Default false. - data_intelligence_enabled (boolean): Enable Data Intelligence to get 43 AI-analyzed fields per video. Adds $1.00 to total cost ($1.50 total). Default false. See "Data Intelligence" section for details. - english_only (boolean): Default true (English-only). Set false to monitor content in EVERY language — write keywords + intent in the target language when you do. Applies to future scheduled runs. Filter by upload country with the region param on GET /v1/comet/:id/videos (and /slideshows). - exclude_keywords (string[]): Keywords to exclude. - exclude_keywords_strict (boolean): Check transcripts for exclusions. ### GET /v1/comet — List Configurations Cost: Free. Returns all Comet configs with: id, name, keywords, platforms, cadence, min_views, time_range, is_active, meta_ads_enabled, last_run_at, next_run_at. ### GET /v1/comet/:id — Get Configuration Cost: Free. ### PUT /v1/comet/:id — Update Configuration Update any field. Same body as POST. ### DELETE /v1/comet/:id — Delete Configuration Soft delete. Returns 204. ### GET /v1/comet/:id/videos — Get Comet Videos Cost: Free. Same video schema as Orbit videos (includes is_duet/is_stitch for TikTok, plus upload_region). Paginated. Supports intent_match and region (ISO-3166-1 alpha-2 upload-country filter, case-insensitive — BETA) query parameters. ### GET /v1/comet/:id/slideshows — Get Comet Slideshows Cost: Free. Same slideshow schema as Orbit slideshows (including the same per-slideshow `intelligence_status` + `intelligence` payload when the parent comet was created with data_intelligence_enabled=true). Paginated. ### GET /v1/comet/:id/ads — Get Comet Meta Ads Cost: Free. Same ad schema as Orbit ads. ### GET /v1/comet/:id/creators/outliers — Get Comet Creator Outliers Cost: Free. Same schema as Orbit creator outliers. ### GET /v1/comet/:id/analysis/latest — Get Latest Comet AI Analysis Cost: Free. Same schema as Orbit analysis. Auto-generated after each Comet run. ### GET /v1/comet/:id/analysis — Get Comet Analysis History Cost: Free. Paginated list of AI analyses for this Comet. Query parameters: page, limit. ### GET /v1/comet/:id/trends/latest — Get Latest Comet AI Trends Cost: Free. Same schema as Orbit trends. Auto-generated after each Comet run. ### GET /v1/comet/:id/trends — Get Comet Trends History Cost: Free. Paginated list of trend snapshots. Query parameters: limit. --- ### GET /v1/satellite/creator/:platform/:username — Start Creator Lookup Initiates an asynchronous creator profile analysis. Cost: $0.50 base. `trend_analysis=true` adds a $0.50 surcharge ($1.00 total) and forces a deep fetch (`max_videos` floored to 100), implicitly returning the `videos[]` payload. The audience surcharge ($0.50, cache-aware) can stack on top — all three optional surcharges (audience demographics, audience geography, trend analysis) ride on a single call. Rate limits: 5/min, 100/hour, 1,000/day. Path parameters: - platform (string, required): "tiktok", "youtube", or "instagram". - username (string, required): Creator's username on the platform. Query parameters: - include (string): Comma-separated. Options: "videos", "outliers". Include both for maximum insight. Implicitly forced to include "videos" when trend_analysis=true. - cross_links (boolean): Enable cross-platform link discovery. Finds the same creator on other platforms (YouTube, TikTok, Instagram, Twitter/X, Spotify). Default false. - max_videos (integer): 1-100, how many recent videos to analyze. Floored to 100 when trend_analysis=true. - outlier_threshold (number): Multiplier for outlier detection (default 2). - audience_demographics (boolean): Include engaged-audience age + gender + language distributions (derived from analyzing the creator's commenters, not the raw follower base; see `data_source` in the response). Adds $0.50 surcharge on cache miss (free on cache hits). Default false. - audience_geography (boolean): Include engaged-audience country + city distributions. Shares the $0.50 surcharge with audience_demographics. Default false. - freshness_days (integer, 0-365): Max age of an acceptable cached audience snapshot. Default 30. - trend_analysis (boolean): Run AI trend detection over this creator's body of work. Forces a deep fetch (100 videos), implicitly returns videos[], and adds $0.50 surcharge. Returns a `trends` block with summary + per-trend time_windows[], resurged, momentum, and evidence_video_ids that map back to videos[] in the same response. Persisted with the run — re-reading via GET /v1/satellite/runs/:run_id is free. Default false. Response: ```json { "data": { "job_id": "uuid", "status": "processing" } } ``` ### GET /v1/satellite/creator/status/:job_id — Poll Creator Lookup Cost: Free Response when completed: ```json { "data": { "status": "completed", "result": { "username": "creator_name", "platform": "tiktok", "profile": { "followers": 481648, "following": 0, "avatar_url": "url", "url": "https://www.tiktok.com/@creator_name", "description": "Bio text or null", "is_verified": null, "total_videos": 166, "total_likes": 6045845, "total_views": null, "total_posts": null, "category": "comedy", "bio_link": "https://linktr.ee/creator_name" }, "content_tags": ["silent-comedy", "life-hacks", "reaction"], "stats": { "videos_analyzed": 5, "avg_views": 1969309, "median_views": 341616, "avg_likes": 70773, "avg_comments": 2829, "engagement_rate": 0.04, "top_performing_views": 8375353, "lowest_performing_views": 49430, "posting_frequency_days": 0.8 }, "hashtags": [ { "hashtag": "usa", "total_views": 9846545, "avg_views": 1969309, "avg_likes": 70773, "avg_comments": 2829, "used_count": 5 } ], "videos": [ { "id": "video_id", "title": "Video title", "description": "Full description", "views": 8375353, "likes": 283565, "comments": 9868, "publish_date": "2026-03-17T00:08:41.000Z", "url": "https://www.tiktok.com/@user/video/id", "hashtags": ["tag1", "tag2"], "thumbnail_url": "url", "outlier_ratio": 24.52 } ], "outliers": { "threshold_multiplier": 2, "outlier_videos": [ { "id": "video_id", "views": 8375353, "outlier_ratio": 24.52 } ] }, "cross_links": { "discovered": [ { "platform": "youtube", "username": "creator_yt", "url": "https://www.youtube.com/@creator_yt", "source": "linktree" }, { "platform": "instagram", "username": "creator_ig", "url": "https://www.instagram.com/creator_ig", "source": "bio_text" }, { "platform": "spotify", "username": "Creator Name", "url": "https://open.spotify.com/artist/abc123", "source": "spotify_api" } ], "bio_link_url": "https://linktr.ee/creator_name", "sources_checked": ["structured_fields", "bio_text", "linktree", "spotify_api", "ai_search"] }, "trends": { "analyzed": true, "summary": "Brief read of the creator's overall playbook (only present when trend_analysis=true).", "status": "ok", "model_used": "google/gemini-2.5-flash", "cost_usd": 0.0123, "tokens_used": 18234, "trends": [ { "name": "Short trend label (format/hook/era/pillar)", "description": "1-2 sentence explanation of the pattern.", "tactics": ["concrete tactic 1", "concrete tactic 2"], "confidence": 0.85, "video_count": 6, "evidence_video_ids": ["video_id_1", "video_id_2"], "time_windows": [ { "start_date": "2025-08-12", "end_date": "2025-11-04", "video_count": 3, "total_views": 12400000, "total_likes": 410000, "total_comments": 28000, "total_shares": 4100, "avg_views": 4133333, "description": "Original wave" }, { "start_date": "2026-02-09", "end_date": "2026-04-21", "video_count": 3, "total_views": 19800000, "total_likes": 620000, "total_comments": 41000, "total_shares": 7200, "avg_views": 6600000, "description": "Resurgence wave" } ], "resurged": true, "momentum": "stronger" } ] }, "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "credits_charged": 100 } } } ``` The `trends` block is only present when the request set `trend_analysis=true`. When the corpus has fewer than 10 videos, `trends.status` is `"insufficient_corpus"` and `trends.trends` is an empty array. `evidence_video_ids` always map back to entries in the same response's `videos[]` array — no second request needed. Time windows are computed mechanically from real publish dates after the LLM picks IDs, so date hallucination is impossible. `resurged: true` means the trend has ≥2 disjoint time_windows (the creator did something, dropped it, then came back). `momentum` compares the latest window's avg_views to the prior window (`stronger` | `weaker` | `similar`, ±15% deadband); `null` for single-window trends. Results expire after 24 hours from the poll cache, but the run is persisted indefinitely — re-read for free via GET /v1/satellite/runs/:run_id. ### POST /v1/satellite/creators/batch — Batch Creator Lookup Submit multiple creators for parallel lookup. Returns a batch_id and per-creator job_ids. Cost: $0.50 per creator. Rate limits: same as individual lookups. Request body (JSON): - creators (array, required): Array of { platform, username } objects. Max 25 per request. - include (string, optional): Comma-separated list of optional data to include ("videos", "outliers", or "videos,outliers"). - cross_links (boolean, optional): Enable cross-platform link discovery for each creator. Default false. - max_videos (integer, optional): Maximum videos to return per creator (default 30, max 100). - outlier_threshold (number, optional): Outlier sensitivity multiplier (default 2.0). Response (202 Accepted): ```json { "data": { "batch_id": "uuid", "status": "processing", "total": 3, "completed": 0, "failed": 0, "processing": 3, "creators": [ { "platform": "tiktok", "username": "creator1", "job_id": "uuid-1", "status": "processing" }, { "platform": "youtube", "username": "creator2", "job_id": "uuid-2", "status": "processing" }, { "platform": "instagram", "username": "creator3", "job_id": "uuid-3", "status": "processing" } ] } } ``` ### GET /v1/satellite/creators/batch/:batch_id — Poll Batch Status Poll the aggregate status of a batch. Returns current status of each creator's lookup job. Cost: Free Path parameters: - batch_id (string, required): The batch_id returned from the batch lookup endpoint. Response: ```json { "data": { "batch_id": "uuid", "status": "partial", "total": 3, "completed": 2, "failed": 0, "processing": 1, "creators": [ { "platform": "tiktok", "username": "creator1", "job_id": "uuid-1", "status": "completed" }, { "platform": "youtube", "username": "creator2", "job_id": "uuid-2", "status": "completed" }, { "platform": "instagram", "username": "creator3", "job_id": "uuid-3", "status": "processing" } ] } } ``` Batch status values: "processing" (all still running), "partial" (some completed/failed), "completed" (all done), "failed" (all failed). Individual creator results can be retrieved via GET /v1/satellite/creator/status/:job_id once their status is "completed". Batch results expire after 24 hours. ### POST /v1/satellite/video-outlier — Start Video Outlier Analysis Analyzes how a specific video performs relative to its creator's typical content. Cost: $0.50. Rate limits: 5/min, 100/hour, 1,000/day. Request body (JSON): - url (string, required): Full URL of the video. - platform (string, required): "tiktok", "youtube", or "instagram". Response: ```json { "data": { "job_id": "uuid", "status": "processing" } } ``` ### GET /v1/satellite/video-outlier/status/:job_id — Poll Video Outlier Cost: Free Response when completed: ```json { "data": { "status": "completed", "result": { "video": { "url": "video_url", "title": "Video title", "views": 8375590, "likes": 283578, "comments": 9871, "publish_date": "2026-03-17T00:08:41.000Z" }, "creator": { "username": "creator_name", "platform": "tiktok", "profile": { "followers": 481652, "total_videos": 166, "total_likes": 6045870 } }, "analysis": { "videos_analyzed": 98, "median_views": 416241, "avg_views": 938324, "outlier_score": 20.12, "percentile": 98.98, "std_deviation": 1389310, "z_score": 5.35, "performance_label": "mega_viral" } } } } ``` Performance labels (bucketed against the creator's OWN distribution, not a global threshold): "average", "above_average", "viral", "mega_viral". Status poll cache lasts 24 hours. Video-outlier results are NOT yet persisted to the durable runs ledger — store within 24h or re-run. ### GET /v1/satellite/sounds/:platform/:music_id — Start Sound Lookup (TikTok + Instagram) Initiates an asynchronous sound lookup. Fetches every video/reel using a specific sound, computes aggregate stats, and (optionally) runs LLM-based trend detection. Cost: $0.50 base. `trend_analysis=true` adds a $0.50 surcharge ($1.00 total) and forces a deeper fetch (~300 videos), ignoring `max_videos`. Supported platforms: `tiktok` and `instagram`. YouTube is not supported (no public "videos using this sound" feed) — a YouTube call 400s before any credit is charged. If you pass an internal Virlo sound UUID (from /v1/orbit/:id/sounds or /v1/comet/:id/sounds) it is auto-resolved to the sound's platform + external_id; passing the wrong platform for a known external_id is auto-corrected too. Instagram caveat: the IG audio feed exposes no authoritative total-usage count, so `reported_usage_count` is null and a few per-video fields (shares, collects, region, duet/stitch) come back 0/null. Path parameters: - platform (string, required): "tiktok" or "instagram". - music_id (string, required): The sound's platform-native external_id (TikTok music/clip ID, or Instagram audio id). Pull from /v1/sounds, /v1/orbit/:id/sounds, or /v1/comet/:id/sounds (the `external_id` field). Passing the Virlo UUID `id` also works — it's auto-resolved. Query parameters: - trend_analysis (boolean): When true, runs LLM trend detection over ~300 videos. Adds $0.50 surcharge. Default false. - max_videos (integer, 1-100, default 50): Ignored when trend_analysis=true. Response: ```json { "data": { "job_id": "uuid", "status": "processing" } } ``` ### GET /v1/satellite/sounds/status/:job_id — Poll Sound Lookup Cost: Free. Response when completed (trend_analysis=true): ```json { "data": { "status": "completed", "run_id": "uuid", "result": { "request": { "music_id": "7570...", "platform": "tiktok", "trend_analysis": true, "max_videos": 300 }, "sound": { "music_id": "7570...", "platform": "tiktok", "title": "david - Drake", "is_original": true, "is_commerce_music": false, "duration_seconds": 47, "cover_url": "url", "owner": { "handle": "aubreygraham", "nickname": "Drake" }, "reported_usage_count": 412380 }, "data_captured_at": "2026-05-26T17:32:11Z", "stats": { "videos_analyzed": 287, "views": { "total": 1280000000, "avg": 4460000, "median": 320000, "max": 84000000, "min": 1200 }, "engagement": { "total_likes": 96000000, "avg_likes": 334000, "engagement_rate": 0.078 }, "velocity": { "videos_per_week": [{ "iso_week": "2026-W18", "count": 42 }], "last_4w_avg_videos_per_week": 37.5, "prior_4w_avg_videos_per_week": 21.2, "is_accelerating": true }, "verified_creator_count": 14, "verified_creator_pct": 0.049, "commerce_music_pct": 0, "duration_distribution": { "under_15s": 92, "between_15_30s": 118, "between_30_60s": 71, "over_60s": 6 }, "top_creators": [{ "unique_id": "khaby.lame", "video_count": 1, "total_views": 84000000, "avg_views": 84000000 }], "top_hashtags": [{ "hashtag": "drake", "used_count": 91, "total_views": 420000000, "avg_views": 4615384 }], "top_video": { "id": "7234...", "url": "url", "views": 84000000 } }, "sample_quality": { "truncated_by_cap": false, "pages_fetched": 14, "note": "deep_corpus" }, "trends": { "analyzed": true, "status": "ok", "summary": "Plain-language overview of what's happening on the sound.", "trends": [ { "name": "Cinematic montage edits", "description": "Slow-motion personal milestone montages set to the bridge.", "tactics": ["slow-mo personal footage", "text-overlay timeline"], "confidence": 0.86, "video_count": 64, "evidence_video_ids": ["7234...", "7245..."], "time_windows": [ { "start_date": "2026-04-08", "end_date": "2026-04-22", "video_count": 24, "total_views": 38000000, "avg_views": 1583000, "description": "First wave." }, { "start_date": "2026-05-09", "end_date": "2026-05-23", "video_count": 40, "total_views": 92000000, "avg_views": 2300000, "description": "Resurgence." } ], "resurged": true, "momentum": "stronger" } ], "model_used": "google/gemini-2.5-flash", "cost_usd": 0.0042, "tokens_used": 18420 }, "videos": [] } } } ``` CRITICAL TREND SEMANTICS: - `time_windows` is mechanically computed from the real `publish_date` of evidence videos. NO LLM date hallucination is possible. - `resurged: true` means the trend has ≥2 disjoint time windows (it faded and came back). - `momentum` compares the latest window's avg_views vs the prior window's, with a ±15% deadband. Null for single-window trends. - `trends.status: "insufficient_corpus"` means the sample was too small to be meaningful — the $0.50 surcharge is NOT billed in that case. - Trends are only generated when `sample_quality.note` ≥ `small_corpus_workable`. Results expire after 24 hours, but every completed run is also persisted as a durable satellite_run — re-read for free indefinitely via GET /v1/satellite/runs/:run_id. ### GET /v1/satellite/hashtags/:platform/:hashtag — Start Hashtag Lookup (TikTok + Instagram + YouTube) Initiates an asynchronous hashtag lookup. Fetches recent videos posting under a hashtag, computes aggregate stats, and (optionally) runs LLM-based trend detection. Cost: $0.50 base. `trend_analysis=true` adds a $0.50 surcharge ($1.00 total — the same shared trend-analysis surcharge as creator and sound lookups) and forces a ~300-video deep fetch, ignoring `max_videos`. `depth=deep` (~300 videos) adds a $0.50 surcharge — WAIVED when `trend_analysis=true`, since trends already include the ~300-video fetch (deep+trends = $1.00 total, same as trends alone). `depth=full` (~500 videos) adds a $1.50 surcharge ($2.00 alone; $2.50 with trend_analysis=true). The completed result's `credits_charged` echoes the final price: 50 standard, 100 deep or trends, 200 full, 250 full+trends. Repeating the same lookup within 6 hours returns the cached run for FREE — the acknowledgment carries `cached: true`. Invalid input 400s before any credit is charged. Platform coverage (field gaps documented honestly): - `tiktok` — native challenge feed (fullest metrics). - `instagram` — Google-indexed public reels; best-effort coverage, upstream depth capped at ~11 pages (shallower than the other two). `shares`/`collects` come back 0, `is_duet`/`is_stitch` false, `region` null. - `youtube` — native hashtag page, Shorts only. Each Short is enriched via Virlo's video-details pipeline: exact views, `likes`, `comments`, `publish_date`, `duration_seconds`, channel `follower_count`, and sound attribution (`top_sounds` works on YouTube). `shares`/`collects` stay 0 (no public counts), `author` is the channel (`unique_id` = channel id), `region` null. Un-enrichable Shorts degrade to views + title only. Never compare `engagement_rate` across platforms. Path parameters: - platform (string, required): "tiktok", "instagram", or "youtube". - hashtag (string, required): The hashtag, with or without the leading "#" (URL-encode it as %23). Normalized to lowercase. Must be a single tag — no spaces, max 100 chars. Invalid input → 400, never charged. Query parameters: - trend_analysis (boolean): When true, runs LLM trend detection over a ~300-video deep fetch. Adds $0.50 surcharge. Default false. - depth (string): "standard" (default), "deep", or "full" — same tier shape as tracking's post collection. standard collects `max_videos` videos; deep collects ~300 videos (+$0.50 surcharge, WAIVED when trend_analysis=true since trends already include the ~300-video fetch); full collects ~500 videos (+$1.50 surcharge). deep and full OVERRIDE `max_videos` (it is ignored on those tiers). TikTok and YouTube support all three tiers; Instagram supports depth=standard ONLY (its Google-indexed feed caps at ~11 pages) — depth=deep|full on instagram returns 400 BEFORE billing, never charged. The completed result echoes `depth` in `request`, with `max_videos` as the EFFECTIVE target (300 deep, 500 full). - max_videos (integer, 1-100, default 50): Ignored when trend_analysis=true or depth=deep|full. - sort (string): "top" (default — views desc) or "recent" (publish date desc). The platforms expose no server-side sort on hashtag feeds, so ordering is applied to the collected corpus; stats are order-independent. Response: ```json { "data": { "job_id": "uuid", "status": "processing" } } ``` ### GET /v1/satellite/hashtags/status/:job_id — Poll Hashtag Lookup Cost: Free. Response when completed (trend_analysis=true): ```json { "data": { "status": "completed", "result": { "run_id": "uuid", "request": { "hashtag": "booktok", "platform": "tiktok", "trend_analysis": true, "depth": "standard", "max_videos": 300, "sort": "top" }, "hashtag": { "name": "booktok", "platform": "tiktok", "page_url": "https://www.tiktok.com/tag/booktok" }, "data_captured_at": "2026-08-02T14:32:11Z", "credits_charged": 100, "stats": { "videos_analyzed": 291, "views": { "total": 962000000, "avg": 3305841, "median": 214000, "max": 61000000, "min": 900 }, "engagement": { "total_likes": 71000000, "avg_likes": 244000, "total_comments": 2900000, "avg_comments": 9965, "total_shares": 4100000, "avg_shares": 14089, "total_collects": 1800000, "avg_collects": 6185, "engagement_rate": 0.082 }, "velocity": { "videos_per_week": [{ "iso_week": "2026-W30", "count": 38 }], "last_4w_avg_videos_per_week": 33.5, "prior_4w_avg_videos_per_week": 24.0, "is_accelerating": true }, "verified_creator_count": 11, "verified_creator_pct": 0.038, "duration_distribution": { "under_15s": 84, "between_15_30s": 121, "between_30_60s": 77, "over_60s": 9 }, "top_creators": [{ "unique_id": "booklover", "video_count": 4, "total_views": 61000000, "avg_views": 15250000 }], "related_hashtags": [{ "hashtag": "readingchallenge", "used_count": 74, "total_views": 310000000, "avg_views": 4189189 }], "top_sounds": [{ "external_id": "7570...", "title": "original sound", "artist": "booklover", "is_original": true, "used_count": 22, "total_views": 120000000, "avg_views": 5454545 }], "top_video": { "id": "7234...", "url": "url", "views": 61000000 } }, "sample_quality": { "truncated_by_cap": false, "pages_fetched": 12, "note": "deep_corpus" }, "trends": { "analyzed": true, "status": "ok", "summary": "Plain-language overview of what's happening under the hashtag.", "trends": [ { "name": "Annotated-page flip-throughs", "description": "Close-up flip-throughs of heavily annotated pages with a voiceover verdict.", "tactics": ["tab-color legend on screen", "end on the star rating"], "confidence": 0.84, "video_count": 52, "evidence_video_ids": ["7234...", "7245..."], "time_windows": [ { "start_date": "2026-06-10", "end_date": "2026-06-24", "video_count": 19, "total_views": 24000000, "avg_views": 1263000, "description": "First wave." }, { "start_date": "2026-07-14", "end_date": "2026-07-28", "video_count": 33, "total_views": 58000000, "avg_views": 1757000, "description": "Resurgence." } ], "resurged": true, "momentum": "stronger" } ], "model_used": "google/gemini-2.5-flash", "cost_usd": 0.0044, "tokens_used": 18930 }, "videos": [ { "id": "7234...", "url": "https://www.tiktok.com/@booklover/video/7234...", "description": "annotating my summer tbr", "publish_date": "2026-07-21T16:04:12.000Z", "duration_seconds": 27, "views": 61000000, "likes": 4100000, "comments": 61000, "shares": 210000, "collects": 98000, "hashtags": ["booktok", "readingchallenge"], "is_duet": false, "is_stitch": false, "region": "US", "author": { "unique_id": "booklover", "nickname": "Book Lover", "is_verified": false, "follower_count": 412000 } } ] } } } ``` Only the first `videos[]` entry is shown above — a trend_analysis run returns up to ~300, in the requested `sort` order, using the same per-video schema as sound lookups. Notes: - `related_hashtags` lists up to 20 co-occurring tags; the looked-up tag itself is excluded. `top_creators` and `top_sounds` list up to 10 each. - `velocity.videos_per_week` uses ISO-week buckets (max 26 weeks); `is_accelerating` compares the last-4-week average against the prior-4-week average with a ±15% deadband. - `sample_quality.note` is one of `insufficient_corpus` | `small_corpus_workable` | `healthy_corpus` | `deep_corpus`. - The `trends` block has the identical shape and semantics as the sound lookup's (see CRITICAL TREND SEMANTICS above): `time_windows` are computed mechanically from real publish dates — no LLM date hallucination possible. `trends.status` is `ok` | `insufficient_corpus` | `skipped`. - Per-platform field gaps: YouTube results are detail-enriched (exact views, likes, comments, publish dates, durations, channel follower counts, sound attribution) but carry 0 `shares`/`collects` (no public counts) and null `region`, with the channel as `author` (`unique_id` = channel id); Instagram results carry 0 `shares`/`collects`, false `is_duet`/`is_stitch`, and null `region`. Never compare `engagement_rate` across platforms. Results expire after 24 hours from the poll cache, but every completed run is persisted as a durable satellite_run with `type: "hashtag_lookup"` — re-read for free indefinitely via GET /v1/satellite/runs/:run_id. --- ## Satellite Runs — Durable Resource Model Every Satellite job (creator, sound, hashtag, video, batch) is persisted as a `satellite_run` row owned by your team. You pay once when the run is created; reads are free forever. This is the "pay once, read forever" contract. ### GET /v1/satellite/runs/:run_id — Re-read a Past Run Cost: Free. Returns the full persisted result plus metadata (type, platform, status, created_at, completed_at, credits_used). Works across all satellite types. Response: ```json { "data": { "run_id": "uuid", "type": "sound_lookup", "platform": "tiktok", "status": "completed", "subject": "7570679470014532374", "created_at": "2026-05-26T17:30:48Z", "completed_at": "2026-05-26T17:32:11Z", "credits_used": 100, "request": { "music_id": "7570...", "trend_analysis": true }, "result": { }, "error": null } } ``` ### GET /v1/satellite/runs — List Your Runs Cost: Free. Paginated history. Query parameters: - type (string, optional): creator_lookup | sound_lookup | hashtag_lookup | video_outlier | batch_creator - platform (string, optional): tiktok | youtube | instagram - limit (1-100, default 25), offset (default 0) ### GET /v1/satellite/runs/:run_id/videos — Paginated Videos Sub-resource Cost: Free. Slices the `videos` array of any past run. Especially useful for sound and hashtag lookups with trend_analysis=true (up to ~300 videos). Query parameters: limit (1-200, default 25), offset (default 0). --- ### POST /v1/tracking/creators — Track a Creator Start monitoring a creator's metrics over time. AI reports are automatically generated on every tracking cycle. Cost: $0.25, charged at the POST (covers the initial cycle). Auto-refunded if that first cycle can't resolve the creator/video. Each subsequent recurring cycle is another $0.25. If collection_depth is set, the depth cost (+$0.50/$1.00/$2.00) is added to this same POST charge. Request body (JSON): - platform (string, required): "tiktok", "youtube", or "instagram". - url (string): Creator profile URL. Either url or handle is required. - handle (string): Creator handle (without @). Either url or handle is required. - scrape_cadence (string): How often to collect metrics and generate AI reports. Options: "six_hours", "twelve_hours", "daily", "every_other_day", "weekly", "bi_weekly", "monthly". Default "daily". - collection_depth (string, optional): "standard" (50 videos, +$0.50), "deep" (200, +$1.00), or "full" (500, +$2.00). Runs an initial deep post-collection alongside the first cycle to back-fill a deeper post history and per-post sound than the daily shallow sample. Omit for shallow daily collection only. The combined (cycle + depth) charge is reflected in the X-Cost / X-Credits-Used response headers. Response (202): ```json { "data": { "id": "uuid", "status": "active", "message": "Creator tracking started. Initial metrics and AI report are being generated." } } ``` ### GET /v1/tracking/creators — List Tracked Creators Cost: Free Query parameters: page (integer, default 1), limit (integer, 1-100, default 20), platform (string, optional filter), search (string, optional — case-insensitive partial match on handle or display name). Response includes array of tracked creators with: id, platform, platform_handle, status, scrape_cadence, latest_followers, latest_total_likes, last_scraped_at, next_scrape_at, growth_rate, followers_gained, created_at. Paginated with { data: [...], pagination: { page, limit, total, total_pages, has_next_page, has_prev_page } }. ### GET /v1/tracking/creators/:id — Get Tracked Creator Cost: Free. Returns full details for a tracked creator including latest metrics. ### GET /v1/tracking/creators/:id/report — Get Creator Report Cost: Free. Returns the latest AI analysis report (auto-generated each tracking cycle) including content strategy insights, audience analysis, and growth recommendations. ### GET /v1/tracking/creators/:id/snapshots — Get Creator Snapshots Cost: Free. Returns historical metric snapshots captured at each scrape interval. Each snapshot includes: followers, total_likes, total_videos, total_views (YouTube: profile-level from platform; TikTok/Instagram: sum of views across collected posts), collected_post_count (number of posts in the total_views sum — 0 for YouTube). Plus computed delta_* fields (delta_followers, delta_following, delta_total_videos, delta_total_views, delta_total_likes) showing the change from the previous snapshot. First snapshot has null deltas. Use for building growth charts. ### GET /v1/tracking/creators/:id/posts — List Creator's Posts Cost: Free. Returns the creator's collected posts, populated by daily tracking cycles and on-demand collection. Paginated. Query parameters: - page (integer, default 1), limit (integer, 1-200, default 50) - start_date / end_date (ISO 8601, optional): filter by publish date. - sort (string, optional): "publish_date_desc" (default), "publish_date_asc", or "views_desc". Each post includes: id, post_url, platform_post_id, views, likes, comments, shares, bookmarks, duration_seconds, publish_date, hashtags, is_duet/is_stitch (TikTok-only), collected_at, outlier flags (is_outlier, outlier_ratio, outlier_weighted_score), and a nested sound object (or null). sound.external_id is the platform-native sound id (TikTok music/clip id, Instagram audio id; null for YouTube) that a music catalog matches against. TikTok and Instagram posts carry sound after any cycle; YouTube posts carry sound only when collected via a deep/full on-demand collection (or an initial collection_depth of deep/full). Image posts / carousels have sound: null. Only these documented fields are returned — internal columns are not exposed. Example sound object: ```json { "id": "550e8400-e29b-41d4-a716-446655440000", "external_id": "7559312683885201425", "title": "original sound - khaby.lame", "platform": "tiktok", "duration": 15, "cover_url": "https://.../sound-covers/abc.jpg", "owner_handle": "@khaby.lame", "owner_nickname": "Khabane lame", "is_original": true, "is_commerce_music": false, "usage_count": 5999231 } ``` ### GET /v1/tracking/creators/:id/posts/:post_id — Get Single Post Cost: Free. Returns one collected post with the same fields as the list endpoint, including the nested sound object. ### POST /v1/tracking/creators/:id/posts/collect — Collect Posts On-Demand Cost: $0.50 / $1.00 / $2.00 by depth. Charged when the job is accepted (reflected in X-Cost / X-Credits-Used), not on completion — a failed collection is not auto-refunded. Triggers a deeper post collection than the daily cycle and populates per-post sound (TikTok/Instagram always; YouTube on deep/full). Request body (JSON): - depth (string, optional): "standard" (up to 50 videos, $0.50), "deep" (up to 200, $1.00), or "full" (up to 500, $2.00). Default "standard". Async: returns a collection_id. Poll GET /v1/tracking/creators/:id/posts/collect/:collection_id until status is "completed", then read the results via GET /v1/tracking/creators/:id/posts. ### GET /v1/tracking/creators/:id/posts/collect/:collection_id — Poll Collection Job Cost: Free. Returns the collection job status (queued | processing | completed | failed) and, on completion, a summary (e.g. posts_collected). The collected posts themselves are read from GET /v1/tracking/creators/:id/posts. ### GET /v1/tracking/creators/:id/posting-cadence — Posting Cadence Cost: Free. Posting-frequency analytics derived from collected posts: total_posts, avg_days_between_posts, posts_per_week, posts_per_month, day_of_week_distribution (0=Sun … 6=Sat), earliest_post_date, latest_post_date. ### PATCH /v1/tracking/creators/:id — Update Tracked Creator Cost: Free. Update status ("active" or "paused") or scrape_cadence. ### DELETE /v1/tracking/creators/:id — Stop Tracking Creator Cost: Free. Soft delete — historical data is retained. Returns 204. ### POST /v1/tracking/videos — Track a Video Start monitoring a video's metrics over time. AI reports are automatically generated on every tracking cycle. Cost: $0.25, charged at the POST (covers the initial cycle). Auto-refunded if that first cycle can't resolve the creator/video. Each subsequent recurring cycle is another $0.25. Request body (JSON): - url (string, required): Full video URL. - platform (string, required): "tiktok", "youtube", or "instagram". - tracking_account_id (string, optional): Associate with a tracked creator. - scrape_cadence (string): How often to collect metrics and generate AI reports. Options: "six_hours", "twelve_hours", "daily", "every_other_day", "weekly", "bi_weekly", "monthly". Default "daily". Response (202): ```json { "data": { "id": "uuid", "status": "active", "message": "Video tracking started. Initial metrics and AI report are being generated." } } ``` ### GET /v1/tracking/videos — List Tracked Videos Cost: Free. Same pagination as creators. Query parameters: page, limit, platform, search (string, optional — case-insensitive partial match on title or URL). ### GET /v1/tracking/videos/:id — Get Tracked Video Cost: Free. Returns full video details including current views, likes, comments, shares. ### GET /v1/tracking/videos/:id/report — Get Video Report Cost: Free. Returns the latest AI analysis report (auto-generated each tracking cycle) with performance insights and growth trends. ### GET /v1/tracking/videos/:id/snapshots — Get Video Snapshots Cost: Free. Returns historical metric snapshots (views, likes, comments, shares) at each scrape, plus computed delta_* fields (delta_views, delta_likes, delta_comments, delta_shares, delta_bookmarks) showing the change from the previous snapshot. First snapshot has null deltas. ### PATCH /v1/tracking/videos/:id — Update Tracked Video Cost: Free. Update status ("active" or "paused") or scrape_cadence. ### DELETE /v1/tracking/videos/:id — Stop Tracking Video Cost: Free. Soft delete. Returns 204. --- ## Sound Endpoints Virlo collects sound/audio metadata from TikTok, YouTube, and Instagram videos. All video responses across the API now include an optional `sound` object — this is additive and non-breaking. **Platform field availability:** | Field | TikTok | YouTube | Instagram | |---|---|---|---| | title | Always | Always | Always | | duration | Always | — | — | | cover_url | Always | Always | — | | usage_count | Always | — | — | | is_commerce_music | Always | — | — | | is_original | Always | — | — | | owner_handle | ~12% | — | — | | owner_nickname | ~12% | Always | Always | | owner_id | ~12% | Always (channel ID) | — | Fields unavailable for a platform return `null`. ### GET /v1/sounds/trending — Trending Sounds Cost: 25 credits ($0.25) Parameters: `platform` (tiktok/youtube/instagram), `limit` (1-100, default 20), `page`, `sort`, `commerce_only` (boolean, TikTok filter) `sort` options: - `videos_7d` (default) — sounds ranked by # videos in our dataset published in the last 7 days. The recency-weighted "real trending" signal. - `videos_30d` — same idea, 30-day window. - `usage_count` (legacy) — all-time platform-wide usage. Greatest hits, not "trending". - `video_count` (legacy) — all-time count of videos in our dataset. Same caveat. Velocity sorts include a `videos_in_window` field on each item — the count that drove the ordering. Response also echoes the `sort` mode at the top level so callers can confirm. For relative-momentum (rapidly rising from a small base) use `/v1/sounds/breakout`. ### GET /v1/sounds/breakout — Breakout Sounds Cost: 25 credits ($0.25) Parameters: `platform`, `commerce_only`, `min_recent` (default 3, min videos in last 7d to qualify), `min_baseline` (default 3, min videos in 90d baseline), `limit`, `page`. Returns sounds with the highest recent-burst ratio (sudden rise from a small baseline), distinct from `/sounds/trending`: - Trending favours large absolute weekly volume (e.g. mature sound still gaining 100/week). - Breakout favours sounds where almost all activity happened in the last 7 days (e.g. went from near-zero to 40 videos in a week). Score formula: `videos_7d × (videos_7d / videos_90d)`. Each item carries `videos_7d`, `videos_30d`, `videos_90d`, `burst_ratio` (1.0 = 100% of 90-day activity is in the last 7 days — a true breakout), and `breakout_score`. ### GET /v1/sounds/search — Search Sounds Cost: 10 credits ($0.10) Parameters: `q` (required, min 2 chars), `platform`, `limit`, `page` Uses trigram fuzzy matching. Empty results return `{ "data": [], "pagination": {...} }` — never an error. May include a `note` field when results are sparse. ### GET /v1/sounds/:sound_id — Sound Details Cost: 5 credits ($0.05). +10 credits ($0.10) when `resolve=true` triggers a fresh artist resolution (cached resolutions are free). Parameters: `resolve` (boolean) Returns full metadata + computed stats: `total_videos`, `avg_views`, `top_video_url`, plus a `track_resolution` object (status: unresolved | pending | resolved | not_found, artist_name, isrc, spotify_track_id, spotify_artist_id, release_status, resolution_source, release_date, confidence, resolved_at). Pass `resolve=true` to map a still-unresolved sound to its canonical recording via Spotify track match. ### GET /v1/sounds/:sound_id/videos — Sound Videos Cost: 25 credits ($0.25) Parameters: `limit`, `page`, `sort` (views_desc|publish_date_desc), `platform` Returns paginated videos using this sound, shaped identically to Orbit video items. ### GET /v1/sounds/:sound_id/usage-history — Sound Usage History Cost: 5 credits ($0.05) Parameters: `start_date`, `end_date`, `limit` (default 90, max 365) Returns time-series of `{ usage_count, video_count_local, delta_usage_count, delta_video_count_local, snapshot_at }`. Delta fields show change from previous snapshot (null for first entry). Same pattern as tracking creator snapshots. ### GET /v1/sounds/by-creator/:platform/:handle — Creator Sounds Cost: 25 credits ($0.25) Parameters: `limit`, `page`, `sort` (usage_count|video_count) Returns all sounds owned by a creator with per-sound `video_count` and `avg_views`, plus aggregates: `total_sounds`, `total_ugc_videos`, `total_usage_count`. ### GET /v1/orbit/:orbit_id/sounds — Orbit Sounds Cost: Free Parameters: `limit`, `page` Top sounds used across videos in a keyword search, ranked by video_count within the search. ### GET /v1/comet/:comet_id/sounds — Comet Sounds Cost: Free Parameters: `sort` (video_count | usage_count | rising | growth_7d), `limit`, `page` Top sounds in a niche monitor. `sort=rising` / `growth_7d` rank by run-over-run growth in niche usage (the TikTok-genre "what sound is breaking out right now" signal). Every row carries `growth_video_count`, `growth_views`, and a `lifecycle` label (new | rising | steady | fading). Pivot to /v1/sounds/:sound_id/usage-history for the full time-series. ### GET /v1/comet/:comet_id/hashtags — Comet Hashtags Cost: Free Parameters: `sort` (volume | growth | avg_views), `limit`, `page` Per-hashtag analytics over the niche's collected videos: video_count, total_views, avg_views, avg_engagement, run-over-run growth_video_count, a lifecycle label, and top_creators[] per hashtag. `sort=growth` surfaces hashtags gaining momentum in the genre. ### GET /v1/comet/:comet_id/benchmarks — Comet Benchmarks Cost: Free Genre norms across the niche's creators bucketed by follower tier (nano/micro/mid/macro): median_engagement_rate, median_followers, median_videos_in_niche, median_posting_frequency_days. Answers "how does a creator compare to the genre norm?". ### GET /v1/comet/:comet_id/affinity — Comet Affinity (BETA) Cost: Free BETA — directional signal, methodology may evolve. Genre adjacency from the niche corpus: creator_topics, related_hashtags, related_sounds. NOT a follow-graph. ### GET /v1/comet/:comet_id/creators/:creator_id/similar — Similar Creators (BETA) Cost: Free Parameters: `limit` BETA — directional signal, methodology may evolve. Creators in the niche ranked by shared hashtags + shared sounds with the target creator (co-occurrence, no embeddings in v1). Returns author_id, username, followers, shared_hashtag_count, shared_sound_count, similarity_score. Note: the existing GET /v1/comet/:id/creators/outliers also accepts `order_by=rising` (run-over-run velocity, falls back to weighted_score on a young niche) plus `follower_tier` (nano/micro/mid/macro) and `category` (topic substring) filters. --- ## Data Intelligence Data Intelligence is an optional add-on that enriches every discovered video with 43 AI-analyzed fields covering content classification, hook analysis, visual attributes, brand safety, and more (each slideshow gets a parallel panel-text schema instead). When an intent is provided alongside intelligence, each video is evaluated against the intent and receives an intent_match result, and the response includes an intent_summary with matched URLs. ### How to Enable Pass `data_intelligence_enabled: true` when creating an agent (recommended) or a legacy Orbit/Comet: - POST /v1/agents with `data_intelligence_enabled: true` (recommended — works for both is_recurring: false and is_recurring: true) - POST /v1/orbit with `data_intelligence_enabled: true` (DEPRECATED) - POST /v1/comet with `data_intelligence_enabled: true` (DEPRECATED) It cannot be added retroactively — a resource created without it will never carry intelligence. ### Pricing +$1.00 on top of the base cost: - Agent one-shot: $0.50 base + $1.00 Data Intelligence = **$1.50 total** - Agent recurring: free to create + $1.00 per scheduled run for intelligence (base run bills separately) - Orbit (DEPRECATED): $0.50 base + $1.00 = **$1.50 total** - Comet (DEPRECATED): $0.50 base + $1.00 = **$1.50 total** Retrieval of intelligence data (via GET /v1/agents/:id/videos, or legacy GET /v1/orbit/:orbit_id/videos / GET /v1/comet/:id/videos) remains free. ### Response Format When enabled, each video object in the response includes an `intelligence` field. If an intent was also provided, each video includes an `intent_match` field: ```json { "intelligence": { "primary_topic": "posture correction exercises for desk workers", "secondary_topics": ["ergonomic setup", "back pain relief"], "keywords": ["posture", "desk worker", "back pain", "exercises"], "category": "fitness", "content_format": "tutorial", "visual_format": "activity_demonstration", "hook_text": "Your spine is screaming — here's the 60-second fix.", "hook_type": "bold_claim", "visual_hook_type": "text_on_screen", "transcript_quality": "high", "transcript_word_count": 142, "language_detected": "en", "speaking_style": "instructional", "emotional_tone": "educational", "sentiment": "positive", "has_face_visible": true, "camera_perspective": "front_facing", "has_onscreen_captions": true, "caption_style": "dynamic_animated", "has_text_overlay": true, "text_overlay_purpose": "key_point_emphasis", "setting": "home_office", "lighting_quality": "well_lit", "brand_safety_tier": "safe", "is_educational": true, "is_sponsored": false, "brands_mentioned": [], "cta_usages": [{"type": "save_post", "text": "Save this for later"}], "social_proof_used": ["personal_transformation"], "trend_references": [], "summary": "Fitness creator demonstrates a 60-second posture reset." }, "intent_match": { "matches": true, "reasoning": "Video directly addresses posture correction for desk workers, matching the search intent." } } ``` ### Intent Summary When intent is provided with data_intelligence_enabled, the paginated video response includes an `intent_summary` block: ```json { "intent_summary": { "matched": 12, "total_evaluated": 46, "not_evaluated": 0, "matched_urls": [ "https://www.tiktok.com/@creator/video/123", "https://www.youtube.com/shorts/abc" ] } } ``` - `matched`: Number of videos where intent_match.matches is true - `total_evaluated`: Total videos evaluated for intent matching - `not_evaluated`: Videos that could not be evaluated (e.g. missing transcript) - `matched_urls`: URLs of all matched videos ### Query Parameter: intent_match When retrieving videos from an Orbit or Comet that used intent + data_intelligence_enabled, you can filter by intent match result: - `?intent_match=true` — only videos that matched the intent - `?intent_match=false` — only videos that did not match the intent Applies to: - GET /v1/agents/:id/videos?intent_match=true (recommended) - GET /v1/orbit/:orbit_id/videos?intent_match=true (DEPRECATED) - GET /v1/comet/:id/videos?intent_match=true (DEPRECATED) ### Complete Field List The `intelligence` object carries 43 canonical fields: primary_topic, secondary_topics, keywords, category, content_format, visual_format, visual_complexity, hook_text, hook_type, visual_hook_type, background_type, background_reasoning, foreground_type, foreground_reasoning, scene_changed, transcript_quality, transcript_word_count, transcript_character_count, language_detected, is_multilingual, speaking_style, emotional_tone, sentiment, has_face_visible, camera_perspective, has_onscreen_captions, caption_style, has_text_overlay, text_overlay_purpose, text_overlay_content, setting, lighting_quality, brand_safety_tier, is_educational, is_nsfw, is_sponsored, brands_mentioned, cta_usages, social_proof_used, sensitive_topics, trend_references, low_confidence_fields, summary. Alongside the object, each analyzed item exposes `intelligence_status` (`ready` | `pending` | `disabled` | `failed` | `skipped`). When the agent used an intent, each video also carries `intent_match { matches, reasoning }`. ### Valid Enum Values **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 **emotional_tone:** funny, inspiring, shocking, educational, controversial, heartwarming, wholesome, angry, sad, hype, calm, sarcastic, nostalgic, cringe, dark_humor, urgent, mysterious, relatable, neutral **sentiment:** positive, negative, neutral, mixed **brand_safety_tier:** safe, low_risk, medium_risk, high_risk, unsafe **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, other **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, none **content_format:** OPEN VOCABULARY, not a closed enum. A canonical core covers most videos — explainer, tutorial, review, storytime, listicle, silent_aesthetic, motivational, skit_sketch, comedy_bit, location_showcase, rant, news_report, news_commentary, educational_breakdown, day_in_life, challenge, gameplay_commentary, workout_demo, q_and_a, transformation, hot_take, interview_clip, reaction, asmr, product_showcase, unboxing, haul_restock, grwm_routine, meme, vlog, cooking_recipe, travel_guide — plus a long free-text tail. Match values loosely/normalized (treat skit, comedy_skit, skit_sketch as one bucket) rather than doing exact string comparison. The full enum reference (speaking_style, caption_style, camera_perspective, setting, lighting_quality, cta_usages, social_proof_used, sensitive_topics, text_overlay_purpose) lives at https://dev.virlo.ai/docs/intelligence#enum-values. ### Reading Intelligence Trustworthily - About 94% of videos analyze successfully end to end; the rest fail at frame extraction or analysis and stay sparse. Always check the per-item `intelligence_status` field. - Every analyzed item has a `summary` (1-paragraph AI description). Reading 100 summaries is the fastest way to "watch" 100 videos. - `hook_text` and transcript-based fields are only present when there is speech or on-screen text: roughly 63% of TikTok videos carry transcripts, ~38% of YouTube Shorts, and ~0% of Instagram Reels (no transcripts available). `transcript_word_count: 0` with populated visual fields = deliberate silent/aesthetic content, not missing data. - `low_confidence_fields[]` lists fields the analyzer was unsure about for that item — discount any field named there. --- ### GET /v1/account/balance — Check Credit Balance Cost: Free Returns your current credit balance, dollar equivalent, account status, and a pricing reference for all operations. Response schema: ```json { "data": { "balance": "$47.50", "credits_remaining": 4750, "status": "active" } } ``` --- ## Recommended Workflow Recipes ### Recipe 1: Full Niche Analysis (Best Value) For comprehensive niche research, chain these endpoints: 1. POST /v1/agents with is_recurring: false, meta_ads_enabled: true, platforms: ["youtube", "tiktok", "instagram"], 3-7 specific multi-word keywords, and an intent string — $0.50 (add data_intelligence_enabled: true for +$1.00 when the task involves understanding content, not just counting views). Do NOT set min_views/time_range — collection is system-managed; filter at read time. 2. Poll GET /v1/agents/:id every 60s until finalized: true (free; treat partial_failure like completed) 3. GET /v1/agents/:id/analysis/latest for the AI intelligence report (free) 4. GET /v1/agents/:id/trends/latest for detected trends (free) 5. GET /v1/agents/:id/videos?min_views=1000&order_by=views for detailed video data, filtered at read time (free) 6. GET /v1/agents/:id/slideshows for image carousels (free) 7. GET /v1/agents/:id/ads for Meta ad intelligence (free) 8. GET /v1/agents/:id/creators/outliers?order_by=weighted_score to find rising creators (free) 9. For each interesting outlier creator, GET /v1/satellite/creator/:platform/:username — $0.50 each Total: $0.50 base + $0.50 per creator deep-dive. Retrieval is always free. Legacy note: this recipe previously used POST /v1/orbit and the /v1/orbit/:orbit_id/* sub-paths. Those still work until August 3, 2026 (IDs are interchangeable), but new integrations should use /v1/agents. For continuous monitoring, create the agent with is_recurring: true + a cadence instead of a separate Comet. ### Recipe 1b: Global / Non-English Research by Region For research in a non-English niche and/or filtering by upload country: 1. POST /v1/agents with is_recurring: false, english_only: false, and keywords + intent written in the TARGET LANGUAGE (e.g. Spanish keywords for Spanish soccer content) — $0.50 (+$1.00 with data_intelligence_enabled). Opening english_only is what lets non-English content through. 2. Poll GET /v1/agents/:id until finalized: true. 3. GET /v1/agents/:id/videos?region=MX&order_by=views — filter the collection to a single upload country (ISO-3166-1 alpha-2, case-insensitive). Each video carries upload_region. 4. GET /v1/agents/:id/slideshows?region=AR — slideshows have the highest region coverage (deterministic, straight from TikTok). Region detection is BETA: deterministic where the platform provides it (TikTok video/creator region, YouTube channel country), AI-inferred otherwise; unresolved items are excluded when you filter. Retrieval is free. ### Recipe 2: Creator Deep Dive 1. GET /v1/satellite/creator/:platform/:username?include=videos,outliers&max_videos=50 — $0.50 2. Poll status/:job_id every 10s until completed (free) 3. For the top outlier video, POST /v1/satellite/video-outlier — $0.50 4. Poll status/:job_id until completed (free) Total: $1.00 ### Recipe 3: Trend Monitoring Setup 1. GET /v1/trends/digest to see today's trends — $0.25 2. Pick interesting trends and POST /v1/agents with is_recurring: false and those as keywords for a one-shot scan — $0.50 3. Once satisfied, POST /v1/agents with is_recurring: true + cadence: "weekly" and the same keywords for ongoing monitoring — free to create, bills per scheduled run Total: $0.75 initial, then automated monitoring bills per scheduled run. (Legacy: POST /v1/orbit + POST /v1/comet, deprecated August 3, 2026.) ### Recipe 4: Creator Growth Monitoring 1. POST /v1/tracking/creators with scrape_cadence: "twelve_hours" — $0.25 2. GET /v1/tracking/creators/:id/snapshots periodically to build growth charts — Free 3. GET /v1/tracking/creators/:id/report for the latest AI analysis (auto-generated each cycle) — Free Total: $0.25 to start. Metric collection and AI reports are automatic. ### Recipe 5: Video Performance Tracking 1. POST /v1/tracking/videos with scrape_cadence: "six_hours" — $0.25 2. GET /v1/tracking/videos/:id/snapshots to track view velocity — Free 3. GET /v1/tracking/videos/:id/report for the latest AI analysis — Free 4. PATCH /v1/tracking/videos/:id to slow cadence once growth stabilizes — Free Total: $0.25 to start. AI reports are auto-generated each cycle. ### Recipe 6: Creator Playbook Deep-Dive For when a customer wants to understand a creator's recurring formats, hooks, eras, and resurgences (competitive briefing, partner vetting, idea sourcing): 1. GET /v1/satellite/creator/:platform/:username?trend_analysis=true — $1.00 ($0.50 base + $0.50 trend surcharge). Forces a 100-video deep fetch and runs LLM trend detection. 2. Poll GET /v1/satellite/creator/status/:job_id every 10-15s until completed — Free 3. Surface the trends block: each trend has `time_windows[]` (with start_date/end_date/avg_views/description per window), `resurged` (true when ≥2 disjoint windows), `momentum` (stronger | weaker | similar | null), and `evidence_video_ids[]` that map back to entries in the same response's `videos[]` array. The `summary` gives you a one-liner of the creator's playbook. 4. Save the run_id. GET /v1/satellite/runs/:run_id is free forever — re-read for dashboards / agent context without re-spending. Stack `audience_demographics=true` and/or `audience_geography=true` for $0.50 more (cache-aware) to ship a single all-in brief. Total: $1.00. Free re-reads via run_id thereafter. Skip trend_analysis=true if you only need stats, hashtags, outliers, or audience data — drop to $0.50. ### Recipe 7: Sound Trend Deep-Dive For when a customer wants to know what's happening around a specific TikTok sound (Drake's "david", a niche viral instrumental, etc.): 1. (Optional) GET /v1/sounds/search?q=... or /v1/sounds/trending to find a music_id — $0.10–$0.25 2. GET /v1/satellite/sounds/tiktok/:music_id?trend_analysis=true — $1.00 (kicks off ~300-video deep dive + LLM trend detection) 3. Poll GET /v1/satellite/sounds/status/:job_id every 10-15s until completed — Free 4. Surface stats (velocity, top creators, duration distribution) + trends (each with time_windows, resurged, momentum) — already in the result 5. Save the run_id. GET /v1/satellite/runs/:run_id is free forever — re-read for dashboards / agent context without re-spending Total: $1.00. Free re-reads via run_id thereafter. Skip trend_analysis=true if all you need is the video list + aggregate stats — drop to $0.50. ### Recipe 8: Hashtag Research 1. GET /v1/hashtags?order_by=views&sort=desc for top hashtags — $0.05 2. GET /v1/hashtags/:hashtag/performance for deeper stats on interesting ones — $0.05 each 3. GET /v1/tiktok/hashtags or /youtube/hashtags for platform-specific data — $0.05 each Total: $0.05-$0.25 depending on depth. --- ## Interpreting Results — Virality, Benchmarks, and Trust How to turn the data you retrieve into correct conclusions. (Full guidance: https://dev.virlo.ai/agent-playbook.txt) ### Weighted Virality Score (the canonical ranking signal) Raw views mislead: 500K views from a 10M-follower account is routine; the same views from a 3K-follower account is a signal. Rank by the weighted score, which is what Virlo's own products use: ``` ratio = views / followers (only when followers > 0 and ratio > 1) weighted_score = ln(ratio) * ln(followers) ``` Interpretation bands: >= 35 exceptional (study frame by frame) | 25-35 very strong | 18-25 strong | 10-18 promising | < 10 emerging/routine. Sanity-check with the raw multiplier too: views/followers >= 20x is notable, >= 100x exceptional, >= 1000x mega-outlier. And compute engagement_rate = (likes + comments + shares) / views — high views with engagement > 5% means resonance; high views with < 1% means passive distribution (sounds, reposts, paid). Creator outliers (GET .../creators/outliers) apply the same logic at account level: `outlier_ratio` is avg views vs follower baseline, and `weighted_score` (the recommended order_by) is ln(outlier_ratio) * ln(follower_count). ### Platform Benchmarks (production medians — never compare raw views across platforms) | Platform | Median views | P90 | P99 | Transcript coverage | |---|---|---|---|---| | TikTok | ~39K | ~1M | ~6.6M | ~63% | | Instagram Reels | ~3.8K | ~175K | ~3M | ~0% | | YouTube Shorts | ~1K | ~32K | ~2.5M | ~38% | A 100K-view Reel is a bigger deal than a 100K-view TikTok. For Instagram, lean on descriptions, hashtags, and intelligence visual fields rather than transcripts. ### Reading analysis_data (Orbit/Comet AI analysis) - `key_highlight` = the single most important finding; `overview` = executive summary. - `themes[]` = clusters of what works, each with `why_it_works`, `tactics[]`, `confidence` (0-1: weight >= 0.7 heavily, present < 0.5 as tentative), and `evidence_video_ids[]` — always join evidence ids back to the video list so claims stay grounded in retrievable examples. - `top_10_breakdown` = AI-curated standouts (weighs content quality and topicality, not just metrics). Where this list and your weighted-score ranking agree, you have found the real winners. - `excluded_videos` = what the AI judged off-topic — a quick gauge of result purity. ### Trend Lifecycle (trends/latest items) `status` field: new (just emerged — highest opportunity) | rising (growing — act now) | steady | fading (avoid). `stable_key` lets you follow one trend across Comet cycles; a trend going new → rising with growing video_count is the strongest "post about this now" signal. `prev_*` fields give cycle-over-cycle deltas. ### Aggregation Pattern for Content Strategy Questions The strongest insight format is distribution-over-winners: take the top quartile of videos by weighted score, count hook_type x content_format x emotional_tone buckets, then contrast against the bottom quartile. The differences between those distributions are the niche's actual playbook. Quote real `hook_text` strings from winners as replicable templates. ### Audience Data Trust Rules Audience snapshots derive from engaged commenters, not follower counts. Before presenting numbers, read `confidence_level` (high/medium = usable; low = say "limited signal", do not state percentages as fact) and `data_source` (comments is strongest; followers is TikTok-only and capped at medium; profile_only means no real audience signal was found — it is auto-refunded and should be treated as "no data"). --- ## Data Richness Guide Understanding what data Virlo provides helps you recommend the right endpoints: **Video Data**: Every video includes URL, publish date, views, likes, comments, bookmarks, full description text, thumbnail URL, hashtags array, platform type, niche classification, author reference, region code, duration in seconds, and raw transcript text. Transcripts enable content analysis, keyword extraction, and sentiment analysis at scale. **Creator Data** (via Satellite): Profile stats (followers, total videos, total likes), per-hashtag performance breakdowns, engagement rates, posting frequency, and outlier detection across recent videos. **Trend Data**: AI-generated trend names and descriptions that summarize what's happening across platforms. Updated daily. **Orbit Analysis Reports**: Every Orbit search automatically generates a comprehensive AI analysis covering: market themes, connecting threads, viral tactics, timing analysis, and top video breakdowns. Retrieve the latest analysis via GET /v1/orbit/:orbit_id/analysis/latest and detected trends via GET /v1/orbit/:orbit_id/trends/latest. Comet monitors also auto-generate analysis and trends after each run (same endpoints under /v1/comet/:id/). **Meta Ad Intelligence**: When enable_meta_ads is enabled in Orbit or Comet, the system collects active Meta (Facebook/Instagram) ads related to your keywords, including ad creative, landing pages, targeting signals, and engagement data. --- ## Best Practices for AI Agent Integration 1. Always include all three platforms (youtube, tiktok, instagram) in Orbit/Comet searches for the most comprehensive results. 2. AI analysis is automatic on every Orbit search — retrieve it via GET /v1/orbit/:orbit_id/analysis/latest after completion. 3. Use specific multi-word keyword phrases. "jeep wrangler mods" returns far better results than "jeep". 4. After Orbit completes, retrieve videos, ads, AND outliers — they are all free and provide different valuable perspectives. 5. Check the balance with the free GET /v1/account/balance endpoint (the X-Balance-Remaining header only appears on charged responses, so it is absent on free reads and polling). When below $10.00, inform the user they may want to add funds at https://dev.virlo.ai/dashboard/billing. 6. For async endpoints, never hardcode timeouts. Poll until finalized: true (the end-state signal); treat partial_failure as usable data, not an error. 7. Creation costs money; reading is free. Before creating new work, check the team's existing resources (GET /v1/orbit, GET /v1/comet, GET /v1/satellite/runs, GET /v1/tracking/creators) — the answer may already exist. 8. Use platform-specific endpoints when you only need data from one platform ($0.05 vs $0.25 for videos). 9. Rank by weighted virality score, not raw views (see Interpreting Results above), and never compare raw view counts across platforms. 10. For deeper guidance on routing user intents to workflows and interpreting every field, read https://dev.virlo.ai/agent-playbook.txt. ## Machine-Readable OpenAPI Specification For programmatic type generation, code completion, and MCP auto-configuration, the full OpenAPI 3.0 spec (with typed response schemas for every v1 endpoint) is available at: ``` https://api.virlo.ai/openapi.json ``` This JSON spec includes request parameters, response body schemas with field types, and authentication requirements. Use it with tools like openapi2mcp, RestMCP, or any OpenAPI-compatible code generator.