Quickstart
Drop any block onto a format timeline, expose its fields as API parameters, then render a fresh MP4 on demand — from any trigger you choose.
Time to first render: under 10 minutes.
1. Prerequisites
- Account & API key. Create an account at /sign-up, sign in, then go to Settings → API Keys and create a new key. Copy it immediately — it is shown only once.
export API_KEY="vam_live_<your-key>"
export BASE="https://your-domain.com/api/v1"
See Authentication for the full auth model.
- Brand kit (optional but recommended). A brand kit holds your logo, colors, and music. If you already have one, grab its
id. To create a minimal one:
curl -s -X POST "$BASE/brand-kits" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Brand"}' | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])"
Save the resulting id as $BK_ID.
2. Choose or create a block
Any block on a format timeline can be parameterized — built-in blocks (stat card, title card, quote, key-value, logo reveal, end card, outro CTA, countdown, labeled list, stat grid, photo) and custom user blocks alike. The only exception is transition ops (cuts, fades), which have no content surface.
Option A — use a built-in block. Skip to step 3. When you drop any built-in block onto a format timeline and edit one of its fields, the platform auto-creates a named API parameter for that field.
Option B — create a custom user block. A user block is a reusable canvas layout — cells for text, stats, images, and data tables — that you can drop into any format and whose fields behave identically to built-in block fields once dropped.
- Go to /user-blocks/new in the dashboard.
- Drag cells onto the canvas: text, stat, image, or data table.
- Give the block a descriptive name (e.g.
sports-recap).
You can also describe what you want in the AI chat panel and let the assistant build the layout for you.
Tip: Image-cell previews in the block editor may show placeholders until you save and reload — this is a known authoring-time limitation. The rendered MP4 uses the actual uploaded asset.
3. Mark fields as API parameters
Any cell field can be exposed as a named API variable. This is what lets you call the render API later with fresh data.
-
In the block canvas, click the gear icon (⚙) next to any cell field in the inspector panel on the right.
-
A "Mark as parameter" dialog appears. Give the field a human-readable name:
| Cell | Suggested parameter name | |---|---| | Athlete name (text) |
player.name| | Score (stat) |team.score| | Odds (stat) |vegas.spread| | Headshot (image) |player.photo| | Stats table (data table) |game.stats| -
Repeat for every field that should vary per render.
-
Save the block.
The block now has a defined parameter contract — a list of named, typed variables callers can supply.
4. Build a format
A format is a full video composition: intro block, content blocks, outro, music. It is built by dropping blocks onto a timeline — built-in blocks and custom user blocks mix freely.
- Go to /formats/new.
- Add intro and outro blocks from the palette if desired.
- Drag your
sports-recapblock onto the timeline. - An instance label prompt appears, pre-filled with
sports-recap-1. You can rename it to something meaningful (e.g.game1) — this prefix scopes the API variables exposed by this instance. Press Enter to confirm. - Give the format a name (e.g.
Daily Sports Recap) and save.
Note: Image-cell previews in the format editor may show placeholders until save-and-reload. The rendered MP4 uses the actual uploaded asset.
The format's slug (e.g. daily-sports-recap) is derived from the name and is used in all API calls.
5. Set author defaults
Author defaults are the values the format uses when the API caller sends nothing. They let you set safe fallbacks so the format always renders cleanly.
-
In the format editor at /formats/new, select the instance tile on the timeline.
-
Open the "API Parameters" tab in the right panel.
-
For each inherited parameter, set a default value:
game1.player.name→"Player Name"game1.team.score→0game1.vegas.spread→"Even"
-
Save the format.
Tip: Do this on the format's create page (
/formats/new). The format detail-page editor (/formats/:slug) currently shows a legacy pointer-override UI for these fields — the API Parameters tab parity is a near-term follow-up.
6. Discover the format schema
The schema endpoint tells you exactly what variables your format accepts, their types, and an example request body you can copy-paste.
curl -s "$BASE/formats/daily-sports-recap/schema" \
-H "Authorization: Bearer $API_KEY"
Response:
{
"slug": "daily-sports-recap",
"version": 1745276400000,
"durationFrames": 450,
"variables": [
{
"name": "game1.player.name",
"type": "text",
"required": false,
"defaultValue": "Player Name",
"sourceBlock": "sports-recap",
"deprecated": false
},
{
"name": "game1.team.score",
"type": "number",
"required": false,
"defaultValue": 0,
"sourceBlock": "sports-recap",
"deprecated": false
},
{
"name": "game1.vegas.spread",
"type": "text",
"required": false,
"defaultValue": "Even",
"sourceBlock": "sports-recap",
"deprecated": false
}
],
"exampleBody": {
"variables": {
"game1.player.name": "Player Name",
"game1.team.score": 0,
"game1.vegas.spread": "Even"
}
}
}
Whenever you add parameters or rename variables, re-fetch this endpoint to get the current contract. The version field increments on every format save, so you can detect staleness in your integration.
7. Call the render API
Send a POST with variables containing your live data. The platform resolves each named variable to the correct cell inside the format, enqueues a render job, and returns immediately.
curl -s -X POST "$BASE/formats/daily-sports-recap/renders" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"variables": {
"game1.player.name": "LeBron James",
"game1.team.score": 112,
"game1.vegas.spread": "-3.5"
}
}'
Response:
{
"id": "rj_01jt5m...",
"status": "queued",
"formatSlug": "daily-sports-recap"
}
Save the render id.
Variable scoping. Variables are prefixed by the instance label you set in step 4 (game1.*). If your format has only one instance, you can use the bare name shortcut — "player.name" instead of "game1.player.name" — and the platform resolves it automatically. If two instances expose the same bare name, the API returns 422 variable_ambiguous with the scoped alternatives in details.fields.
Error responses.
| Code | Meaning |
|---|---|
| 422 unknown_variable | Variable name not declared on this format — typo or stale integration |
| 422 variable_ambiguous | Bare name matches multiple instances |
| 422 missing_required_variable | A required variable was not supplied and has no default |
| 422 invalid_variable_type | Value type doesn't match the parameter's declared type |
8. Poll and download
Renders are asynchronous. Poll until status is completed or failed, then fetch the presigned download URL.
Poll:
curl -s "$BASE/renders/rj_01jt5m..." \
-H "Authorization: Bearer $API_KEY"
Response (in progress):
{ "id": "rj_01jt5m...", "status": "rendering", "progress": 0.6 }
Response (done):
{ "id": "rj_01jt5m...", "status": "completed", "progress": 1 }
Recommended polling interval: 3–5 seconds. Alternatively, use webhooks to receive a render.completed push instead of polling.
Download URL:
curl -s "$BASE/renders/rj_01jt5m.../signed-url" \
-H "Authorization: Bearer $API_KEY"
Response:
{
"url": "https://your-spaces-bucket.nyc3.digitaloceanspaces.com/renders/rj_01jt5m....mp4?X-Amz-..."
}
The URL is valid for 24 hours. Download the MP4, post it wherever you need it.
Full example (Node.js)
const BASE = "https://your-domain.com/api/v1";
const API_KEY = process.env.API_KEY!;
const FORMAT_SLUG = "daily-sports-recap";
async function renderSportsRecap(data: {
playerName: string;
teamScore: number;
vegasSpread: string;
}) {
// 1. Trigger render
const renderRes = await fetch(`${BASE}/formats/${FORMAT_SLUG}/renders`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
variables: {
"game1.player.name": data.playerName,
"game1.team.score": data.teamScore,
"game1.vegas.spread": data.vegasSpread,
},
}),
});
if (!renderRes.ok) {
const err = await renderRes.json();
throw new Error(`Render failed: ${JSON.stringify(err)}`);
}
const { id } = await renderRes.json();
// 2. Poll until done
while (true) {
await new Promise((r) => setTimeout(r, 3000));
const statusRes = await fetch(`${BASE}/renders/${id}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const job = await statusRes.json();
if (job.status === "completed") break;
if (job.status === "failed") throw new Error("Render failed");
console.log(`Progress: ${Math.round((job.progress ?? 0) * 100)}%`);
}
// 3. Fetch download URL
const urlRes = await fetch(`${BASE}/renders/${id}/signed-url`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { url } = await urlRes.json();
return url;
}
// Call it whenever you have new data
const mp4Url = await renderSportsRecap({
playerName: "LeBron James",
teamScore: 112,
vegasSpread: "-3.5",
});
console.log("MP4 ready:", mp4Url);
Next steps
- Rendering on demand — your code decides when and how to trigger renders. See the Rendering on demand recipe for patterns: cron jobs, webhooks, user actions, and batch loops.
- Webhooks — receive
render.completedpush events instead of polling. See Webhooks. - Interactive API reference — explore every endpoint with a live Try-it panel at /docs/api.