Turn a spreadsheet into one branded video per row with a single API call.
This example renders a personalized offer video for every customer in
data/customers.csv — their name, their product, their
photo, their coupon code — from one designed template, using Zvid's
bulk render endpoint.
Each 1080×1920 video greets the customer by name, showcases their product with a Ken Burns zoom and discount badge, then reveals their personal coupon code:
| Scene 1 — greeting | Scene 2 — product | Scene 3 — coupon |
|---|---|---|
| "HEY, AMIRA" + headline + 20% OFF chip | product photo, discount badge, name & tagline | "Your code is ready." + AMIRA-20 |
The same pattern powers e-commerce win-back campaigns, real-estate listing videos (one per property), sales outreach (one per prospect), HR onboarding (one per hire) — anywhere you have a table of people or things and want a video for each.
npm install
cp .env.example .env # put your API key in .env (app.zvid.io → API Keys)
npm run dry-run # validate every row + credit estimate — free
npm run sample # render the first 2 rows only
npm start # render all 4 sample rowsRequires Node.js ≥ 20. A successful run downloads the MP4s into out/ and
writes out/results.csv — a manifest mapping every CSV row to its video URL,
thumbnail, credits spent, and any error.
Personalized video campaign — 4 video(s) from customers.csv
Batch 1: blk_rkrJEvDXoCUszBZhpiKu — 4 job(s) queued, 44 credits reserved
4 done · 0 failed · 0 rendering — 41s
Downloading 4 video(s) to out/ ...
Done. 4 rendered — 44 credits reserved. Manifest: out/results.csv
The n8n/ folder has ready-to-import n8n workflows that do everything
this CLI does — CSV and Google Sheets sources, polling and
webhook-driven variants, with the same free dry-run, batching and retries —
built from n8n core nodes only, so they run on self-hosted n8n and n8n Cloud
alike. For native Zvid nodes (bulk render, validate, render-finished trigger),
install the official
@zvid/n8n-nodes-zvid
community package. Full instructions: n8n/README.md.
One HTTP request fans out into N render jobs:
payloadis the designed video (template.json) with{{variable}}placeholders in its text, colors, and image URLs, plus avariablesblock declaring defaults. You can pass a stored"template": "tpl_…"id instead — same request otherwise.itemsis your spreadsheet: each entry'svariablesmerge over the batch-levelvariables, which merge over the template defaults.- Validation is best-effort per item: valid rows queue immediately, broken
rows come back in the response's
itemErrorsarray with field-level messages — one bad row never sinks the batch. - The
202response returns abulkId; pollGET /api/render/bulk/{bulkId}until every job is terminal, or setZVID_WEBHOOK_URLto get a signed webhook per finished job instead. - Credits are reserved per job and automatically refunded for failed jobs.
- Up to 500 items per request (plan limits may be lower); the script batches
larger CSVs automatically (
--batch-size).
| File | Purpose |
|---|---|
template.json |
The designed video — 3 scenes, animations, transitions, music. Edit visuals here. |
campaign.json |
Batch-level variables: brand name, accent color, headline, music. |
data/customers.csv |
One row per video. Sample photos from Pexels & Unsplash. |
src/index.js |
CLI: load → validate → submit → poll → download → manifest. |
src/csv.js |
The file you edit — maps your columns to template variables. |
src/zvid.js |
Minimal API client (fetch + retries with backoff, typed errors). |
src/util.js |
.env loader, concurrency helper, streaming downloads. |
n8n/ |
Ready-to-import n8n workflows: CSV / Google Sheets / webhook variants. |
| Column | Template variable | Example |
|---|---|---|
first_name |
firstName |
Amira |
product_name |
productName |
The Field Watch |
product_tagline |
productMeta |
Sapphire glass · Italian leather |
product_image |
productImage |
any public http(s) image, ideally ≥ 1080×1920 |
discount_label |
discountLabel |
20% OFF |
coupon_code |
couponCode |
AMIRA-20 |
cta_url |
ctaUrl |
arcadia.example/amira |
To use your own spreadsheet, export it as CSV with these headers — or change
REQUIRED_COLUMNS and rowToVariables() in src/csv.js to
match your columns.
- Brand it: change
brandName,accentColor,headline, andmusicUrlincampaign.json— the accent color flows through every band, chip, and highlight automatically. - Keep copy lengths similar to the defaults (±30%) so the designed type scale and layout keep looking right; don't shrink font sizes to squeeze in longer text.
- Swap the design: any project JSON with
{{placeholders}}works astemplate.json. Build one visually in the Zvid editor, or start from a library example.condition(e.g.showOffer) anditerateare supported too. - Different output: change
resolution(full-hd,instagram-post, …) or add anoverridesobject to the submit call.
- Dry-run first:
npm run dry-runruns every row throughPOST /api/render/validate/api-key— free, returns field-level errors and the exact credit cost before you commit a large batch. - Polling vs webhooks: polling is simplest and what this script does; for
big batches or serverless callers, set
ZVID_WEBHOOK_URLand receiverender.completed/render.failedevents per job (HMAC-signed — see webhook docs). - Retries: the client retries 429/5xx/network errors with exponential
backoff and honors
Retry-After. Failed renders are not auto-retried by the script — they're listed inresults.csvwith their error; fix the rows and re-run just those. - Rate limits & queueing: bulk items count against your plan's render rate limits, and bulk jobs yield slightly to interactive renders — large batches are throughput-friendly by design.
- Exit codes:
0all rendered,1fatal (config/auth/credits),2partial (some rows failed or timed out) — CI-friendly.
Product photos by Adrian Regeci, Minh Tri, Philipp Aleev, Rendy Ramdani and Pavel Danilyuk on Pexels; Luke Peterson, Kiran CK and Beau Carpenter on Unsplash. Music from Pixabay.