Make a video with one request

Pick what you want below, copy that request, put in your key and a face — that's the whole job. ~20 minutes later you have a video URL.

The two keys

Variant Video API keyrequiredvv_…

Goes in the Authorization header of every request. Mint at /keys.

Variant API keyoptionalftk_…

Connects your Variant avatars — they present the videos and finished clips publish straight to their channels, at scale. Mint at variantmobile.com; goes in the variant block.

This key is required to use your existing Variant avatars.

Plus every video needs a face — a photo URL or that Variant avatar; the choices are spelled out below the examples.

Pick what you want to make

1 · A video about a company’s latest EARNINGS CALL
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "earnings-call",
    "ticker": "NKE",
    "photo_url": "https://.../face.jpg"
  }'

# Any US-listed company that holds earnings calls works.
# No ticker in mind? DELETE the ticker line -- it automatically
# covers the newest earnings call on a US listing.
2 · A video about a company’s latest SEC FILING
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "recent-event",
    "ticker": "HOG",
    "photo_url": "https://.../face.jpg"
  }'

# Works when the company has a recent filing worth covering; if all
# its filings are routine you get a clear refusal, not a bad video.
# DELETE the ticker line and it covers the newest notable filing
# anywhere in the market.
3 · A fast ~10 second ALERT on one ticker
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "catalyst",
    "ticker": "HOG",
    "photo_url": "https://.../face.jpg"
  }'
# ticker is REQUIRED here. Fastest format (~5-8 min).
4 · A video from YOUR OWN TEXT (your reference material goes in "notes")
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "talking-head",
    "photo_url": "https://.../face.jpg",
    "notes": "PASTE YOUR REFERENCE TEXT HERE. The model writes the
      script and the title from this text and nothing else."
  }'

# "notes" works on every presenter format. On catalyst /
# recent-event / earnings-call it REPLACES the automatic research.
# Want your exact words instead? Use "script" -- it is spoken
# verbatim and the model never runs.

# Same call with "template": "trade-video" puts the presenter ON
# LOCATION: the face is re-staged into a scene before it is animated.
# Add "scene": "beach", "limo" or "penthouse" to pick the spot
# (blank = beach), "ticker" for the $TICKER chip, and "platform"
# (e.g. "Metamask") for the TRADED ON badge. Best kept short:
#   "script": "Just bought some ETH. Come ride the wave with me."

# Or skip writing anything: send the raw trade and the farm's model
# writes the alert line itself (direction-guarded, ~24 words), and
# the chip, badge and title fill in from the facts:
#   "template": "trade-video", "scene": "limo",
#   "trade": {"platform": "MetaMask", "asset_in": "ETH",
#             "asset_out": "USDC", "side": "buy", "chain": "ethereum"}
5 · A bull-vs-bear FIGHT CARD (uses your Variant insights)
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "showdown-no-lipsync",
    "ticker": "HOG",
    "faces": { "bull": "https://.../a.jpg", "bear": "https://.../b.jpg" },
    "winner": "bull",
    "variant": { "api_key": "ftk_YOUR_VARIANT_KEY", "publish": false }
  }'

# Only works for tickers YOUR Variant insights pipeline has covered.
# See which those are:
#   curl https://variantmobile.com/api/insights -H "Authorization: Bearer ftk_..."
# Or DELETE the ticker line to use your newest insight.

Get the finished video

The reply to every request above is a receipt with an id
{ "id": "74685195-...", "status": "running", ... }

# come back for it (~20 min):
curl https://variantvideo.com/api/v1/videos/74685195-... \
  -H "Authorization: Bearer vv_YOUR_KEY"

{ "status": "done",
  "video_url": "https://res.cloudinary.com/.../final.mp4" }

# or never poll: add "webhook_url": "https://your-site/hook" to the
# request and we POST you this same record when it finishes.

The face — pick exactly one

Option A — a photo

Put an image link in photo_url (or upload in the studio). The person in the photo becomes the presenter, lip-synced to the narration.

Option B — your Variant avatar

Put your Variant key in "variant": {"api_key": "ftk_…"} and skip photo_url. Your avatar's face becomes the presenter and its handle appears in the corner. Add "avatar_mode": "cartoon" for the cartoon face.

Neither = refused

If you send no photo and no Variant key, the request is refused with a clear message. A video is never given a default face.

The corner label — is_general

Videos normally show a small label top-left: the handle, tier and ticker chip. Add "is_general": true and the label is gone entirely — a clean, anonymous video.

Making a general clip for a Variant channel? The one flag to set is as_channel. Publishing with "variant": { …, "as_channel": true, "channel_id": "cha_…" } does both halves at once: Variant plays the post faceless (its feed hides the creator block), and the video automatically renders with is_general on so no identity is in the pixels either. You only set is_general by itself when you want a clean video without publishing it anywhere.

Posting it to Variant — three choices

Don't post

Leave the variant block out (or add "publish": false inside it if you only needed the key for the face or the fight card's material). You just get the video URL.

Post as your avatar

Include "variant": {"api_key": "ftk_…"}. The finished video posts to your avatar's channel under its name and its followers are notified.

Post as a channel

Add "as_channel": true and a "channel_id" — the video belongs to the channel, no person's name on it. Variant plays channel posts faceless (no creator block at all), so the video automatically renders as general too — no identity bar in the pixels. Pass "is_general": false only if you want the bar anyway. List your channel ids: curl variantmobile.com/api/channels -H "Authorization: Bearer ftk_…"

Posting to Variant is a granted capability on the Variant side — the render always works; only the posting step needs the grant.

POST/api/v1/videos

The request from step 1, precisely. Only template is required.

Field
templaterequiredWhich format to render. One of: earnings-call, recent-event, catalyst, catalyst-split, social-clip, chart-head, talking-head, talking-head-split, trade-video. What each one makes is in the Templates table below.
tickersee leftWhich company, e.g. "NKE". catalyst, catalyst-split and chart-head: required. social-clip: required unless tweet_url is given — the post then carries the clip and no chart is drawn. earnings-call: any US company with calls; leave it out to cover the newest call. recent-event: needs a recent notable filing (refused otherwise); leave it out for the newest one in the market. Fight card: only tickers your Variant insights cover — list them at variantmobile.com/api/insights with your ftk_ key, or leave it out for your newest insight.
voiceoptionalNarrator. Any id from the Voices table; default af_sarah.
photo_urlsee lefthttps URL of a face photo (JPEG/PNG/WebP) for the presenter. Required unless variant.api_key is given — then the Variant avatar's own face (and its handle on the identity bar) is used. A render is never given a default face.
tweet_urloptionalsocial-clip only: an X post URL rendered as a card on the clip; the narration is written as a reaction to the post. With a ticker the card sits above the chart; without one the card takes the chart's place and no ticker is needed.
notesoptionalYour own material: the model writes the script and title from it instead of fetching from the feeds. Required (or script) for talking-head and trade-video.
backgroundoptionaltalking-head-split only: the gradient under the presenter — blue (default), green, red, black, purple or orange. Every choice is the same pale-into-deep treatment at a different hue.
sceneoptionaltrade-video only: where the presenter is filmed — beach, limo or penthouse. Blank picks the beach.
platformoptionaltrade-video only: renders a TRADED ON {PLATFORM} badge beside the ticker chip (e.g. "Metamask").
tradeoptionaltrade-video only: a facts object ({platform, asset_in, asset_out, side, chain, market}) — the farm writes the trade-alert line from it. Beats notes; script beats both.
scriptoptionalA finished script, spoken verbatim — skips the model entirely.
avatar_modeoptional"realistic" (default) or "cartoon" — which of the Variant avatar's faces to use when the key supplies the face.
music / music_trackoptionaltrue adds a bed from the farm's pool (GET /api/music lists tracks); music_track names one, blank picks randomly. The self-sourced formats carry their own bed and ignore this.
username / channel / leveloptionalOverride the identity bar's handle, channel line and tier band; blank uses the Variant avatar's identity when a key is given.
resolution / rangeoptional"480p" (default) or "720p"; chart range "1y" (default), "6m" or "2y". 720p needs a ~90 GB+ GPU: the hosted farm renders 480p and refuses 720p at submit; the option exists for larger self-hosted machines.
is_generaloptionaltrue removes the identity bar (handle, channel, tier, ticker chip) from the frame entirely.
webhook_urloptionalhttps URL that receives one POST when the render finishes or fails.
facesfight cardshowdown-no-lipsync only: one https image URL per side — {bull, bear}. The photos become the generated fight artwork.
voicesoptionalFight card: a voice id per side, e.g. {"bull": "am_adam"}. Omitted sides get distinct defaults so the debate never comes out in one voice.
winneroptionalshowdown-no-lipsync only: "bull" or "bear"; default bull.
variantoptionalPublish the finished video to Variant — see Publish to Variant below. For the cast formats the key is also required as the source of their bull/bear material; add "publish": false to use it for material only.
Example — cover whatever is newest: no ticker at all
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template": "recent-event",
       "photo_url": "https://.../face.jpg"}'
Example — bring your own material: the model writes the script from your notes
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "template": "catalyst",
    "ticker": "HOG",
    "photo_url": "https://.../face.jpg",
    "notes": "Harley just raised full-year guidance on the strength of
      its touring lineup. The market still prices it like a shrinking
      brand, but dealer inventories are the leanest in a decade."
  }'
# with "notes", nothing is fetched from the feeds -- the script and
# title are written from YOUR text. Works on catalyst, social-clip,
# chart-head, talking-head, trade-video, recent-event and earnings-call.
Example — your exact words, no model at all
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "template": "talking-head",
    "photo_url": "https://.../face.jpg",
    "script": "Three things matter in this market right now. Rates,
      earnings breadth, and positioning. Let me take them in order."
  }'
# "script" is spoken verbatim -- the model never touches it
Example — a fast ~10s alert on one ticker
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"template": "catalyst", "ticker": "HOG",
       "photo_url": "https://.../face.jpg"}'

Get the result

GET/api/v1/videos/{id}

status walks queued → running → done (or failed, with error). When done: title, video_url, thumbnail_url, published_to_variant. Someone else's id is a 404, indistinguishable from one that never existed.

GET/api/v1/videos

Your 50 most recent renders, newest first, under videos.

Templates

The values that go in template — and for each one, exactly what happens to the ticker you send.

earnings-callyour ticker — or the newest call

What it makes: a ~30s violet-themed video about the company's latest earnings call — presenter over a stock chart, animated fact cards, and a quote card.

What we do with your ticker: we find the company's newest earnings call transcript, cut it to management's prepared remarks (the analysts' Q&A and the boilerplate are dropped), and our model writes the narration, the title and three on-screen facts from those remarks only. The quote is never written: a real sentence is lifted verbatim from the transcript with the executive's name on it. Leave the ticker out and it covers the newest earnings call on a US listing.

Use it for: earnings-day coverage of any company on your watchlist — {"template": "earnings-call", "ticker": "NKE"} the morning after Nike reports.

recent-eventyour ticker — or the newest catalyst

What it makes: a ~30s forest-green video about the company's most recent notable SEC filing — headline card, presenter over a chart, animated points, quote card.

What we do with your ticker: we pull the company's recent filings from an events feed that grades each one (a guidance change or a CFO departure is a catalyst; a routine proxy is not) and take the newest one worth covering. The narration, title and points are written from that filing's analysis; the quote is copied verbatim from the press release the company attached to the filing on the SEC's own site, with the executive's name. If every recent filing is routine we refuse with a clear error rather than narrate nothing. Leave the ticker out and it covers the newest confident catalyst in the whole market.

Use it for: reacting to news — an 8-K lands, you submit the ticker, the explainer exists 20 minutes later.

catalystticker required

What it makes: a ~10 second alert: what just happened, and why it matters. The fastest format (~5–8 min).

What we do with your ticker: same events feed as recent-event — the ticker's most recent graded catalyst — but compressed to a two-sentence spoken alert instead of a full explainer. Refused at submit if the ticker has no current catalyst.

Use it for: high-frequency posting — a stream of quick hits across a portfolio.

catalyst-splitticker required

What it makes: the same ~10 second alert, split in half — the top half is an 8-second cinematic establishing shot generated for this event, looping over the alert; the bottom half is the presenter on a soft blue gradient, with a black VARIANT / variantmobile.com channel bar riding the seam between the halves.

Where the shot comes from: the pass that writes the alert also writes a purely-visual b-roll direction for the company and its industry, and a video model renders it in a fixed house style — glossy 3D-animated, cinematic, with no text, logos, brand names or people talking. It generates while the avatar renders, so the clip costs little extra time.

Use it for: the alert with production value — when the quick hit should look like a produced segment, not a template.

social-clipticker or X post

What it makes: a ~15s clip of the presenter over the company's live one-year stock chart, with captions.

What we do with your ticker: the chart is drawn from live market data for that symbol, and the narration is a short take on the company written by our model. Pass tweet_url and the post is pinned above the chart as a card — and the narration is written as a reaction to that post instead of a generic take.

No ticker? An X post alone carries the clip: the card takes the chart's place, the identity bar drops its ticker chip, and the presenter reacts to the post. One of the two is required — with neither there is nothing to put on the page.

Use it for: a quick market-commentary clip when there is no specific filing to point at — or a straight reaction to someone's post.

chart-headticker + notes or script

What it makes: the presenter over the ticker's live chart on a slate field — a navy gradient page with a steel-blue glow behind the presenter, the chart card in a matching dark theme, and the identity bar in white. Captions, closing splash, 9:16.

What we do with your ticker: only the chart — it is drawn from live market data for that symbol (span withrange: 1y, 6m or 2y). Every spoken word is yours: send notes (the model writes the script and title from them) or script (spoken verbatim). No feeds are touched.

Use it for: your own commentary on a name — the take is yours, the chart keeps it anchored to the tape.

showdown-no-lipsynccast + variant key

What it makes: the fight card — a bull-versus-bear debate as generated arcade-style artwork per round, each side's claims spoken over it in its own voice, and a declared winner at the end.

Where the material comes from: the Variant insights feed — the two-and-two bull and bear case for the ticker — so variant.api_key with insights access is required. Each side's argument is written by our model from its side's points. Leave ticker out and the newest insight is covered.

What you supply: two face images (they become the fight artwork), optionally a voice per side, and the winner.

The fight card carries no identity bar by construction — no handle, channel or tier appears on it, only the ticker and company being debated. is_general is accepted and changes nothing here; it exists for the presenter formats.

A fight card
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{
    "template": "showdown-no-lipsync",
    "ticker": "HOG",
    "faces": {
      "bull": "https://.../bull.jpg",
      "bear": "https://.../bear.jpg"
    },
    "voices": { "bull": "am_michael", "bear": "af_heart" },
    "winner": "bull",
    "variant": { "api_key": "ftk_YOUR_VARIANT_KEY", "publish": false }
  }'

Example output: a fight card. The lip-synced three-hander (showdown, example) is studio-only — three avatar generations per render is the farm's heaviest job, so it stays off the programmatic door.

talking-headnotes or script required

What it makes: the simplest thing that works — your presenter over a flat background, speaking your material, with captions. No ticker needed.

What you supply: notes (the model writes the script and title from them) or script (spoken verbatim), plus a face — photo_url or your Variant avatar via the key.

talking-head-splitnotes or script + colour

What it makes: the split-screen frame — an 8-second cinematic establishing shot generated from your material loops in the top half, your presenter speaks in the bottom half, and the channel band rides the seam. No branded opener.

What you supply: notes orscript plus a face, like talking-head — no feeds are touched, and a ticker is optional (it becomes the chip). Pick the page with background: blue (default), green, red, black, purple or orange — every choice is the same pale-into-deep gradient at a different hue. With a verbatim script, one small model pass still writes the shot's visual direction from it.

Use it for: produced-looking commentary on anything — your words carry the clip, the generated footage sets the scene.

trade-videonotes or script required

What it makes: your presenter announces a move on location — the face you supply is re-staged into the chosen scene before it is animated, so the whole frame is the shot. Full-bleed 9:16 with captions and the closing splash. No ticker needed. Built for short clips ("just bought some ETH, come ride the wave"): the render time tracks the length of what is said.

The scenes (pass one as scene; blank picks the beach): beach — a lounge chair on a sunny tropical beach, laptop on their lap, waves rolling in behind them. limo — the back of a sleek black limousine at night, city lights streaking past the window. penthouse — a rooftop terrace over a glittering skyline at dusk.

What you supply (one of three, in priority order): script is spoken verbatim; trade is a facts object the farm's own model turns into a punchy first-person trade-alert line (direction-guarded, ~24 words); notes is free text the model narrates. Plus a face — photo_url or your Variant avatar via the key — and optionally the scene.

The trade object: {"platform", "asset_in", "asset_out", "side", "chain", "market"} — asset_in is what was bought, asset_out what was sold, side is buy/sell/long/short. A swap ("I swapped USDC for Ethereum") needs both assets; a plain position needs asset_in or market. When you send trade facts, the $TICKER chip and the TRADED ON badge fill themselves from them (explicit ticker/platform still win), and the video's title is derived from the facts ("USDC → ETH", "Long MON").

The top bar: the same identity strip as every format — handle, channel, tier and the $TICKER chip (pass ticker to show one, e.g. ETH) — plus this format's own TRADED ON badge: pass platform (say, "Metamask") and it renders beside the chip. Publishing with as_channel keeps the clip general and drops the identity bar, same as everywhere else.

Good to know: the face in the finished video is the face you sent, preserved through the scene change. A clear, well-lit source photo makes a better scene.

Programs that pick a template at runtime can fetch this catalog as JSON from GET /api/v1/templates — you never need it just to render.

Voices

The values that go in voice. All 23 installed; American then British, female then male within each.

Voice
af_sarahAm. female · defaultMeasured and articulate, reads as an analyst.
af_heartAm. femaleWarm and even, a safe default for most avatars.
af_bellaAm. femaleBright and energetic, good for punchy takes.
af_nicoleAm. femaleSofter and closer-mic'd, more intimate.
af_skyAm. femaleLighter and younger sounding.
af_novaAm. femaleClear and neutral, little colour.
af_aoedeAm. femaleSmooth mid-range, unhurried.
af_koreAm. femaleFirm and grounded.
am_michaelAm. maleSteady and credible.
am_adamAm. maleDeeper, slower, more authoritative.
am_ericAm. maleCrisp and businesslike.
am_onyxAm. maleLow and resonant, heavier delivery.
am_liamAm. maleYounger and more conversational — the natural pick, and the default male voice.
am_fenrirAm. maleGruff, with more edge.
am_puckAm. malePlayful and quick, less formal — the energetic pick.
bf_emmaBr. femaleComposed and newsreaderly.
bf_aliceBr. femaleLighter and brisker.
bf_isabellaBr. femaleRicher and slower.
bf_lilyBr. femaleSoft and youthful.
bm_georgeBr. maleDeep and measured.
bm_danielBr. maleNeutral and even.
bm_lewisBr. maleWarmer with more body.
bm_fableBr. maleStorytelling cadence.

Publish to Variant

Add a variant object to the create request and the finished video is published into Variant (variantmobile.com) automatically. This is separate from the vv_ key: it uses your Variant API key (ftk_…, minted on Variant's API page), and on Variant the key is the identity — it publishes as whichever avatar it was minted for.

Granted capability. Variant's pre-made publish endpoint is restricted — your Variant account needs publish access granted in addition to the key. Without the grant the render still succeeds; only the publish step is refused.
Field
variant.api_keyrequiredYour Variant key (ftk_…). Decides which avatar it publishes as.
variant.as_channeloptionaltrue publishes as the channel itself — general content, no personal attribution. Also implies is_general, so no handle is burned into the frame.
variant.channel_idsee leftRequired when as_channel is true; otherwise optional (defaults to the avatar's first channel).
variant.title / badge / badge_coloroptionalOverrides for the Variant post; blank title uses the generated one.

trade-video publishes losslessly. When the template is trade-video, the publish call carries the trade metadata with it — type: "TRADE", tradePlatform (from platform or the trade facts) and tradeAssets (from the facts, or the ticker) — so the post lands in the Variant feed as a TRADE with its venue buttons lit, not as a generic upload.

Example — render and publish as the key's avatar
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "recent-event",
    "ticker": "HOG",
    "variant": { "api_key": "ftk_YOUR_VARIANT_KEY", "badge": "CATALYST" }
  }'
# no photo_url needed here: the Variant avatar's own face is used
Example — publish as a channel, no personal attribution
curl -X POST https://variantvideo.com/api/v1/videos \
  -H "Authorization: Bearer vv_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "earnings-call",
    "variant": {
      "api_key": "ftk_YOUR_VARIANT_KEY",
      "as_channel": true,
      "channel_id": "cha_..."
    }
  }'

Webhooks

Give webhook_url at submit and we POST the finished record on done or failed. Delivery is retried: a failed or timed-out attempt (8-second leash each) is tried again after 30 seconds, 2 minutes, and 10 minutes before we give up. Make your endpoint idempotent — a retry can replay a delivery you already processed. If all attempts fail, the status endpoint still has everything.

What arrives
POST your-endpoint
Content-Type: application/json

{ "id": "…", "status": "done", "template": "earnings-call",
  "ticker": "NKE", "title": "…",
  "video_url": "https://res.cloudinary.com/…/final.mp4",
  "thumbnail_url": "…", "error": null,
  "published_to_variant": true,
  "variant_video_id": "vid_…" }

# variant_video_id is the id Variant returned when the farm published
# there for you (null when it didn't publish) — link your records to
# the live post without re-ingesting anything.

Limits & errors

Free during the beta: 12 renders per hour, 40 per day. A render is minutes of real GPU — the limits are honest, not decorative. Paid tiers with higher limits come later; the API will not change shape when they do.

Code
400Bad input; the body says what — a missing template, a non-https webhook_url, or nothing to cover: {"error": "no recent catalyst for MGM: …"}
401Missing, wrong, or revoked key.
404Not your video id.
429{"error": "rate limit: 12 renders per hour on Free (beta)"}
502The render farm refused; detail says why. The video row stays visible as failed.
A transient infrastructure failure (an out-of-memory moment, an unreachable upstream, a timeout) is retried once automatically before anything is surfaced — you only ever see terminal outcomes, and finished stage work is reused so the retry is cheap. Deterministic refusals are never retried.

Run it yourself instead

The entire pipeline — models, studio, this same API — is a pip install for your own GPU. Fastest renders, no queue, no limits, yours to modify. Setup lives in the docs.

pip install video-gen-local
vgl setup     # fetches models, checks the GPU
vgl serve     # the studio and API on your machine