API reference
Everything for the viralcli API — authentication, discovery and enrichment, the async job flow, tracked items, and signed webhooks. Built for CLIs and automated agents. Base URL https://api.viralcli.com.
Contents▾
Introduction
viralcli is viral-signal intelligence for short-form video. Give it a seed topic or a video link and get back ranked breakouts, hook transcripts, visual-beat storyboards, and a heating/saturating format verdict — plus true longitudinal velocity for anything you track. It is an API and a CLI built for autonomous content-creation agents and pipelines: usage-metered, no seats, one API key.
Two kinds of work, metered separately:
| Op | What | Cost |
|---|---|---|
| Discovery | A seed topic → ranked breakout videos + adoption histogram + format verdict. Metadata only, so it's cheap. | 1 discovery call |
| Enrichment | One video fully analysed: hook transcript + storyboard + metrics. Cached by platform+video_id — repeats are free. | 1 enrichment credit |
Supported platforms: youtube · tiktok · instagram (availability varies by plan). Responses are deterministic JSON; storyboards return image/jpeg. An OpenAPI schema is served live at /docs on the API host.
Concepts
| Term | Meaning |
|---|---|
Discovery | A seed topic → ranked breakout videos + adoption histogram + format verdict. Metered as discovery calls. |
Enrichment | One video fully analysed: hook transcript + storyboard + metrics. The expensive op. Cached by platform+video_id — repeats are free. |
Snapshot | One metrics reading of a video at a point in time. Frequent sweeps build a time series. |
Trajectory | The full snapshot series → true instantaneous velocity (Δviews/Δt) and acceleration. Tells you a video is peaking now vs decaying. |
Format verdict | HEATING / STEADY / SATURATING / INSUFFICIENT_DATA, derived cross-sectionally from one scrape (videos of many ages trace the format's lifecycle without waiting). |
Tracked item | A video or seed registered for the watcher to poll on an adaptive cadence (fast while accelerating, daily once cold). Drives webhooks and the longitudinal series. |
Quickstart
1. Get an API key. Keys are issued out of band (no public signup endpoint) and shown once — store it securely. Only a SHA-256 hash is kept server-side.
2. Confirm the key and see your plan, limits, and usage:
curl https://api.viralcli.com/v1/account \
-H "Authorization: Bearer sk_live_your_key_here"3. Run your first discovery — a seed topic in, ranked breakouts + a format verdict out:
curl -X POST https://api.viralcli.com/v1/discovery \
-H "Authorization: Bearer sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{ "seed": "tiktok made me buy it gadget" }'4. Pick a breakout and deep-enrich it (transcript + storyboard + metrics) via analyze, or do both in one step with pipeline.
Authentication
Every /v1/* route requires a key. Pass it either way — both schemes are accepted:
# Authorization header (recommended)
curl https://api.viralcli.com/v1/account -H "Authorization: Bearer sk_live_your_key"
# or the X-API-Key header
curl https://api.viralcli.com/v1/account -H "X-API-Key: sk_live_your_key"Keys carry a plan that sets your quotas, rate limit, platform access, and history window. They are stored SHA-256-hashed at rest — the raw value is shown exactly once. Revoking a key takes effect immediately. Quotas are per account, not per key.
| Failure | Status |
|---|---|
| Missing / invalid / revoked key | 401 |
| Rate limit (see Retry-After) | 429 |
Plans & limits
Quotas are monthly; exceeding discovery or enrichment returns 402. Concurrency caps simultaneous in-flight enrichment jobs (429 when full). History bounds how far back trajectory/snapshot reads reach.GET /v1/account returns your plan, limits, and current usage.
| Plan | Discovery/mo | Enrich/mo | Tracked | Concurrency | Rate/min | Platforms | History |
|---|---|---|---|---|---|---|---|
| Free — $0 | 20 | 25 | 3 | 1 | 20 | YouTube | 7 days |
| Starter — $29 | 1,000 | 800 | 15 | 2 | 90 | YouTube, TikTok | 90 days |
| Pro — $79 ★ | 3,000 | 2,500 | 50 | 4 | 240 | all | 1 year |
| Scale — $199 | 10,000 | 6,000 | 200 | 8 | 600 | all | unlimited |
| Enterprise | custom | custom | custom | dedicated | custom | all | unlimited |
Where enabled, overage runs about $0.02/discovery and $0.08/enrichment. A platform not included in your plan returns 403.
Errors
Error bodies are {"error": "...", "detail": "..."}, or FastAPI's {"detail": ...} for validation errors.
| Status | Meaning |
|---|---|
| 400 | Invalid request / blocked URL (SSRF guard). |
| 401 | Missing or invalid API key. |
| 402 | Plan quota exhausted (discovery / enrichment / tracked). |
| 403 | Platform not included in your plan. |
| 404 | Not found (also returned for resources you don't own — no existence leak). |
| 413 | Request body too large (64 KB cap). |
| 422 | Validation error (field constraints). |
| 429 | Rate limit or account concurrency limit (see Retry-After). |
| 5xx | Internal error (no internals leaked). |
Rate limits & quotas
Defense in depth — several independent controls:
- Per-account rate limit — per-plan requests/min;
429+Retry-After. - Per-IP pre-auth throttle — applied before auth, so anonymous floods and bogus-key spraying are capped regardless of account (
IP_RATE_PER_MIN;/healthzexempt). - Per-account concurrency — caps simultaneous enrichment jobs (
429). - Global in-flight ceiling — caps total concurrent enrichments across all accounts.
- Monthly quotas — discovery / enrichment caps (
402); a 64 KB body cap, validated before any quota spend.
Health & account
/healthzno authLiveness probe, no auth. Returns { "status": "ok" }.
/v1/accountYour plan, limits, and current usage — the fastest way to confirm a key works.
{
"account_id": "…", "plan": "pro",
"limits": { "discovery": 3000, "enrich": 2500, "tracked": 50,
"concurrency": 4, "rate_per_min": 240,
"platforms": ["*"], "history_days": 365 },
"usage": { "discovery_used": 12, "enrich_used": 340 }
}Discovery
A seed topic → ranked breakouts + adoption histogram + format verdict. Synchronous; costs 1 discovery.
/v1/discovery// request
{ "seed": "tiktok made me buy it gadget" }// 200
{ "seed": "…", "platform": "youtube", "n": 45, "verdict": "HEATING",
"adoption_histogram": [ { "band": "0-7d", "n": 1, "median_vpd": 12633 }, … ],
"breakouts": [
{ "id": "0qnmNFoe7AY", "title": "…",
"url": "https://www.youtube.com/shorts/0qnmNFoe7AY",
"platform": "youtube",
"views_per_day": 51789, "age_days": 365, "views": 18902807,
"sound": null, "sound_id": null }, …
] }verdict is one of HEATING / STEADY / SATURATING / INSUFFICIENT_DATA / RANKED_BY_VIEWS(YouTube doesn't expose Short upload dates, so when no velocity read is possible the breakouts are ranked by total views instead). sound / sound_id name the audio riding a breakout where the platform exposes it (TikTok; Instagram best-effort).
On-demand discovery currently samples YouTube; the public trends feedis the multi-platform surface (YouTube + TikTok + Instagram, refreshed continuously). Replay a seed's verdict over time with signals.
Sound discovery
Every video riding ONE sound, with the same velocity ranking and heating/saturating verdict as topic discovery — “is this audio still worth riding?”. Synchronous; costs 1 discovery. Recorded into signals as sound:<id>, so a sound's verdict is replayable over time like a topic's.
/v1/sounds/discovery// request — a sound id or its canonical URL
{ "sound": "7016547803243022337", "platform": "tiktok" }// 200
{ "sound_id": "7016547803243022337", "sound": "original sound — creator",
"platform": "tiktok",
"sound_url": "https://www.tiktok.com/music/x-7016547803243022337",
"n": 32, "verdict": "HEATING",
"adoption_histogram": [ … ],
"breakouts": [ { "id": "…", "url": "…", "views_per_day": 51789, … }, … ] }What counts as a sound id per platform: TikTok — the music id (the number in tiktok.com/music/…-<id>, also returned as sound_id on every discovery breakout). YouTube— the source video's 11-char id (YouTube models reused Shorts audio as youtube.com/source/<videoId>/shorts). Instagram — the audio id from instagram.com/reels/audio/<id>/ (best-effort). Platform access follows your plan, like analyze.
Analyze (deep-enrich)
Deep-enrich one video: hook transcript + storyboard + metrics. Asynchronous (costs 1 enrichment) — you get a job id, then poll it. Results cache by platform+video_id, so repeats are free.
/v1/analyze// request
{ "url": "https://www.youtube.com/watch?v=abc", "density": 9, "full": false }density = storyboard frame count (1–25). full = whole-video transcript (default: hook window only). → 202 { "job_id": "…", "status": "queued" }.
/v1/jobs/{job_id}{ "job_id": "…", "kind": "analyze", "status": "done",
"result": {
"platform": "youtube", "video_id": "abc", "creator": "…", "caption": "…",
"views": 1000, "likes": 1, "comments": 1,
"age_days": 2.0, "views_per_day": 500, "duration_sec": 30,
"transcript": "hook text…", "transcript_kind": "hook"
},
"error": null }status: queued | running | done | error. transcript_kind: hook | full | captions (YouTube native captions are used when present — free, no transcription). Storyboards are not stored — fetch one on demand from storyboard.
Pipeline (end-to-end)
Seed → discovery → fully enrich the top breakout, in one call. Asynchronous; costs 1 enrichment.
/v1/pipeline// request
{ "seed": "tiktok made me buy it gadget" }→ 202 { "job_id": "…", "status": "queued" }. The job result is the discovery signal plus the top breakout fully enriched (transcript + storyboard + metrics). Poll it via jobs.
Niche deep-dive
The creator workflow in one call: a niche keyword → the most viral videos in it, each with its hook(what's said in the opening seconds), stats, velocity, sound, and a storyboard URL. Search marketing or programming and get the proven openings to model your own content on. Asynchronous; charges top_nenrichments (unused refunded if a result can't be enriched).
/v1/niche// request
{ "seed": "programming", "platform": "youtube", "top_n": 5, "density": 6 }platform: youtube | tiktok | instagram. top_n 1–8 (each = 1 enrichment). → 202 { "job_id": "…", "status": "queued" }; poll via jobs.
// job result
{ "seed": "programming", "platform": "youtube", "verdict": "HEATING",
"adoption_histogram": [ … ], "n_sampled": 29,
"results": [
{ "rank": 1, "title": "…", "url": "https://…", "creator": "@…",
"views": 4200000, "views_per_day": 91000, "age_days": 3.1, "duration_sec": 41,
"hook": "you've been doing this wrong your whole life",
"transcript_kind": "hook",
"storyboard_url": "/v1/storyboard?url=https://…",
"sound": "original sound — …" }, …
] }Every niche call also deepens the public dataset — the signal is recorded and the winners are tracked — so the data compounds as people use it.
Trends, sounds, creators & search demand
Four public, no-auth, quota-free feeds over the live dataset (capped + cached). Point a dashboard, an agent, or a uptime monitor at them directly.
/v1/trendsno authCurated + auto-discovered topics with per-platform breakouts, verdicts, sounds, seasonality, and shifts (verdicts that flipped in the last 24h).
/v1/sounds/trendingno authNamed audio carrying multiple current breakouts, ranked by the summed velocity of the distinct videos riding it.
/v1/creators/risingno authCreators recurring across current breakouts (2+ videos), velocity-ranked — who's producing winners now.
/v1/search/insightsno authSearch-demand intelligence in three views: all, trending (real demand + healthy supply), and content_gap (high search demand but thin supply — the openings). Built by joining TikTok search autocomplete against our own discovery data.
// /v1/search/insights -> data
{ "all": [ … ],
"trending": [ { "query": "skincare routine men", "demand": 12,
"supply_n": 8, "supply_verdict": "HEATING",
"supply_top_vpd": 713318, "is_gap": false }, … ],
"content_gap": [ { "query": "tiktok made me buy it viral product",
"demand": 12, "supply_n": 4, "is_gap": true }, … ] }/v1/shop/trendingno authTikTok Shop products ranked by sold_per_day— sold-VELOCITY measured between snapshots of TikTok's own cumulative sold counts (cumulative sold_count until a product has 2+ snapshots). Each row carries price, rating, reviews, seller, the search queries it rides, a seriesof sold-over-time points, and a TikTok Shop link. Coverage grows itself: every sweep harvests TikTok's related searches into the swept rotation.
/v1/videos/momentumno authVideos accelerating right now: tracked videos whose recent views/day (last 48h of watcher snapshots) exceeds their lifetime pace, each with a views seriessparkline. Trajectory data a single scrape can't reconstruct.
/healthz/collectionno authPer-platform collection liveness + disk — 200 when healthy, 503 when any platform's data goes stale.
Storyboard
A live-generated contact sheet — a grid of N keyframes with timestamps. Costs 1 enrichment; never stored.
/v1/storyboard?url=&density=curl "https://api.viralcli.com/v1/storyboard?url=https://youtu.be/abc&density=9" \
-H "Authorization: Bearer sk_live_your_key" --output storyboard.jpgReturns image/jpeg. density is 1–25 (default 9). 400if the URL isn't a supported platform host (SSRF guard).
Videos & trajectory
Read what you've already analysed or tracked. History is trimmed to your plan window.
/v1/videos/{platform}/{video_id}Identity + transcript + recent snapshots. 404 if the video hasn't been seen.
/v1/videos/{platform}/{video_id}/trajectory{ "platform": "youtube", "video_id": "abc",
"snapshots": [ { "captured_at": "…", "views": 100000, "views_per_day": 10000 }, … ],
"metrics": { "n": 3, "true_vpd": 100000, "accel": 40000,
"peaking": false, "lifetime_vpd": 10000 } }true_vpd is real instantaneous views/day (needs ≥2 snapshots); accelneeds ≥3. This is the difference between “went viral once” and “peaking right now.”
Signals
/v1/signals?seed=Historical discovery signals for a seed — replay how a format's verdict and breakouts changed over time.
Tracked items
Register a video or seed for the watcher to poll on an adaptive cadence (fast while accelerating, daily once cold). Tracked items drive the longitudinal series and webhook events.
/v1/tracked-items{ "kind": "video", "ref": "<video_id | url | seed>",
"platform": "youtube", "interval_sec": 900 }→ 201 { id, … }. 402 past your plan's tracked-item limit. kind is video or seed; interval_sec is 60–86400.
/v1/tracked-itemsList your tracked items.
/v1/tracked-items/{id}/v1/tracked-items/{id}204 on delete. 404 if it isn't yours.
Managing webhooks
Subscribe an https endpoint to events. The signing secret is returned exactly once at creation.
/v1/webhooks// request
{ "url": "https://your-app.com/hooks", "event_types": ["*"], "tracked_item_id": null }// 201 — secret shown once
{ "id": 42, "url": "https://your-app.com/hooks",
"event_types": ["*"], "tracked_item_id": null,
"secret": "whsec_…" }The URL must be public https — private, loopback, link-local, and metadata IPs are rejected at create and at delivery (defeats DNS rebinding). event_types is ["*"] for all, or any of breakout, peaking, accelerating, format_heating. tracked_item_id scopes to one item; omit for account-wide.
/v1/webhooksList your subscriptions (secrets are never returned again).
/v1/webhooks/{id}204 on delete.
Receiving & verifying
On a matching event we POST JSON to your URL with retries (exponential backoff — 2, 4, 8… minutes, capped at 1h, up to 6 attempts, then dead-lettered):
POST /your-endpoint
X-Socialdata-Event: breakout
X-Socialdata-Delivery: 1234
X-Socialdata-Signature: sha256=<hmac>
Content-Type: application/json
{ "id": "<event-uuid>", "type": "breakout",
"data": { "true_vpd": 120000, "accel": 40000, "snapshots": 3 } }Verify the signature — HMAC-SHA256 of the raw body with your secret:
import hashlib, hmac
def verify(secret, body_bytes, header):
expected = "sha256=" + hmac.new(secret.encode(), body_bytes,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)Respond 2xx to acknowledge. Use the id as an idempotency key to dedupe redelivery. (Delivery headers carry the X-Socialdata- prefix — the engine name behind viralcli.)
Event types
| Type | Fires when |
|---|---|
breakout | A tracked video's true velocity crosses the breakout threshold. |
peaking | Velocity was rising and turned down — the video is peaking now. |
accelerating | Velocity is increasing beyond the acceleration threshold. |
format_heating | A tracked seed's verdict is HEATING with fresh breakouts. |
Python client & MCP
Don't hand-write HTTP. The official Python client wraps every endpoint as a typed method, and the MCP server exposes the same surface as native tools for Claude Desktop, Claude Code, and any MCP client. Both share one endpoint definition, so they never drift.
Python client
from clients.viralcli import ViralCLI
vc = ViralCLI(api_key="sk_live_...") # key only needed for paid calls
vc.live_trends() # public — trends, sounds, shifts
vc.search_insights() # public — all / trending / content_gap
vc.discover("marketing") # breakouts + verdict
vc.niche("programming", top_n=5) # viral videos + hooks + storyboards
vc.analyze("https://www.tiktok.com/@u/video/123")Async jobs (analyze, niche, pipeline) submit-and-poll for you; pass wait=False for the raw { job_id }. Every method returns the parsed data.
MCP server
One self-contained file, downloadable from this domain — no repo access needed. The six public tools work with no key and no signup.
curl -O https://www.viralcli.com/viralcli_mcp.py
pip install "mcp[cli]" requests
# Claude Code (one line):
claude mcp add viralcli -e VIRALCLI_API_KEY=sk_live_... -- python /abs/path/viralcli_mcp.py// Claude Desktop / any MCP client — mcpServers
{
"viralcli": {
"command": "python",
"args": ["/abs/path/viralcli_mcp.py"],
"env": { "VIRALCLI_API_KEY": "sk_live_..." }
}
}Tools: live_trends, trending_sounds, rising_creators, search_insights, shop_trending, videos_momentum (public — no key), plus discover, niche, analyze (keyed). Then ask your agent things like “what's trending in skincare and what hooks are the top videos using?” — it calls niche for you — or “what's selling fastest on TikTok Shop this week?”— that's shop_trending.
Security
- API keys stored only as SHA-256 hashes; raw key shown once; revocable.
- Authorization — every resource is account-scoped; cross-account access returns
404(no existence leak). - SSRF —
/analyzeand/storyboardaccept only known platform hosts; webhook targets must resolve to public addresses (rechecked at delivery). - Input validation — strict Pydantic models; 64 KB body cap; validation runs before quota.
- Transport — deploy behind TLS; HSTS,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy, andCache-Control: no-storeon every response. - Storage — no video or image bytes are persisted; only metrics, identity, and transcript text.
Self-hosting
viralcli is self-hostable. Mint an account key, run the API and the watcher, and flush the hot store to Parquet on a cron:
pip install -r requirements.txt
scrapling install && python -m playwright install chromium # TikTok stealth fallback
python -m service.admin create-account --plan pro # mint account + key
uvicorn service.app:app --host 0.0.0.0 --port 8000 # API
python watcher.py # watcher + webhook delivery
python -m store.lake flush # (cron) hot SQLite → ParquetTunable via env: WHISPER_DEVICE (cpu/cuda), WHISPER_MODEL, HOOK_SECONDS, N_WORKERS, N_TRANSCRIBE, N_BROWSER, POLL_*, VPD_BREAKOUT, ACCEL_HOT, SOCIALDATA_DB, SOCIALDATA_LAKE, SOCIALDATA_CORS.