> ## 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.

# Get Job Status

> Poll the status of a video generation job

Retrieve the current status of a video generation job, including per-project progress, completed videos, and final output URLs.

## Authentication

<ParamField header="Authorization" type="string" required>
  Your Nouvel API key. Format: `Bearer nvl_xxxx`
</ParamField>

## Path Parameters

<ParamField path="jobId" type="string" required>
  The job ID returned from `POST /api/v1/generate`.

  ```
  550e8400-e29b-41d4-a716-446655440000
  ```
</ParamField>

## Response

<ResponseField name="jobId" type="string" required>
  The job UUID.
</ResponseField>

<ResponseField name="status" type="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                  |
</ResponseField>

<ResponseField name="overallProgress" type="integer" required>
  Overall completion percentage (0-100). Capped at 99 while `status: "running"`. Reaches 100 only when terminal.
</ResponseField>

<ResponseField name="stage" type="string" required>
  Current pipeline stage for running jobs:

  * `preparing` - Scraping product page, generating scripts
  * `video` - Generating lip-synced avatar and product scenes
  * `audio` - Synthesizing voiceover
  * `lip_sync` - Applying lip sync to match voiceover
  * `stitching` - Combining scenes, adding captions, final export
</ResponseField>

<ResponseField name="totalProjects" type="integer" required>
  Total number of videos in this job.
</ResponseField>

<ResponseField name="completedProjects" type="integer" required>
  Number of videos successfully completed.
</ResponseField>

<ResponseField name="failedProjects" type="integer" required>
  Number of videos that failed during generation.
</ResponseField>

<ResponseField name="projects" type="array" required>
  Per-project status summary. Each entry contains:

  <Expandable title="Project Object Schema">
    <ResponseField name="id" type="string">
      Project UUID.
    </ResponseField>

    <ResponseField name="title" type="string">
      Auto-generated project title.
    </ResponseField>

    <ResponseField name="status" type="string">
      Project-level status: `preparing`, `generating`, `post_processing`, `stitching`, `completed`, `failed`, `cancelled`.
    </ResponseField>

    <ResponseField name="progress" type="integer">
      Individual project progress (0-100).
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="completedDetails" type="array">
  Detailed results for completed/failed projects. **Only populated when job status is terminal** (`completed`, `partial`, `failed`).

  <Expandable title="Completed Project Schema">
    <ResponseField name="projectId" type="string">
      Project UUID.
    </ResponseField>

    <ResponseField name="title" type="string">
      Project title.
    </ResponseField>

    <ResponseField name="url" type="string">
      Original product URL submitted.
    </ResponseField>

    <ResponseField name="finalOutputUrl" type="string | null">
      **URL to the final video file (MP4).** This is the primary output. `null` if generation failed.
    </ResponseField>

    <ResponseField name="description" type="string | null">
      AI-generated video description/concept.
    </ResponseField>

    <ResponseField name="status" type="string">
      Final project status: `completed` or `failed`.
    </ResponseField>

    <ResponseField name="aspectRatio" type="string | null">
      Video aspect ratio: `"9:16"` (vertical), `"1:1"` (square), `"16:9"` (horizontal). Currently always `"9:16"`.
    </ResponseField>

    <ResponseField name="durationSeconds" type="integer | null">
      Video duration in seconds. Typically 15s.
    </ResponseField>

    <ResponseField name="sceneCount" type="integer">
      Number of scenes in the video (typically 3-5).
    </ResponseField>

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

## Example Requests

<CodeGroup>
  ```bash cURL theme={null}
  curl https://app.nouvel.ai/api/v1/jobs/550e8400-e29b-41d4-a716-446655440000 \
    -H "Authorization: Bearer nvl_xxxx"
  ```

  ```typescript TypeScript - Simple Status Check theme={null}
  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}`);
    });
  }
  ```

  ```python Python - Polling Loop with Exponential Backoff theme={null}
  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)
  ```
</CodeGroup>

## Response Examples

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

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

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

  ```json 401 - Unauthorized theme={null}
  {
    "error": "Invalid API key"
  }
  ```

  ```json 404 - Job Not Found theme={null}
  {
    "error": "Job not found"
  }
  ```
</CodeGroup>

## Polling Best Practices

<AccordionGroup>
  <Accordion title="Poll at regular intervals">
    * Poll every **10-15 seconds** during generation
    * **Stop polling** when status is terminal: `completed`, `partial`, `failed`, or `cancelled`
    * Use exponential backoff to reduce server load: start at 10s, increase to 15s, then 20s max
  </Accordion>

  <Accordion title="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

    **Regular polling is critical.** Without it, jobs may stall.
  </Accordion>

  <Accordion title="Monitor stage transitions">
    Track the `stage` field to understand where the job is:

    1. `preparing` (30-60 sec) - Scraping, script generation, visual concept
    2. `audio` (10-20 sec) - TTS voiceover synthesis + force alignment
    3. `video` (60-90 sec) - Lip-synced avatar rendering
    4. `stitching` (30-60 sec) - Final render with product slides, transitions, captions

    **Total time: 2-5 minutes**
  </Accordion>

  <Accordion title="Handle partial failures gracefully">
    When `status: "partial"`:

    * Some videos succeeded, some failed
    * `completedDetails` includes both successes and failures
    * Filter by `status: "completed"` to get successful videos
    * Use successful videos; retry failed ones if needed
  </Accordion>

  <Accordion title="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
  </Accordion>
</AccordionGroup>

<Warning>
  **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.
</Warning>

## Understanding Progress

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

### 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

<Tip>
  Instead of polling, register a `webhookUrl` when calling [POST /api/v1/generate](/api-reference/generate) to receive a notification when the job completes. Webhooks retry up to 3 times with exponential backoff.

  See the [Webhooks guide](/api-reference/webhooks) for payload format, signature verification, and delivery behavior.
</Tip>

## Next Steps

Once your job reaches `status: "completed"`:

1. **Extract video URLs** from `completedDetails[].finalOutputUrl`
2. **Download videos** for review or local storage
3. **Publish to social platforms** using the [Publishing API](/api-reference/publish)
4. **Track performance** with built-in analytics

<CodeGroup>
  ```typescript Download Completed Videos theme={null}
  // 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}`);
      }
    }
  }
  ```

  ```python Publish Completed Videos theme={null}
  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']}")
  ```
</CodeGroup>
