Skip to main content
This guide will walk you through creating your first AI-generated video ad using the Nouvel API.

Prerequisites

Before you begin, make sure you have:

Active Subscription

You need a Scale ($249/mo) or Business ($849/mo) plan to access the API.Upgrade your plan →

API Key

Create an API key with at least generate and projects:read permissions.Create API key →
Don’t have an account yet? Sign up for free and upgrade to a qualifying plan.

Step 1: Create an API Key

1

Navigate to Settings

Go to Settings → API Keys in your Nouvel dashboard.
2

Create a new key

Click “Create API Key” and configure:
  • Name: My First API Key (or any descriptive name)
  • Permissions: Select generate and projects:read
  • Expiration: 30 days (recommended for testing)
3

Copy and store

Copy the API key (starts with nvl_) and store it securely. You’ll only see this once!
Store your API key in an environment variable for security:
.env
NOUVEL_API_KEY=nvl_xxxxxxxxxxxxxxxxxxxx

Step 2: Generate a Video

Send a POST request to /api/v1/generate with a product URL:
curl -X POST https://app.nouvel.ai/api/v1/generate \
  -H "Authorization: Bearer nvl_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": ["https://example.com/products/protein-powder"],
    "variantCount": 1
  }'

Response

{
  "jobId": "clx8k9v2t0001qz8x7r4e5w6m",
  "totalProjects": 1,
  "status": "running"
}
jobId
string
required
Unique identifier for this generation job. Use this to poll status.
totalProjects
number
Total number of video variants being generated (urls × variantCount)
status
string
Initial job status, always "running" for new jobs

Step 3: Poll for Completion

Video generation takes 2-5 minutes. Poll the status endpoint to track progress:
curl https://app.nouvel.ai/api/v1/jobs/clx8k9v2t0001qz8x7r4e5w6m \
  -H "Authorization: Bearer nvl_xxxxxxxxxxxxxxxxxxxx"

Status Response

{
  "jobId": "clx8k9v2t0001qz8x7r4e5w6m",
  "status": "running",
  "overallProgress": 65,
  "stage": "video",
  "totalProjects": 1,
  "completedProjects": 0,
  "failedProjects": 0,
  "projects": [
    {
      "id": "clx8k9v2t0002qz8x7r4e5w6n",
      "title": "Premium Whey Protein - Build Muscle Fast",
      "status": "generating",
      "progress": 65
    }
  ],
  "completedDetails": []
}
status
string
Job status: running, completed, partial, failed, or cancelled
overallProgress
number
Overall progress percentage (0-100)
stage
string
Current generation stage: preparing, video, audio, lip_sync, or stitching
projects
array
Array of individual video projects being generated
completedDetails
array
Array of completed projects with full metadata (only populated when status is terminal)
Recommended polling interval: 10 seconds. Polling more frequently doesn’t speed up generation and may hit rate limits.

Step 4: Get the Result

When status becomes completed, the completedDetails array contains your video(s):
{
  "jobId": "clx8k9v2t0001qz8x7r4e5w6m",
  "status": "completed",
  "overallProgress": 100,
  "completedProjects": 1,
  "completedDetails": [
    {
      "projectId": "clx8k9v2t0002qz8x7r4e5w6n",
      "title": "Premium Whey Protein - Build Muscle Fast",
      "url": "https://example.com/products/protein-powder",
      "finalOutputUrl": "https://app.nouvel.ai/api/cdn/videos/final-20261115-abc123.mp4",
      "description": "A 15-second UGC ad showcasing the benefits of premium whey protein...",
      "status": "completed",
      "aspectRatio": "9:16",
      "durationSeconds": 15,
      "sceneCount": 3,
      "script": "Struggling to build muscle? This premium whey protein changed everything for me. Just one scoop after my workout and I saw results in weeks. The best part? It actually tastes good. Try it yourself and see the difference."
    }
  ]
}
finalOutputUrl
string
Direct download URL for the completed video (MP4 format, publicly accessible)
script
string
Full voiceover script used in the video
aspectRatio
string
Video aspect ratio: 9:16 (vertical), 1:1 (square), or 16:9 (landscape)
durationSeconds
number
Video duration in seconds (typically 15)

Full Working Example

Here’s a complete TypeScript example that generates a video and waits for completion:
nouvel-generate.ts
const NOUVEL_API_KEY = process.env.NOUVEL_API_KEY;
const BASE_URL = 'https://app.nouvel.ai';

interface GenerateResponse {
  jobId: string;
  totalProjects: number;
  status: string;
}

interface JobStatus {
  jobId: string;
  status: string;
  overallProgress: number;
  stage: string;
  completedDetails: Array<{
    projectId: string;
    title: string;
    finalOutputUrl: string | null;
    script: string | null;
  }>;
}

async function generateVideo(productUrl: string): Promise<string> {
  const response = await fetch(`${BASE_URL}/api/v1/generate`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOUVEL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      urls: [productUrl],
      variantCount: 1,
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Generation failed: ${JSON.stringify(error)}`);
  }

  const data: GenerateResponse = await response.json();
  return data.jobId;
}

async function pollJobStatus(jobId: string): Promise<JobStatus> {
  const response = await fetch(`${BASE_URL}/api/v1/jobs/${jobId}`, {
    headers: {
      'Authorization': `Bearer ${NOUVEL_API_KEY}`,
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to get job status: ${response.status}`);
  }

  return response.json();
}

async function waitForCompletion(jobId: string): Promise<JobStatus> {
  const terminalStates = ['completed', 'partial', 'failed', 'cancelled'];

  while (true) {
    const status = await pollJobStatus(jobId);

    console.log(
      `[${new Date().toISOString()}] ` +
      `Progress: ${status.overallProgress}% - Stage: ${status.stage}`
    );

    if (terminalStates.includes(status.status)) {
      return status;
    }

    // Wait 30 seconds before next poll
    await new Promise(resolve => setTimeout(resolve, 30000));
  }
}

async function main() {
  const productUrl = 'https://example.com/products/protein-powder';

  console.log(`Starting video generation for: ${productUrl}`);

  // Step 1: Start generation
  const jobId = await generateVideo(productUrl);
  console.log(`Job created: ${jobId}`);

  // Step 2: Wait for completion
  const result = await waitForCompletion(jobId);

  // Step 3: Get the video URL
  if (result.status === 'completed' && result.completedDetails.length > 0) {
    const video = result.completedDetails[0];
    console.log('\n✅ Video generation complete!');
    console.log(`Title: ${video.title}`);
    console.log(`Video URL: ${video.finalOutputUrl}`);
    console.log(`Script: ${video.script}`);
  } else {
    console.error('\n❌ Generation failed or partially completed');
    console.error(JSON.stringify(result, null, 2));
  }
}

main().catch(console.error);
Run it:
NOUVEL_API_KEY=nvl_xxxxxxxxxxxxxxxxxxxx npx tsx nouvel-generate.ts

Understanding Generation Time

Video generation typically takes 15-20 minutes and goes through these stages:
StageDurationDescription
preparing~1 minScraping product data, generating ad concept
video~10 minAI video generation with actors (longest step)
audio~30 secText-to-speech voiceover generation
lip_sync~4 minSyncing actor’s lips to voiceover
stitching~1 minAdding captions and final rendering
Queue delays: During peak hours, jobs may queue for an additional 5-10 minutes before video stage begins.

Request Parameters

The POST /api/v1/generate endpoint accepts these parameters:
urls
string[]
required
Array of 1-3 product URLs to generate videos for. Each URL must be a valid product page (not a category, homepage, or blog).Example: ["https://shop.com/products/shoes", "https://shop.com/products/jacket"]
variantCount
number
default:1
Number of video variants to generate per URL (1-3). Each variant has a different ad angle and actor.Total videos = urls.length × variantCount (max 9)
userInstructions
string
Optional custom instructions for the AI (max 2000 characters). Use this to specify:
  • Target audience (e.g., “Target women aged 25-35”)
  • Ad angle (e.g., “Focus on sustainability and eco-friendliness”)
  • Tone (e.g., “Professional and trustworthy, not salesy”)
  • Specific features to highlight
Example: "Target fitness enthusiasts. Emphasize muscle recovery benefits. Professional tone."

Error Handling

Always check the response status and handle errors appropriately:
async function generateVideo(productUrl: string) {
  const response = await fetch('https://app.nouvel.ai/api/v1/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${NOUVEL_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      urls: [productUrl],
      variantCount: 1,
    }),
  });

  // Handle specific error codes
  if (!response.ok) {
    const error = await response.json();

    if (response.status === 401) {
      throw new Error('Invalid API key');
    }

    if (response.status === 402) {
      throw new Error('Quota exceeded. Upgrade your plan or add credits.');
    }

    if (response.status === 422 && error.error === 'not_a_product') {
      throw new Error(`Not a product page: ${error.reason}`);
    }

    throw new Error(`API error: ${JSON.stringify(error)}`);
  }

  return response.json();
}

Common Issues

Cause: Invalid or expired API keySolution:
  • Verify your API key is correct (starts with nvl_)
  • Check if the key has expired in your dashboard
  • Ensure you’re using Bearer token format: Authorization: Bearer nvl_...
Cause: You’ve used all videos in your monthly quotaSolution:
  • Add credits for overage usage ($8/video)
  • Upgrade your plan for higher quota
  • Wait until your quota resets at the start of your billing cycle
Cause: The URL is not a valid product pageSolution:
  • Ensure the URL points to a specific product, not a category or homepage
  • The URL must contain product information (title, description, images)
  • Try a different product URL from the same site
Cause: Exceeded rate limit of 60 requests/minuteSolution:
  • Implement exponential backoff in your polling logic
  • Don’t poll more frequently than every 10 seconds
  • Spread out your generation requests over time
Cause: Final stitching stage can take 1-2 minutesSolution:
  • Continue polling - this is normal
  • If stuck for >5 minutes, contact support
Cause: Various reasons (actor availability, technical issues, etc.)Solution:
  • Check the failedProjects count in the status response
  • If status: "partial", some videos succeeded - check completedDetails
  • Failed generations don’t count against your quota
  • Retry the same URL - it often succeeds on second attempt

Next Steps

Generate Endpoint

Full reference for video generation

Job Status Endpoint

Complete job polling documentation

List Projects

Browse and filter your projects

Need Help?

API Support

Email our team for technical assistance