Get Job Status
curl --request GET \
--url https://app.nouvel.ai/api/v1/jobs/{jobId} \
--header 'Authorization: <authorization>'import requests
url = "https://app.nouvel.ai/api/v1/jobs/{jobId}"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://app.nouvel.ai/api/v1/jobs/{jobId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.nouvel.ai/api/v1/jobs/{jobId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.nouvel.ai/api/v1/jobs/{jobId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.nouvel.ai/api/v1/jobs/{jobId}")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.nouvel.ai/api/v1/jobs/{jobId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"jobId": "<string>",
"status": "<string>",
"overallProgress": 123,
"stage": "<string>",
"totalProjects": 123,
"completedProjects": 123,
"failedProjects": 123,
"projects": [
{
"id": "<string>",
"title": "<string>",
"status": "<string>",
"progress": 123
}
],
"completedDetails": [
{
"projectId": "<string>",
"title": "<string>",
"url": "<string>",
"finalOutputUrl": {},
"description": {},
"status": "<string>",
"aspectRatio": {},
"durationSeconds": {},
"sceneCount": 123,
"script": {}
}
]
}Video Generation
Get Job Status
Poll the status of a video generation job
GET
/
api
/
v1
/
jobs
/
{jobId}
Get Job Status
curl --request GET \
--url https://app.nouvel.ai/api/v1/jobs/{jobId} \
--header 'Authorization: <authorization>'import requests
url = "https://app.nouvel.ai/api/v1/jobs/{jobId}"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://app.nouvel.ai/api/v1/jobs/{jobId}', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.nouvel.ai/api/v1/jobs/{jobId}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.nouvel.ai/api/v1/jobs/{jobId}"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://app.nouvel.ai/api/v1/jobs/{jobId}")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.nouvel.ai/api/v1/jobs/{jobId}")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"jobId": "<string>",
"status": "<string>",
"overallProgress": 123,
"stage": "<string>",
"totalProjects": 123,
"completedProjects": 123,
"failedProjects": 123,
"projects": [
{
"id": "<string>",
"title": "<string>",
"status": "<string>",
"progress": 123
}
],
"completedDetails": [
{
"projectId": "<string>",
"title": "<string>",
"url": "<string>",
"finalOutputUrl": {},
"description": {},
"status": "<string>",
"aspectRatio": {},
"durationSeconds": {},
"sceneCount": 123,
"script": {}
}
]
}Retrieve the current status of a video generation job, including per-project progress, completed videos, and final output URLs.
Authentication
string
required
Your Nouvel API key. Format:
Bearer nvl_xxxxPath Parameters
string
required
The job ID returned from
POST /api/v1/generate.550e8400-e29b-41d4-a716-446655440000
Response
string
required
The job UUID.
string
required
Overall job status. One of:
| Status | Description |
|---|---|
pending | Job created, not yet started |
running | Generation in progress |
completed | All videos generated successfully |
partial | Some videos completed, some failed |
failed | All videos failed |
cancelled | Job was cancelled |
integer
required
Overall completion percentage (0-100). Capped at 99 while
status: "running". Reaches 100 only when terminal.string
required
Current pipeline stage for running jobs:
preparing- Scraping product page, generating scriptsvideo- Generating lip-synced avatar and product scenesaudio- Synthesizing voiceoverlip_sync- Applying lip sync to match voiceoverstitching- Combining scenes, adding captions, final export
integer
required
Total number of videos in this job.
integer
required
Number of videos successfully completed.
integer
required
Number of videos that failed during generation.
array
required
array
Detailed results for completed/failed projects. Only populated when job status is terminal (
completed, partial, failed).Show Completed Project Schema
Show Completed Project Schema
string
Project UUID.
string
Project title.
string
Original product URL submitted.
string | null
URL to the final video file (MP4). This is the primary output.
null if generation failed.string | null
AI-generated video description/concept.
string
Final project status:
completed or failed.string | null
Video aspect ratio:
"9:16" (vertical), "1:1" (square), "16:9" (horizontal). Currently always "9:16".integer | null
Video duration in seconds. Typically 15s.
integer
Number of scenes in the video (typically 3-5).
string | null
Full voiceover script used in the video.
Example Requests
curl https://app.nouvel.ai/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer nvl_xxxx"
const jobId = '550e8400-e29b-41d4-a716-446655440000';
const response = await fetch(`https://app.nouvel.ai/api/v1/jobs/${jobId}`, {
headers: {
'Authorization': `Bearer ${process.env.NOUVEL_API_KEY}`,
},
});
const job = await response.json();
console.log(`Job ${job.status}: ${job.overallProgress}% complete`);
console.log(`Stage: ${job.stage}`);
console.log(`Completed: ${job.completedProjects}/${job.totalProjects}`);
if (job.status === 'completed') {
job.completedDetails.forEach(video => {
console.log(`✓ ${video.title}: ${video.finalOutputUrl}`);
});
}
import requests
import time
api_key = 'nvl_xxxx'
job_id = '550e8400-e29b-41d4-a716-446655440000'
url = f'https://app.nouvel.ai/api/v1/jobs/{job_id}'
headers = {'Authorization': f'Bearer {api_key}'}
poll_interval = 10 # Start at 10 seconds
while True:
response = requests.get(url, headers=headers)
job = response.json()
print(f"[{job['status']}] {job['overallProgress']}% - Stage: {job['stage']}")
# Terminal statuses
if job['status'] in ['completed', 'partial', 'failed', 'cancelled']:
print(f"\nJob finished: {job['status']}")
if job.get('completedDetails'):
for video in job['completedDetails']:
if video['status'] == 'completed':
print(f"✓ {video['title']}")
print(f" URL: {video['finalOutputUrl']}")
print(f" Duration: {video['durationSeconds']}s")
else:
print(f"✗ {video['title']} - Failed")
break
# Exponential backoff: 10s → 15s → 20s (max)
time.sleep(poll_interval)
poll_interval = min(poll_interval + 5, 20)
Response Examples
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "running",
"overallProgress": 45,
"stage": "video",
"totalProjects": 2,
"completedProjects": 0,
"failedProjects": 0,
"projects": [
{
"id": "proj_abc123",
"title": "Protein Powder - Energetic Angle",
"status": "generating",
"progress": 60
},
{
"id": "proj_def456",
"title": "Protein Powder - Science-Based Angle",
"status": "preparing",
"progress": 30
}
],
"completedDetails": []
}
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "completed",
"overallProgress": 100,
"stage": "stitching",
"totalProjects": 2,
"completedProjects": 2,
"failedProjects": 0,
"projects": [
{
"id": "proj_abc123",
"title": "Protein Powder - Energetic Angle",
"status": "completed",
"progress": 100
},
{
"id": "proj_def456",
"title": "Protein Powder - Science-Based Angle",
"status": "completed",
"progress": 100
}
],
"completedDetails": [
{
"projectId": "proj_abc123",
"title": "Protein Powder - Energetic Angle",
"url": "https://example.com/products/protein-powder",
"finalOutputUrl": "https://storage.app.nouvel.ai/videos/final-abc123.mp4",
"description": "High-energy UGC ad showcasing post-workout benefits",
"status": "completed",
"aspectRatio": "9:16",
"durationSeconds": 15,
"sceneCount": 4,
"script": "I've tried every protein powder out there, and this one actually tastes amazing..."
},
{
"projectId": "proj_def456",
"title": "Protein Powder - Science-Based Angle",
"url": "https://example.com/products/protein-powder",
"finalOutputUrl": "https://storage.app.nouvel.ai/videos/final-def456.mp4",
"description": "Authority-driven ad emphasizing clinical research and results",
"status": "completed",
"aspectRatio": "9:16",
"durationSeconds": 15,
"sceneCount": 3,
"script": "Here's what makes this different from every other protein supplement..."
}
]
}
{
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"status": "partial",
"overallProgress": 100,
"stage": "stitching",
"totalProjects": 3,
"completedProjects": 2,
"failedProjects": 1,
"projects": [
{
"id": "proj_abc123",
"title": "Product A - Variant 1",
"status": "completed",
"progress": 100
},
{
"id": "proj_def456",
"title": "Product A - Variant 2",
"status": "completed",
"progress": 100
},
{
"id": "proj_ghi789",
"title": "Product B - Variant 1",
"status": "failed",
"progress": 0
}
],
"completedDetails": [
{
"projectId": "proj_abc123",
"title": "Product A - Variant 1",
"url": "https://example.com/products/product-a",
"finalOutputUrl": "https://storage.app.nouvel.ai/videos/final-abc123.mp4",
"status": "completed",
"aspectRatio": "9:16",
"durationSeconds": 15,
"sceneCount": 4,
"script": "..."
},
{
"projectId": "proj_def456",
"title": "Product A - Variant 2",
"url": "https://example.com/products/product-a",
"finalOutputUrl": "https://storage.app.nouvel.ai/videos/final-def456.mp4",
"status": "completed",
"aspectRatio": "9:16",
"durationSeconds": 15,
"sceneCount": 3,
"script": "..."
},
{
"projectId": "proj_ghi789",
"title": "Product B - Variant 1",
"url": "https://example.com/products/product-b",
"finalOutputUrl": null,
"status": "failed",
"aspectRatio": null,
"durationSeconds": null,
"sceneCount": 0,
"script": null
}
]
}
{
"error": "Invalid API key"
}
{
"error": "Job not found"
}
Polling Best Practices
Poll at regular intervals
Poll at regular intervals
- Poll every 10-15 seconds during generation
- Stop polling when status is terminal:
completed,partial,failed, orcancelled - Use exponential backoff to reduce server load: start at 10s, increase to 15s, then 20s max
The endpoint drives the pipeline forward
The endpoint drives the pipeline forward
This endpoint doesn’t just return status—it actively triggers pipeline progression:
- Checks for scene generation updates
- Initiates post-processing when scenes complete
- Triggers stitching when all scenes are ready
Monitor stage transitions
Monitor stage transitions
Track the
stage field to understand where the job is:preparing(30-60 sec) - Scraping, script generation, visual conceptaudio(10-20 sec) - TTS voiceover synthesis + force alignmentvideo(60-90 sec) - Lip-synced avatar renderingstitching(30-60 sec) - Final render with product slides, transitions, captions
Handle partial failures gracefully
Handle partial failures gracefully
When
status: "partial":- Some videos succeeded, some failed
completedDetailsincludes both successes and failures- Filter by
status: "completed"to get successful videos - Use successful videos; retry failed ones if needed
Implement timeout protection
Implement timeout protection
- Jobs have a 15-minute staleness timeout (no progress)
- Absolute timeout: 30 minutes from creation
- If a job exceeds these limits, it fails automatically
- Build your own client-side timeout (e.g., 25 minutes) to detect stalled jobs
Don’t poll too aggressively. Polling faster than every 5 seconds provides no benefit and wastes API quota. The pipeline takes 2-5 minutes regardless of poll frequency.
Understanding Progress
Progress calculation:
overallProgress= weighted average of all projects- Each project contributes equally to the overall progress
- Progress is capped at 99% while
status: "running" - Reaches 100% only when terminal
Per-Project Progress Breakdown
| Stage | Progress Range | What’s Happening |
|---|---|---|
preparing | 0-20% | Scraping product page, analyzing content, generating script concept |
generating | 20-70% | Lip-synced avatar rendering + product scene composition |
post_processing | 70-85% | TTS voiceover synthesis, audio mixing, lip sync application |
stitching | 85-99% | Combining scenes, generating captions, final video export |
completed | 100% | Video ready, finalOutputUrl available |
Error Codes
| Code | Description |
|---|---|
| 401 | Invalid or missing API key |
| 404 | Job ID not found |
| 500 | Internal server error |
Webhook Alternative
Instead of polling, register a
webhookUrl when calling POST /api/v1/generate to receive a notification when the job completes. Webhooks retry up to 3 times with exponential backoff.See the Webhooks guide for payload format, signature verification, and delivery behavior.Next Steps
Once your job reachesstatus: "completed":
- Extract video URLs from
completedDetails[].finalOutputUrl - Download videos for review or local storage
- Publish to social platforms using the Publishing API
- Track performance with built-in analytics
// After job completes
const job = await fetch(`https://app.nouvel.ai/api/v1/jobs/${jobId}`, {
headers: { 'Authorization': `Bearer ${apiKey}` },
}).then(r => r.json());
if (job.status === 'completed') {
for (const video of job.completedDetails) {
if (video.status === 'completed' && video.finalOutputUrl) {
// Download video
const videoResponse = await fetch(video.finalOutputUrl);
const videoBlob = await videoResponse.blob();
// Save locally or upload to your CDN
console.log(`Downloaded: ${video.title}`);
}
}
}
import requests
# Get job status
response = requests.get(
f'https://app.nouvel.ai/api/v1/jobs/{job_id}',
headers={'Authorization': f'Bearer {api_key}'}
)
job = response.json()
if job['status'] == 'completed':
for video in job['completedDetails']:
if video['status'] == 'completed':
# Use Publishing API to schedule social posts
publish_response = requests.post(
'https://app.nouvel.ai/api/v1/publish',
headers={'Authorization': f'Bearer {api_key}'},
json={
'projectId': video['projectId'],
'platforms': [
{'platform': 'instagram', 'accountId': 'your-ig-account-id'},
{'platform': 'tiktok', 'accountId': 'your-tt-account-id'},
],
'caption': video['script'],
'scheduledAt': '2026-06-10T10:00:00Z'
}
)
print(f"Scheduled: {video['title']}")

