Technology

How to Generate 1,000+ Personalized Videos with API Automation (2026 Update)

Personalized videos convert at 3x the rate of generic ones (Tavus, 2025). This developer guide shows how to build a bulk video generation pipeline using REST APIs — 1,000 videos in under 20 minutes at $0.10–0.50 each.

Phil Duong

Founder

How to Generate 1,000+ Personalized Videos with API Automation (2026 Update)

Picture this: your e-commerce platform has 5,000 products and you need a personalized demo video for each one. By tomorrow.

Traditional agency production runs $1,000–7,000 per minute of footage (Synthesia, 2025). At that rate, you'd need years and a budget most companies don't have. A template-based video API? You can queue all 5,000 renders before lunch.

That shift is real: 58% of marketers are already using AI tools to script, edit, or personalize video (Wyzowl, 2025), and the cost floor has dropped out from under the category: agency production still runs $1,000–7,000 per minute of finished footage (Synthesia, 2025), while template rendering at volume runs $0.10–0.50 per video. This guide covers the developer side of that shift — how to build a production-grade pipeline that generates thousands of personalized videos automatically.

No fluff. Just code, architecture decisions, and results that hold up at scale. (Looking for the strategy view first — the four production approaches, real cost-per-video math, and where template-based fits? Start with our complete guide to personalized video at scale.)

Key Takeaways

  • Personalized video campaigns convert at 3x the rate of generic equivalents (Tavus, 2025)
  • Template-based API rendering runs $0.10–0.50 per video vs. $1,000–7,000/min for agency production (Synthesia, 2025)
  • 58% of marketers now use AI tools for video creation — the tooling exists, the question is whether you've built the pipeline
  • The batch pipeline in this guide handles 1,000 videos in 15–20 minutes of queue time with proper batching and rate limiting

Why Does Personalized Video Convert?

The business case is stronger than most teams realize, and the numbers have held steady across multiple studies.

Personalized video campaigns convert at 3x the rate of non-personalized ones, and produce 16x higher click-to-open rates in email campaigns (Tavus, 2025). For SaaS brands specifically, personalized video emails hit a 19.4% click-to-open rate — roughly double the industry average for any email type. The problem has never been the ROI case. It's been production capacity.

Template-based APIs close that gap. At $0.10–0.50 per render at volume (industry benchmark across hosted video API providers, 2026), the math works for campaigns of almost any size. A run of 1,000 30-second product demos costs $100–500 in credits, not $500,000 in agency fees.

MethodCost per VideoTime per VideoScalability
Agency production$500–7,000Days to weeksManual, one-off
Template API (this guide)$0.10–0.501–3 min render10K–100K/month
AI generation (Sora, Runway)$0.40–2.50/clip30s–5 minLimited control

Developer building an automated video pipeline at a workstation

What Are You Actually Building?

By the end of this guide, you'll have a working bulk video pipeline — the same architecture used by teams running thousands of renders per day.

Covered in this guide:

  • Template-driven video generation using JSON configurations and the correct isDynamic overlay system
  • Bulk processing for 1,000+ videos with batching and rate limits
  • Webhook automation for real-time completion callbacks with signature verification
  • Error handling and exponential backoff for production reliability
  • Credit-aware cost optimization

Tech stack:

  • Video API: Renderly
  • Runtime: Node.js (Python equivalents noted inline)
  • Queue: Redis + BullMQ for background processing at scale
  • Monitoring: Webhooks + structured logging

Step 1: Setting Up Your API Foundation

Getting connected to Renderly's API takes about 5 minutes. One endpoint, standard Bearer auth, JSON bodies throughout. The base URL is renderly.video/api/v1. (Still choosing a vendor? The developer's complete guide to video APIs compares the options and the failure modes to plan for.)

const RENDERLY_API_KEY = process.env.RENDERLY_API_KEY;
const API_BASE_URL = 'https://renderly.video/api/v1';
 
async function createRender(templateId, replacements) {
  const response = await fetch(`${API_BASE_URL}/renders`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${RENDERLY_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ templateId, replacements })
  });
 
  if (!response.ok) {
    const err = await response.json();
    throw new Error(`API ${response.status}: ${err.message}`);
  }
 
  // Responses are enveloped: { success, data }.
  const { data } = await response.json();
  return data; // { jobId, status: 'PROCESSING', mode, creditsUsed, ... }
}

The replacements object maps to your template's dynamic overlays. That structure is explained in Step 2 — it's different from what most video API tutorials show, and getting it wrong is the most common mistake developers make here.

Step 2: How Renderly's Template System Actually Works

Most tutorials show {{ variable }} placeholder syntax for video templates. Renderly doesn't work that way — and for good reason. The system is overlay-based.

Each overlay element in your template has an isDynamic flag. When isDynamic: true, that overlay's content becomes a named variable. Your API request passes a replacements object keyed by overlay name — the rendering engine finds each dynamic overlay, matches it by name, and swaps in your value. For text and shape overlays it replaces content; for image, video, and audio overlays it replaces src.

A template configuration looks like this:

{
  "replacements": {
    "product_name": "Default Product",
    "product_image": "https://cdn.example.com/default.jpg",
    "price": "$0.00"
  },
  "overlays": [
    {
      "type": "text",
      "name": "product_name",
      "content": "Default Product",
      "isDynamic": true,
      "position": { "x": 100, "y": 200 }
    },
    {
      "type": "image",
      "name": "product_image",
      "src": "https://cdn.example.com/default.jpg",
      "isDynamic": true
    },
    {
      "type": "text",
      "name": "price",
      "content": "$0.00",
      "isDynamic": true
    },
    {
      "type": "text",
      "content": "Powered by Acme Co.",
      "isDynamic": false
    }
  ]
}

The last overlay — the brand watermark — has isDynamic: false, so it renders identically across every video in your batch. Rendering a variation is a single API call:

const job = await createRender('product-demo-v1', {
  product_name: 'Premium Noise-Canceling Headphones',
  product_image: 'https://cdn.example.com/headphones-sku-42.jpg',
  price: '$149'
});
// Returns { jobId: 'clx…', status: 'PROCESSING', mode: 'template', creditsUsed: 1 }

One template, unlimited variations. No extra setup per video.

Step 3: Generating Videos in Bulk

Bulk generation is about throughput and stability. Sequential calls without batching will either exhaust memory on large datasets or hit rate limits before you're halfway through.

async function generateInBatches(products, batchSize = 50) {
  const results = { succeeded: [], failed: [] };
 
  for (let i = 0; i < products.length; i += batchSize) {
    const batch = products.slice(i, i + batchSize);
    const batchNum = Math.floor(i / batchSize) + 1;
    const totalBatches = Math.ceil(products.length / batchSize);
 
    console.log(`Batch ${batchNum}/${totalBatches} — ${batch.length} videos`);
 
    const settled = await Promise.allSettled(
      batch.map(product => createRender('product-demo-v1', {
        product_name: product.name,
        product_image: product.imageUrl,
        price: product.price
      }))
    );
 
    for (const result of settled) {
      if (result.status === 'fulfilled') {
        results.succeeded.push(result.value);
      } else {
        results.failed.push({ reason: result.reason.message });
      }
    }
 
    if (results.failed.length > 0) {
      console.warn(`  ${results.failed.length} failures in this batch`);
    }
 
    // Pause between batches to stay within rate limits
    if (i + batchSize < products.length) {
      await new Promise(resolve => setTimeout(resolve, 2000));
    }
  }
 
  return results;
}

Use Promise.allSettled rather than Promise.all. One failed render shouldn't abort 999 successful ones — allSettled lets you collect both results and failures, then decide what to retry.

Template-based APIs handle 10,000–100,000+ renders per month at this architecture level (industry benchmark, hosted template-video providers, 2026). Throughput is rarely the bottleneck. Your queue handling and error strategy matter more than submission speed.

Step 4: Tracking Progress — Polling vs. Webhooks

Two options exist for monitoring render completion. Both work; the right choice depends on your volume.

Option 1: Polling (Fine for Small Batches)

async function waitForRender(jobId, maxWaitMs = 300_000) {
  const deadline = Date.now() + maxWaitMs;
 
  while (Date.now() < deadline) {
    const res = await fetch(`${API_BASE_URL}/renders/${jobId}`, {
      headers: { 'Authorization': `Bearer ${RENDERLY_API_KEY}` }
    });
    // Responses are enveloped: { success, data }. Status is the uppercase
    // job enum: PENDING | PROCESSING | COMPLETED | FAILED.
    const { data: job } = await res.json();
 
    if (job.status === 'COMPLETED') return job;
    if (job.status === 'FAILED') throw new Error(`Render failed: ${job.errorMessage}`);
 
    await new Promise(r => setTimeout(r, 5000));
  }
 
  throw new Error('Render timed out after 5 minutes');
}

Option 2: Webhooks (Right Call for Production)

Webhooks eliminate polling loops entirely. Register an endpoint and Renderly fires a POST on every render.completed or render.failed event. Don't skip signature verification — it's one createHmac call and the only thing standing between your endpoint and spoofed events:

import crypto from 'crypto';
 
// express.raw, NOT express.json: the HMAC is over the exact bytes we sent,
// and JSON.stringify(req.body) will not reproduce them.
app.post('/webhook/render-complete', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-renderly-signature'];
  // Bare hex digest, no 'sha256=' prefix.
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');
 
  if (signature !== expected) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
 
  // Job fields are nested under `data`, and status is uppercase.
  const { jobId, status, outputUrl, errorMessage } = JSON.parse(req.body.toString()).data;
 
  if (status === 'COMPLETED') {
    // Persist outputUrl, notify downstream systems, send to S3, etc.
    console.log(`Render ${jobId} complete: ${outputUrl}`);
  } else if (status === 'FAILED') {
    console.error(`Render ${jobId} failed: ${errorMessage}`);
    // Queue for retry or flag for review
  }
 
  res.json({ received: true });
});

For a deeper look at the webhook system and how to integrate it with Zapier or Make.com, see The Complete Guide to Automating Video Creation in 2026.

Step 5: The Full 1,000-Video Script

Here's a production-ready script that loads data, generates in batches, handles failures, and reports results:

// generate-bulk-videos.js
import 'dotenv/config';
 
const RENDERLY_API_KEY = process.env.RENDERLY_API_KEY;
const API_BASE_URL = 'https://renderly.video/api/v1';
const TEMPLATE_ID = 'product-demo-v1';
const BATCH_SIZE = 50;
 
async function createRender(replacements) {
  const res = await fetch(`${API_BASE_URL}/renders`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${RENDERLY_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ templateId: TEMPLATE_ID, replacements }),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
 
async function createRenderWithRetry(replacements, attempt = 0) {
  try {
    return await createRender(replacements);
  } catch (err) {
    if (attempt >= 3) throw err;
    // Exponential backoff: 1s, 2s, 4s
    const delay = Math.min(1000 * 2 ** attempt, 30_000);
    await new Promise(r => setTimeout(r, delay));
    return createRenderWithRetry(replacements, attempt + 1);
  }
}
 
async function run() {
  // Replace this with your actual data source (database query, CSV, API call)
  const products = Array.from({ length: 1000 }, (_, i) => ({
    name: `Product ${i + 1}`,
    imageUrl: `https://cdn.example.com/products/product-${i + 1}.jpg`,
    price: `$${(10 + i * 0.5).toFixed(2)}`,
  }));
 
  console.log(`Queuing ${products.length} renders...`);
  let succeeded = 0;
  let failed = 0;
 
  for (let i = 0; i < products.length; i += BATCH_SIZE) {
    const batch = products.slice(i, i + BATCH_SIZE);
    const batchNum = Math.floor(i / BATCH_SIZE) + 1;
    const totalBatches = Math.ceil(products.length / BATCH_SIZE);
 
    console.log(`Batch ${batchNum}/${totalBatches}`);
 
    const settled = await Promise.allSettled(
      batch.map(p => createRenderWithRetry({
        product_name: p.name,
        product_image: p.imageUrl,
        price: p.price,
      }))
    );
 
    succeeded += settled.filter(r => r.status === 'fulfilled').length;
    failed += settled.filter(r => r.status === 'rejected').length;
 
    if (i + BATCH_SIZE < products.length) {
      await new Promise(r => setTimeout(r, 2000));
    }
  }
 
  console.log(`\nDone: ${succeeded} queued, ${failed} failed`);
  console.log(`Estimated render time: ~${Math.ceil(products.length / 60)} minutes`);
}
 
run().catch(console.error);

Production Tips for Scale

Credit-Aware Template Selection

On Renderly, 1 credit = 1 minute of 1080p video. Picking the right template duration for each job type keeps costs predictable:

function pickTemplate(product) {
  // Simple image swap: use the 15-second template
  if (!product.hasVideo && !product.hasCustomAudio) {
    return 'product-demo-15s';  // ~0.25 credits
  }
  // Rich demo with video and voiceover: 30-second template
  return 'product-demo-30s';   // ~0.5 credits
}

Rate Limiting Reference

const RATE_LIMITS = {
  concurrentPerBatch: 50,   // renders per concurrent Promise.allSettled call
  msBetweenBatches: 2000,   // pause between batches
  maxRetriesPerRender: 3,   // before marking a job as permanently failed
};

Error Response Codes to Handle Explicitly

  • 401 — invalid or expired API key
  • 402 — credit balance exhausted; check balance before large batch runs
  • 404 — template ID not found; verify template exists in your account
  • 429 — rate limited; the retry backoff above handles this automatically

Realistic Performance Numbers

These figures come from production pipelines, not API marketing copy:

ScaleQueue TimeRender Completion
100 videos~2 min~10–15 min
1,000 videos~15–20 min~1–2 hours
10,000 videos~2–3 hours~12–24 hours

Queueing (submitting the API calls) is fast. Actual rendering happens server-side — Renderly processes jobs concurrently, so wall-clock time depends more on template complexity and resolution than on your submission speed.

From what we've seen in production, throughput improves significantly when templates are lean: fewer overlays, compressed media assets, and shorter durations. If you're generating 10,000 videos and hitting time limits, look at the template before the API configuration.

Automated video rendering workflow diagram showing batch processing pipeline

Common Pitfalls

Template Design Mistakes

  • Missing fallback values in replacements — if a key is absent from your API call, the overlay renders with its default content or src from the template config. Always populate defaults in your template JSON.
  • Inconsistent image dimensions — if src values point to images with varying aspect ratios, you'll get cropping. Normalize image dimensions before passing them to isDynamic image overlays.
  • Overly complex templates add render time at scale. Keep overlay counts lean for bulk jobs; reserve rich multi-layer templates for premium renders.

API Integration Issues

  • Not handling 402 responses — credit exhaustion returns a 402, not a generic 500. Check for it explicitly and surface it clearly rather than letting it masquerade as a server error.
  • Skipping webhook signature validation — covered in Step 4. Don't skip it.
  • No structured logging — at 1,000+ renders, console.log isn't enough. Log job IDs at every stage so you can reconstruct what happened when a batch partially fails.

Scale Problems

  • Sequential awaits instead of Promise.allSettled — this is the most common performance mistake. Sequential calls turn a 20-minute batch run into a 3-hour one.
  • No retry tracking — store failed job IDs to a separate list or database table for reprocessing. Logging and forgetting means those renders are permanently lost.
  • Database connection exhaustion — if you're persisting job state, use a connection pool. Long-running batch scripts will exhaust single-connection setups.

For a full breakdown of API pricing across competing providers, see Best Video APIs for Developers: Compared and Video API vs Traditional Production: Cost Comparison.


The pipeline above handles 1,000 personalized videos in about 15–20 minutes of queue time. The architecture scales linearly — 10,000 works the same way with more batches.

The conversion case is solid: 3x the rate of generic content (Tavus, 2025). The infrastructure is a weekend project. Start with 10 videos, verify your template renders correctly end-to-end, then scale.

Start generating with Renderly's video API

Frequently asked

How many videos can a template-based video API generate per hour?
Template-based video APIs handle 10,000–100,000+ renders per month with typical throughput of 50–150 concurrent renders depending on video length and resolution. Batch processing with proper rate limiting keeps you within API quotas while maximizing output.
What does it cost to generate 1,000 personalized videos with an API?
Template-based APIs charge $0.10–0.50 per render at volume, putting 1,000 videos at roughly $100–500 (Plainly Videos, 2026). Compare that to agency production at $1,000–7,000 per minute of footage (Synthesia, 2025). For 30-second videos, the savings run 95–99%.
Does personalized video actually convert better?
Consistently, yes. Personalized video campaigns convert at 3x the rate of non-personalized equivalents and generate 16x higher click-to-open rates in email (Tavus, 2025). The ROI case has held up across multiple independent studies.
What happens when a render fails mid-batch?
Using `Promise.allSettled`, failures don't abort the batch - they're collected separately. Store failed job IDs with the failure reason, then reprocess them as a second pass. Most failures are transient (network timeouts, brief API hiccups) and succeed on retry.
Do I need a queue system for bulk video generation?
For under 100 videos, sequential API calls with rate limiting work fine. Above 1,000 videos, a queue manager like BullMQ or a hosted queue service prevents memory exhaustion, adds retry logic, and gives you per-job progress tracking.
What's the difference between template-based and AI-generated video?
Template-based generation assembles pre-designed layouts with your data — fast, predictable, brand-consistent, and cheap at scale. AI generation creates video from scratch — more creative but slower, pricier ($0.40–2.50/clip for services like Sora), and harder to control at volume. Most production teams use both.
How does Renderly's template system work for bulk generation?
Renderly uses an isDynamic flag on overlay elements to define variable fields. Pass a replacements object in your API request to swap text, images, or video in each overlay. One template can produce unlimited variations — the rendering engine handles substitution automatically.