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 | 25 | 50 | 3 | 1 | 30 | YouTube | 7 days |
| Starter | 500 | 1,000 | 25 | 2 | 120 | YouTube, TikTok | 90 days |
| Pro ★ | 2,500 | 6,000 | 100 | 5 | 300 | all | 1 year |
| Business | 12,000 | 30,000 | 500 | 10 | 1,000 | 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": 2500, "enrich": 6000, "tracked": 100,
"concurrency": 5, "rate_per_min": 300,
"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": "…", "n": 45, "verdict": "HEATING",
"adoption_histogram": [ { "band": "0-7d", "n": 1, "median_vpd": 12633 }, … ],
"breakouts": [
{ "id": "0qnmNFoe7AY", "title": "…",
"views_per_day": 51789, "age_days": 365, "views": 18902807 }, …
] }verdict is one of HEATING / STEADY / SATURATING / INSUFFICIENT_DATA. Replay a seed's verdict over time with signals.
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.
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. |
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.