Documentation

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:

OpWhatCost
DiscoveryA seed topic → ranked breakout videos + adoption histogram + format verdict. Metadata only, so it's cheap.1 discovery call
EnrichmentOne 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.

viralcli reads only public data and is not affiliated with TikTok, Instagram/Meta, or YouTube. One inherent caveat is physics, not a bug: longitudinal velocity needs ≥2 snapshots over time — a single scrape gives only lifetime-average velocity. Tracked items build the true series forward from when you register them.

Concepts

TermMeaning
DiscoveryA seed topic → ranked breakout videos + adoption histogram + format verdict. Metered as discovery calls.
EnrichmentOne video fully analysed: hook transcript + storyboard + metrics. The expensive op. Cached by platform+video_id — repeats are free.
SnapshotOne metrics reading of a video at a point in time. Frequent sweeps build a time series.
TrajectoryThe full snapshot series → true instantaneous velocity (Δviews/Δt) and acceleration. Tells you a video is peaking now vs decaying.
Format verdictHEATING / STEADY / SATURATING / INSUFFICIENT_DATA, derived cross-sectionally from one scrape (videos of many ages trace the format's lifecycle without waiting).
Tracked itemA 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:

shell
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:

shell
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:

shell
# 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.

FailureStatus
Missing / invalid / revoked key401
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.

PlanDiscovery/moEnrich/moTrackedConcurrencyRate/minPlatformsHistory
Free — $020253120YouTube7 days
Starter — $291,00080015290YouTube, TikTok90 days
Pro — $79 3,0002,500504240all1 year
Scale — $19910,0006,0002008600allunlimited
Enterprisecustomcustomcustomdedicatedcustomallunlimited

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.

StatusMeaning
400Invalid request / blocked URL (SSRF guard).
401Missing or invalid API key.
402Plan quota exhausted (discovery / enrichment / tracked).
403Platform not included in your plan.
404Not found (also returned for resources you don't own — no existence leak).
413Request body too large (64 KB cap).
422Validation error (field constraints).
429Rate limit or account concurrency limit (see Retry-After).
5xxInternal 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; /healthz exempt).
  • 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.
Application limits can't stop volumetric floods. Deploy behind an edge layer (network L3/L4 anti-DDoS, a CDN/WAF such as Cloudflare, and a reverse proxy for TLS + timeouts) — that is the actual DDoS defense.

Health & account

GET/healthzno auth

Liveness probe, no auth. Returns { "status": "ok" }.

GET/v1/account

Your plan, limits, and current usage — the fastest way to confirm a key works.

json
{
  "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.

POST/v1/discovery
json
// request
{ "seed": "tiktok made me buy it gadget" }
json
// 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.

POST/v1/sounds/discovery
json
// request — a sound id or its canonical URL
{ "sound": "7016547803243022337", "platform": "tiktok" }
json
// 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.

POST/v1/analyze
json
// 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" }.

GET/v1/jobs/{job_id}
json
{ "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.

POST/v1/pipeline
json
// 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).

POST/v1/niche
json
// 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.

json
// 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.

GET/v1/trendsno auth

Curated + auto-discovered topics with per-platform breakouts, verdicts, sounds, seasonality, and shifts (verdicts that flipped in the last 24h).

GET/v1/sounds/trendingno auth

Named audio carrying multiple current breakouts, ranked by the summed velocity of the distinct videos riding it.

GET/v1/creators/risingno auth

Creators recurring across current breakouts (2+ videos), velocity-ranked — who's producing winners now.

GET/v1/search/insightsno auth

Search-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.

json
// /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 }, … ] }
GET/v1/shop/trendingno auth

TikTok 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.

GET/v1/videos/momentumno auth

Videos 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.

GET/healthz/collectionno auth

Per-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.

GET/v1/storyboard?url=&density=
shell
curl "https://api.viralcli.com/v1/storyboard?url=https://youtu.be/abc&density=9" \
  -H "Authorization: Bearer sk_live_your_key" --output storyboard.jpg

Returns 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.

GET/v1/videos/{platform}/{video_id}

Identity + transcript + recent snapshots. 404 if the video hasn't been seen.

GET/v1/videos/{platform}/{video_id}/trajectory
json
{ "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

GET/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.

POST/v1/tracked-items
json
{ "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.

GET/v1/tracked-items

List your tracked items.

GET/v1/tracked-items/{id}
DELETE/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.

POST/v1/webhooks
json
// request
{ "url": "https://your-app.com/hooks", "event_types": ["*"], "tracked_item_id": null }
json
// 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.

GET/v1/webhooks

List your subscriptions (secrets are never returned again).

DELETE/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):

http
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:

python
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

TypeFires when
breakoutA tracked video's true velocity crosses the breakout threshold.
peakingVelocity was rising and turned down — the video is peaking now.
acceleratingVelocity is increasing beyond the acceleration threshold.
format_heatingA 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

python
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.

shell
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
json
// 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/analyze and /storyboard accept 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, and Cache-Control: no-store on 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:

shell
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 → Parquet

Tunable 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.