> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nouvel.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Generate your first video ad in under 5 minutes

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:

<CardGroup cols={2}>
  <Card title="Active Subscription" icon="credit-card">
    You need a **Scale** (\$249/mo) or **Business** (\$849/mo) plan to access the API.

    [Upgrade your plan →](https://app.nouvel.ai/settings)
  </Card>

  <Card title="API Key" icon="key">
    Create an API key with at least `generate` and `projects:read` permissions.

    [Create API key →](https://app.nouvel.ai/settings)
  </Card>
</CardGroup>

<Info>
  Don't have an account yet? [Sign up for free](https://app.nouvel.ai/register) and upgrade to a qualifying plan.
</Info>

## Step 1: Create an API Key

<Steps>
  <Step title="Navigate to Settings">
    Go to [Settings → API Keys](https://app.nouvel.ai/settings) in your Nouvel dashboard.
  </Step>

  <Step title="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)
  </Step>

  <Step title="Copy and store">
    Copy the API key (starts with `nvl_`) and store it securely. **You'll only see this once!**
  </Step>
</Steps>

<Tip>
  Store your API key in an environment variable for security:

  ```bash .env theme={null}
  NOUVEL_API_KEY=nvl_xxxxxxxxxxxxxxxxxxxx
  ```
</Tip>

## Step 2: Generate a Video

Send a POST request to `/api/v1/generate` with a product URL:

<CodeGroup>
  ```bash cURL theme={null}
  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
    }'
  ```

  ```typescript TypeScript theme={null}
  const NOUVEL_API_KEY = process.env.NOUVEL_API_KEY;

  async function generateVideo() {
    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: ['https://example.com/products/protein-powder'],
        variantCount: 1,
      }),
    });

    if (!response.ok) {
      throw new Error(`API error: ${response.status} ${response.statusText}`);
    }

    const data = await response.json();
    console.log('Job created:', data);
    return data.jobId;
  }

  const jobId = await generateVideo();
  console.log(`Job ID: ${jobId}`);
  ```

  ```python Python theme={null}
  import os
  import requests

  NOUVEL_API_KEY = os.environ.get('NOUVEL_API_KEY')

  def generate_video():
      response = requests.post(
          'https://app.nouvel.ai/api/v1/generate',
          headers={
              'Authorization': f'Bearer {NOUVEL_API_KEY}',
              'Content-Type': 'application/json',
          },
          json={
              'urls': ['https://example.com/products/protein-powder'],
              'variantCount': 1,
          }
      )

      response.raise_for_status()
      data = response.json()
      print(f"Job created: {data}")
      return data['jobId']

  job_id = generate_video()
  print(f"Job ID: {job_id}")
  ```
</CodeGroup>

### Response

```json theme={null}
{
  "jobId": "clx8k9v2t0001qz8x7r4e5w6m",
  "totalProjects": 1,
  "status": "running"
}
```

<ParamField body="jobId" type="string" required>
  Unique identifier for this generation job. Use this to poll status.
</ParamField>

<ParamField body="totalProjects" type="number">
  Total number of video variants being generated (urls × variantCount)
</ParamField>

<ParamField body="status" type="string">
  Initial job status, always `"running"` for new jobs
</ParamField>

## Step 3: Poll for Completion

Video generation takes **2-5 minutes**. Poll the status endpoint to track progress:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.nouvel.ai/api/v1/jobs/clx8k9v2t0001qz8x7r4e5w6m \
    -H "Authorization: Bearer nvl_xxxxxxxxxxxxxxxxxxxx"
  ```

  ```typescript TypeScript theme={null}
  async function pollJobStatus(jobId: string) {
    const response = await fetch(
      `https://app.nouvel.ai/api/v1/jobs/${jobId}`,
      {
        headers: {
          'Authorization': `Bearer ${NOUVEL_API_KEY}`,
        },
      }
    );

    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }

    return response.json();
  }

  // Poll every 10 seconds until complete
  async function waitForCompletion(jobId: string) {
    while (true) {
      const status = await pollJobStatus(jobId);
      console.log(`Progress: ${status.overallProgress}% - ${status.stage}`);

      if (['completed', 'partial', 'failed'].includes(status.status)) {
        return status;
      }

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

  const result = await waitForCompletion(jobId);
  console.log('Generation complete!', result);
  ```

  ```python Python theme={null}
  import time

  def poll_job_status(job_id):
      response = requests.get(
          f'https://app.nouvel.ai/api/v1/jobs/{job_id}',
          headers={'Authorization': f'Bearer {NOUVEL_API_KEY}'}
      )
      response.raise_for_status()
      return response.json()

  def wait_for_completion(job_id):
      """Poll every 10 seconds until complete"""
      while True:
          status = poll_job_status(job_id)
          print(f"Progress: {status['overallProgress']}% - {status['stage']}")

          if status['status'] in ['completed', 'partial', 'failed']:
              return status

          # Wait 30 seconds before next poll
          time.sleep(30)

  result = wait_for_completion(job_id)
  print('Generation complete!', result)
  ```
</CodeGroup>

### Status Response

```json theme={null}
{
  "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": []
}
```

<ResponseField name="status" type="string">
  Job status: `running`, `completed`, `partial`, `failed`, or `cancelled`
</ResponseField>

<ResponseField name="overallProgress" type="number">
  Overall progress percentage (0-100)
</ResponseField>

<ResponseField name="stage" type="string">
  Current generation stage: `preparing`, `video`, `audio`, `lip_sync`, or `stitching`
</ResponseField>

<ResponseField name="projects" type="array">
  Array of individual video projects being generated
</ResponseField>

<ResponseField name="completedDetails" type="array">
  Array of completed projects with full metadata (only populated when `status` is terminal)
</ResponseField>

<Info>
  **Recommended polling interval**: 10 seconds. Polling more frequently doesn't speed up generation and may hit rate limits.
</Info>

## Step 4: Get the Result

When `status` becomes `completed`, the `completedDetails` array contains your video(s):

```json theme={null}
{
  "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."
    }
  ]
}
```

<ResponseField name="finalOutputUrl" type="string">
  Direct download URL for the completed video (MP4 format, publicly accessible)
</ResponseField>

<ResponseField name="script" type="string">
  Full voiceover script used in the video
</ResponseField>

<ResponseField name="aspectRatio" type="string">
  Video aspect ratio: `9:16` (vertical), `1:1` (square), or `16:9` (landscape)
</ResponseField>

<ResponseField name="durationSeconds" type="number">
  Video duration in seconds (typically 15)
</ResponseField>

## Full Working Example

Here's a complete TypeScript example that generates a video and waits for completion:

```typescript nouvel-generate.ts theme={null}
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:

```bash theme={null}
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:

| Stage       | Duration | Description                                    |
| ----------- | -------- | ---------------------------------------------- |
| `preparing` | \~1 min  | Scraping product data, generating ad concept   |
| `video`     | \~10 min | AI video generation with actors (longest step) |
| `audio`     | \~30 sec | Text-to-speech voiceover generation            |
| `lip_sync`  | \~4 min  | Syncing actor's lips to voiceover              |
| `stitching` | \~1 min  | Adding captions and final rendering            |

<Tip>
  **Queue delays**: During peak hours, jobs may queue for an additional 5-10 minutes before `video` stage begins.
</Tip>

## Request Parameters

The `POST /api/v1/generate` endpoint accepts these parameters:

<ParamField body="urls" type="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"]`
</ParamField>

<ParamField body="variantCount" type="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)
</ParamField>

<ParamField body="userInstructions" type="string" optional>
  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."`
</ParamField>

## Error Handling

Always check the response status and handle errors appropriately:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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();
  }
  ```

  ```python Python theme={null}
  def generate_video(product_url):
      response = requests.post(
          'https://app.nouvel.ai/api/v1/generate',
          headers={
              'Authorization': f'Bearer {NOUVEL_API_KEY}',
              'Content-Type': 'application/json',
          },
          json={
              'urls': [product_url],
              'variantCount': 1,
          }
      )

      if not response.ok:
          error = response.json()

          if response.status_code == 401:
              raise Exception('Invalid API key')

          if response.status_code == 402:
              raise Exception('Quota exceeded. Upgrade your plan or add credits.')

          if response.status_code == 422 and error.get('error') == 'not_a_product':
              raise Exception(f"Not a product page: {error.get('reason')}")

          raise Exception(f"API error: {error}")

      return response.json()
  ```
</CodeGroup>

## Common Issues

<AccordionGroup>
  <Accordion title="401 Unauthorized">
    **Cause**: Invalid or expired API key

    **Solution**:

    * 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_...`
  </Accordion>

  <Accordion title="402 Payment Required - Quota Exceeded">
    **Cause**: You've used all videos in your monthly quota

    **Solution**:

    * [Add credits](https://app.nouvel.ai/settings) for overage usage (\$8/video)
    * [Upgrade your plan](https://app.nouvel.ai/settings) for higher quota
    * Wait until your quota resets at the start of your billing cycle
  </Accordion>

  <Accordion title="422 Unprocessable Entity - Not a Product">
    **Cause**: The URL is not a valid product page

    **Solution**:

    * 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
  </Accordion>

  <Accordion title="429 Too Many Requests">
    **Cause**: Exceeded rate limit of 60 requests/minute

    **Solution**:

    * Implement exponential backoff in your polling logic
    * Don't poll more frequently than every 10 seconds
    * Spread out your generation requests over time
  </Accordion>

  <Accordion title="Job stuck at 99% progress">
    **Cause**: Final stitching stage can take 1-2 minutes

    **Solution**:

    * Continue polling - this is normal
    * If stuck for >5 minutes, [contact support](mailto:support@nouvel.ai)
  </Accordion>

  <Accordion title="Job failed during generation">
    **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
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={3}>
  <Card title="Generate Endpoint" icon="wand-magic-sparkles" href="/api-reference/generate">
    Full reference for video generation
  </Card>

  <Card title="Job Status Endpoint" icon="clock" href="/api-reference/jobs">
    Complete job polling documentation
  </Card>

  <Card title="List Projects" icon="list" href="/api-reference/list-projects">
    Browse and filter your projects
  </Card>
</CardGroup>

## Need Help?

<CardGroup cols={1}>
  <Card title="API Support" icon="life-ring" href="mailto:support@nouvel.ai">
    Email our team for technical assistance
  </Card>
</CardGroup>
