Contents
Are you looking to use text to speech to add an AI voice over to your video? Do you want an easy-to-use API that adds the narration and matching captions for you? Then this post has all that you need.
Some common use cases where you might want to generate a video with an AI voice over include:
- Adding a realistic voice to news videos
- Making tutorials and how-to videos more informative and accessible
- Adding voice commentary to promotional videos
- Creating informative voice overs for educational presentations
AI is now a normal part of video production. According to Wyzowl’s State of Video Marketing 2026, 63% of video marketers have used AI to create or edit video.
I will show you how to add an AI voice over and synced captions to a video using Shotstack’s video editing API. It is a step-by-step, easy-to-follow guide, so even those with no prior programmatic video editing experience can automate videos with professional-sounding AI narration.
TL;DR: Put your script in the prompt of an audio asset, choose a speech model and a voice, and add a rich-caption clip that points at it with alias://. One render request generates the voice, captions it word by word and mixes it over your video.
How AI voice overs work in Shotstack
Shotstack is an API-driven video automation platform for creating, editing and distributing videos at scale. With the Shotstack Edit API, the voice over is part of the edit itself. You describe the video in JSON, and the speech is generated while the video renders.
That means there is no separate text-to-speech job to start, check and download. You send one request and get back a finished video with the voice, the captions and the footage already mixed together.
You can choose from ElevenLabs, MiniMax and Amazon Polly speech models. You don’t need an account with any of them: generation is billed in Shotstack credits, from the same balance as rendering.
Prerequisites
Before you start, sign up for Shotstack and copy your production API key from the dashboard. You’ll send it in the x-api-key header of every request. You should also be familiar with running cURL commands from the command line.
Rendering and generated speech both use credits. See pricing for the rates.
Describe the video and the voice over in JSON
To make the example more realistic, we’ll mock up a weather report. An ElevenLabs voice reads the script, the words are captioned as they are spoken, and the narration plays over aerial footage of a coastline.
Create a file named video.json and paste the following JSON into it:
{
"timeline": {
"tracks": [
{
"clips": [
{
"asset": {
"type": "rich-caption",
"src": "alias://voiceover",
"font": {
"family": "Roboto",
"size": 38,
"weight": 700,
"color": "#ffffff"
},
"stroke": {
"width": 3,
"color": "#000000"
},
"active": {
"font": {
"color": "#fcba03"
}
},
"animation": {
"style": "highlight"
}
},
"start": 0,
"length": "end",
"width": 1100,
"height": 150,
"position": "bottom",
"offset": {
"y": 0.05
},
"fit": "none"
}
]
},
{
"clips": [
{
"alias": "voiceover",
"asset": {
"type": "audio",
"prompt": "Moving down to the Central area we are seeing clear skies, a gentle breeze and mild temperatures, perfect for an evening stroll. Temperatures are hovering around a comfortable 55 degrees Fahrenheit, the ideal weather for outdoor activities.",
"model": "elevenlabs-multilingual-v2",
"options": {
"voice": "George"
}
},
"start": 0,
"length": "auto"
}
]
},
{
"clips": [
{
"asset": {
"type": "video",
"src": "https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/table-mountain.mp4",
"volume": 0
},
"start": 0,
"length": "alias://voiceover",
"transition": {
"in": "fade",
"out": "fade"
}
}
]
}
]
},
"output": {
"format": "mp4",
"resolution": "hd"
}
}The edit has three tracks. The first track in the array is the top layer, so the captions sit over the footage:
- Captions: a
rich-captionclip with"src": "alias://voiceover". Shotstack transcribes the voice over and shows each word as it is spoken, with the active word highlighted in yellow. - Voice over: an
audioclip with the script inprompt, theelevenlabs-multilingual-v2model and theGeorgevoice."alias": "voiceover"lets other clips refer to it, and"length": "auto"makes the clip as long as the generated speech. - Footage: a
videoclip with"length": "alias://voiceover", so the footage ends when the voice over does."volume": 0mutes the clip’s own sound.
Nothing in this edit has a fixed duration. The length of the speech isn’t known until it is generated, so every clip takes its length from the voice over. Change the script and the video still fits.
Render the video
Now send a POST request to the Edit API with the JSON file. Run the following command with your own API key in place of YOUR_API_KEY:
curl -X POST \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d @video.json \
https://api.shotstack.io/edit/v1/renderExpect a response like this:
{
"success": true,
"message": "Created",
"response": {
"message": "Render Successfully Queued",
"id": "6623d367-2b2c-438f-8e69-c06d6509a46c"
}
}Copy the id from the response. You’ll use it to check the status of the render in the next step.
Check the status of the render
The render takes a little longer than a plain edit, because the voice over is generated first. Wait a few seconds, then run the command below with the id from the last response:
curl -X GET \
-H "x-api-key: YOUR_API_KEY" \
https://api.shotstack.io/edit/v1/render/6623d367-2b2c-438f-8e69-c06d6509a46cWhile the speech is being created, status is generating. It then moves through preprocessing and rendering to done. Resend the same request until it reads done:
{
"success": true,
"message": "OK",
"response": {
"id": "6623d367-2b2c-438f-8e69-c06d6509a46c",
"owner": "5ca6hu7s9k",
"plan": "payg",
"status": "done",
"error": "",
"duration": 14.47,
"billable": 14.47,
"renderTime": 7168.65,
"credits": 0.24,
"url": "https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/5ca6hu7s9k/6623d367-2b2c-438f-8e69-c06d6509a46c.mp4",
"poster": null,
"thumbnail": null,
"created": "2026-11-09T15:29:01.549Z",
"updated": "2026-11-09T15:31:12.565Z"
}
}When the status is done, open the link in the url field to watch or download the MP4 file. If the status is failed, the error field says why.
Here is the final weather video:
The credits field shows what the render used, rendering and generation together. The first render of this 14-second video used 0.39 credits: 0.24 for rendering and 0.15 for the voice over. Rendering the same script again reused the speech already generated, so the second render used 0.24.
Change the voice
To try another voice, change voice in options. To try another model, change model and pick one of that model’s voices. For example, Amazon Polly’s Matthew voice with newscaster delivery suits a weather report:
{
"alias": "voiceover",
"asset": {
"type": "audio",
"prompt": "Moving down to the Central area we are seeing clear skies, a gentle breeze and mild temperatures, perfect for an evening stroll. Temperatures are hovering around a comfortable 55 degrees Fahrenheit, the ideal weather for outdoor activities.",
"model": "polly-neural",
"options": {
"voice": "Matthew",
"newscaster": true
}
},
"start": 0,
"length": "auto"
}The AI speech generation guide lists every model, voice and option. Write the script in the language you want spoken, and spell out numbers and abbreviations the way they should be read.
Generate voice overs from your own code
cURL is fine for one video. In an application, you’ll build the edit in code for each script. Here is the same video in Node.js 18 or later, with no dependencies: one function builds the edit, and the other renders it and waits for the result.
// voiceover.mjs: adds an AI voice over and synced captions to a video in one render.
// Run with: SHOTSTACK_API_KEY=your-key node voiceover.mjs (Node 18 or later)
const API = 'https://api.shotstack.io/edit/v1';
const headers = {
'Content-Type': 'application/json',
'x-api-key': process.env.SHOTSTACK_API_KEY,
};
function buildEdit({ script, videoUrl, voice }) {
return {
timeline: {
tracks: [
{
clips: [
{
asset: {
type: 'rich-caption',
src: 'alias://voiceover',
font: { family: 'Roboto', size: 38, weight: 700, color: '#ffffff' },
stroke: { width: 3, color: '#000000' },
active: { font: { color: '#fcba03' } },
animation: { style: 'highlight' },
},
start: 0,
length: 'end',
width: 1100,
height: 150,
position: 'bottom',
offset: { y: 0.05 },
fit: 'none',
},
],
},
{
clips: [
{
alias: 'voiceover',
asset: {
type: 'audio',
prompt: script,
model: 'elevenlabs-multilingual-v2',
options: { voice },
},
start: 0,
length: 'auto',
},
],
},
{
clips: [
{
asset: { type: 'video', src: videoUrl, volume: 0 },
start: 0,
length: 'alias://voiceover',
transition: { in: 'fade', out: 'fade' },
},
],
},
],
},
output: { format: 'mp4', resolution: 'hd' },
};
}
async function render(edit) {
const res = await fetch(`${API}/render`, { method: 'POST', headers, body: JSON.stringify(edit) });
const body = await res.json();
if (!res.ok) throw new Error(`Render request failed: ${JSON.stringify(body)}`);
const id = body.response.id;
while (true) {
await new Promise((resolve) => setTimeout(resolve, 5000));
const status = await (await fetch(`${API}/render/${id}`, { headers })).json();
const { status: state, url, error } = status.response;
console.log(`Render ${id}: ${state}`);
if (state === 'done') return url;
if (state === 'failed') throw new Error(`Render failed: ${error}`);
}
}
const url = await render(
buildEdit({
script:
'Moving down to the Central area we are seeing clear skies, a gentle breeze and mild temperatures, ' +
'perfect for an evening stroll. Temperatures are hovering around a comfortable 55 degrees Fahrenheit, ' +
'the ideal weather for outdoor activities.',
videoUrl: 'https://shotstack-assets.s3-ap-southeast-2.amazonaws.com/footage/table-mountain.mp4',
voice: 'George',
})
);
console.log(`Video: ${url}`);Save it as voiceover.mjs and run it with your API key in the environment:
SHOTSTACK_API_KEY=YOUR_API_KEY node voiceover.mjsThe script prints the render status every five seconds, then the URL of the finished video. To voice another video, call buildEdit with a different script, video URL or voice.
Turn any script into a narrated video
In this post, we added an AI voice over and word-by-word captions to a video with one API request. Because the script is part of the edit, changing the words, the voice or the language is a one-line change, and the video always fits the speech.
From this simple example, you can imagine a personalized weather report for every location, with its own script and footage. You could also add lower-third titles and a quiet music track on other tracks to make the video even more engaging.
Sign up for a free Shotstack account to get your API key and render your first AI voice over.
Frequently asked questions (FAQs)
How much does an AI voice over cost?
Speech is charged per character of the script, at a fixed rate for each model, in the same credits as rendering. To see the cost before you render, send the asset to the POST /generate/quote endpoint. It returns the price in credits without generating or charging anything. The AI generation pricing guide explains each model’s rate.
Can I caption a voice over I recorded myself?
Yes. Use an audio asset with the URL of your MP3 in src, instead of a prompt and model, and keep the same alias. The rich-caption clip transcribes recorded audio the same way, so your own narration gets synced captions too.
Which languages can the AI voice speak?
ElevenLabs Multilingual v2 speaks 29 languages, and ElevenLabs Turbo v2.5 speaks 32. MiniMax and Amazon Polly have their own language lists. Write the script in the language you want spoken, and set language in options to choose it explicitly.