Generate YouTube Thumbnails with JavaScript and Node.js
Most of the JS code I write against ThumbAPI lives in three places: a Node script that runs after I push a markdown file to my blog, a small CMS plugin a client uses to generate cover art on publish, and a one-off batch runner I keep on my desktop. The snippets below are the trimmed-down versions of those, with the stuff that only matters to my setup ripped out so you can paste them in and have something working in a few minutes.
Prerequisites
- Node.js 18 or higher (native
fetchis available without extra packages) - A ThumbAPI API key (get one free here)
No additional packages required. The examples below use Node's built-in fetch and fs modules.
Single Thumbnail — Minimal Example
The smallest useful version. One title goes in, one WebP lands on disk.
import fs from "fs";
const API_KEY = "your_api_key_here";
async function generateThumbnail(title) {
const response = await fetch("https://api.thumbapi.dev/v1/generate", {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
title,
format: "youtube",
imageStyle: "faceless",
outputFormat: "webp"
})
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
const base64 = data.image.split(",")[1];
const buffer = Buffer.from(base64, "base64");
fs.writeFileSync("thumbnail.webp", buffer);
console.log(`Saved — ${data.dimensions.width}x${data.dimensions.height}`);
}
generateThumbnail("How the Roman Empire Actually Collapsed");
Using a Custom Asset Dataset
If you've uploaded a style reference dataset to ThumbAPI, pass the asset ID to keep every thumbnail visually consistent with your channel:
async function generateThumbnail(title, customAssetsId = null) {
const body = {
title,
format: "youtube",
imageStyle: "faceless",
outputFormat: "webp"
};
if (customAssetsId) {
body.customAssetsId = customAssetsId;
}
const response = await fetch("https://api.thumbapi.dev/v1/generate", {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
return await response.json();
}
// With dataset
const result = await generateThumbnail(
"How the Roman Empire Actually Collapsed",
"m6XhjtZNdF0N2AFXUOiq"
);
Batch Generation from an Array
Processing a list of titles sequentially with a delay to respect rate limits:
import fs from "fs";
import path from "path";
const API_KEY = "your_api_key_here";
const OUTPUT_DIR = "./thumbnails";
const DELAY_MS = 2000;
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR);
const videos = [
{ title: "How the Roman Empire Actually Collapsed", format: "youtube" },
{ title: "The Deepest Cave Ever Explored", format: "youtube" },
{ title: "Why the Stock Market Crashes Every Decade", format: "youtube" },
];
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 60);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function generateOne(title, format, index) {
const response = await fetch("https://api.thumbapi.dev/v1/generate", {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
title, format, imageStyle: "faceless", outputFormat: "webp"
})
});
if (!response.ok) throw new Error(`${response.status}`);
const data = await response.json();
const buffer = Buffer.from(data.image.split(",")[1], "base64");
const filename = `${String(index).padStart(3, "0")}-${slugify(title)}.webp`;
fs.writeFileSync(path.join(OUTPUT_DIR, filename), buffer);
return filename;
}
async function runBatch() {
console.log(`Starting batch — ${videos.length} thumbnails`);
for (let i = 0; i < videos.length; i++) {
const { title, format } = videos[i];
try {
const filename = await generateOne(title, format, i + 1);
console.log(` Saved: ${filename}`);
} catch (err) {
console.error(` Failed: ${err.message}`);
}
if (i < videos.length - 1) await sleep(DELAY_MS);
}
}
runBatch();
Returning Base64 Directly (No File Write)
When you're integrating thumbnail generation into an API response or a CMS pipeline, you might not want to write to disk at all. Return the base64 string directly and pass it downstream:
async function getThumbnailBase64(title) {
const response = await fetch("https://api.thumbapi.dev/v1/generate", {
method: "POST",
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
title,
format: "youtube",
imageStyle: "faceless",
outputFormat: "webp"
})
});
const data = await response.json();
return data.image; // Full data URI, ready for <img src> or upload
}
// Use in an Express handler
app.post("/generate-thumbnail", async (req, res) => {
const { title } = req.body;
const imageDataUri = await getThumbnailBase64(title);
res.json({ thumbnail: imageDataUri });
});
TypeScript Version
If your project uses TypeScript, here's a typed implementation:
interface ThumbnailRequest {
title: string;
format: "youtube" | "blogpost" | "instagram" | "x";
imageStyle: "faceless" | "with-image" | "with-logo";
outputFormat?: "webp" | "png";
customAssetsId?: string;
}
interface ThumbnailResponse {
image: string;
format: string;
dimensions: {
width: number;
height: number;
};
}
async function generateThumbnail(
params: ThumbnailRequest
): Promise<ThumbnailResponse> {
const response = await fetch("https://api.thumbapi.dev/v1/generate", {
method: "POST",
headers: {
"x-api-key": process.env.THUMBAPI_KEY!,
"Content-Type": "application/json"
},
body: JSON.stringify(params)
});
if (!response.ok) {
const error = await response.text();
throw new Error(`ThumbAPI error ${response.status}: ${error}`);
}
return response.json() as Promise<ThumbnailResponse>;
}
Error Handling Reference
| HTTP Status | Meaning | What to do |
|---|---|---|
200 | Success | Decode and use the image |
401 | Invalid API key | Check your x-api-key header |
422 | Invalid parameters | Check format and imageStyle values |
429 | Rate limit hit | Add a delay and retry |
500 | Server error | Retry after a short wait |
That's the full shape of it. The single-thumbnail function is what I reach for inside an Express handler or a Next.js route; the batch loop is what I run from my terminal when a client sends me a CSV. If you're hitting 429s in a tight loop, bump DELAY_MS to 3000 before adding any retry logic, that fixes it nine times out of ten.

Written by
Aldin KozicaFull-stack developer from Bosnia and Herzegovina. I built ThumbAPI because I kept watching content teams waste hours on thumbnail design when the patterns are predictable enough to automate. The API is the tool I wished existed when building content pipelines for my own projects.
Continue Reading
Upload reference images once, then every thumbnail the API generates matches your channel's visual identity. Step-by-step guide with cURL, Python, and JavaScript examples.
Batch Thumbnail Generation for 100 Videos with PythonBuild a Python script that processes a CSV of video titles and generates production-ready thumbnails for all of them in a single run using ThumbAPI.
How to Automate YouTube Thumbnails for Faceless Channels with One API CallStep-by-step guide to automating faceless YouTube thumbnail generation with a single ThumbAPI POST request. Includes cURL, Python, and Node.js examples.
How to Auto-Generate YouTube Thumbnails with n8n (Step-by-Step)Build an n8n workflow that detects new YouTube uploads, generates a thumbnail with ThumbAPI, and saves it to Drive — fully automated, with batch and multi-platform patterns.
Generate Thumbnails With an API
Try ThumbAPI free. 5 thumbnail generations per month, no credit card required. One API call, production-ready output.