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
Free25503130YouTube7 days
Starter5001,000252120YouTube, TikTok90 days
Pro 2,5006,0001005300all1 year
Business12,00030,000500101,000allunlimited
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": 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.

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

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.

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.

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.