---
title: "VOD & Recordings"
description: "Video-on-demand API — recordings, clips, and archive management"
canonical: "https://docs.ainative.studio/docs/live-streaming/vod"
last-updated: "2026-10-22T17:48:46.689Z"
---

# VOD & Recordings

Source: https://docs.ainative.studio/docs/live-streaming/vod

> Video-on-demand API — recordings, clips, and archive management

# VOD & Recordings

Streams are automatically recorded as VODs (Video on Demand) when they end. You can also upload standalone VOD files. This page covers VOD access, chapter markers, stream scheduling, and thumbnail and transcoding APIs.

---

## VOD upload

<span className="badge badge--post">POST</span> `/api/v1/vod/upload`

Upload a VOD recording and automatically create a linked community post. The video must first be uploaded to Cloudflare Stream — this endpoint links the Cloudflare video to the platform and creates the post.

**Auth required:** Yes  
The authenticated user must belong to a tenant (organization).

### Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `cloudflare_video_id` | string | Yes | Cloudflare Stream video ID |
| `title` | string | Yes | VOD title |
| `description` | string | No | VOD description |
| `visibility` | string | No | `public`, `followers`, or `private` |
| `group_id` | UUID | No | Post to a group |
| `content_warning` | string | No | Content warning text |
| `is_nsfw` | boolean | No | NSFW flag |

### Response

```json
{
  "vod": {
    "id": "vod-uuid",
    "status": "processing",
    "title": "My coding session",
    "cloudflare_video_id": "abc123",
    "playback_url": "https://...",
    "created_at": "2026-04-25T11:00:00Z"
  },
  "post": {
    "id": "post-uuid",
    "type": "VIDEO",
    "content": "My coding session",
    "visibility": "public",
    "created_at": "2026-04-25T11:00:00Z"
  }
}
```

---

## Get VOD details

<span className="badge badge--get">GET</span> `/api/v1/vod/{vod_id}`

Get full details for a VOD including the linked community post.

**Auth required:** Yes

Returns `404` if the VOD is not found within the user's tenant.

---

## Update VOD status

<span className="badge badge--patch">PATCH</span> `/api/v1/vod/{vod_id}/status`

Update the processing status of a VOD. Used by the processing pipeline to mark a VOD as ready or failed.

**Auth required:** Yes

| Query parameter | Type | Description |
|-----------------|------|-------------|
| `status` | string | New status value |

Valid status transitions:

| From | To |
|------|----|
| `processing` | `ready` |
| `processing` | `failed` |
| `ready` | `deleted` |

```bash
PATCH /api/v1/vod/vod-uuid/status?status=ready
```

---

## Chapter markers

Chapters divide a video into named segments for navigation. Chapters can be created manually or generated automatically using AI analysis of the video transcript.

### Generate AI chapters

<span className="badge badge--post">POST</span> `/api/v1/videos/{video_id}/chapters/generate`

Automatically generate chapter markers from a video's transcript. The AI analyzes content structure and timestamps to produce named segments.

**Auth required:** Yes (video owner only)

The video must have a `transcript` stored in its metadata. If no transcript exists, the request returns `400`.

Existing AI-generated chapters are replaced on each call. Manually created chapters are preserved.

### Request body

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `model` | string | `gpt-4o` | AI model to use |
| `temperature` | float | `0.3` | Model temperature |
| `max_tokens` | integer | `2000` | Max response tokens |

### Response

```json
{
  "chapters": [
    {
      "id": "chapter-uuid",
      "video_id": "video-uuid",
      "title": "Introduction",
      "description": "Overview of the session goals",
      "start_time": 0,
      "end_time": 120,
      "order": 0,
      "source": "ai",
      "confidence_score": 0.92,
      "thumbnail_url": null
    }
  ],
  "count": 8,
  "model_used": "gpt-4o",
  "processing_time_seconds": 4.2
}
```

---

### List chapters

<span className="badge badge--get">GET</span> `/api/v1/videos/{video_id}/chapters`

List all chapters for a video. Chapters are returned in order.

**Auth required:** No

| Parameter | Type | Description |
|-----------|------|-------------|
| `source` | string | Filter by `manual` or `ai` |
| `search` | string | Keyword search in title and description |
| `min_confidence` | float | Minimum AI confidence score (0.0–1.0) |

---

### Get chapter

<span className="badge badge--get">GET</span> `/api/v1/videos/{video_id}/chapters/{chapter_id}`

Get a specific chapter by ID.

---

### Create manual chapter

<span className="badge badge--post">POST</span> `/api/v1/videos/{video_id}/chapters`

Create a chapter manually. Validates that the time range is within the video duration and does not overlap with existing chapters.

**Auth required:** Yes (video owner only)

```json
{
  "title": "Database setup",
  "description": "Configuring PostgreSQL and running migrations",
  "start_time": 540,
  "end_time": 1080,
  "order": 3,
  "thumbnail_url": "https://..."
}
```

`start_time` and `end_time` are in seconds. Overlapping chapters return `400`.

---

### Update chapter

<span className="badge badge--put">PUT</span> `/api/v1/videos/{video_id}/chapters/{chapter_id}`

Update an existing chapter. Time range updates are validated for overlaps.

**Auth required:** Yes (video owner only)

Updatable fields: `title`, `description`, `start_time`, `end_time`, `order`, `thumbnail_url`.

---

### Delete chapter

<span className="badge badge--delete">DELETE</span> `/api/v1/videos/{video_id}/chapters/{chapter_id}`

Delete a chapter. Returns `204 No Content`.

**Auth required:** Yes (video owner only)

---

### Chapter navigation

#### Find chapter at timestamp

<span className="badge badge--get">GET</span> `/api/v1/videos/{video_id}/chapters/at-time`

Find the chapter that contains a specific timestamp.

| Parameter | Type | Description |
|-----------|------|-------------|
| `timestamp` | integer | Timestamp in seconds |

Returns `404` if no chapter covers the given timestamp.

#### Get next chapter

<span className="badge badge--get">GET</span> `/api/v1/videos/{video_id}/chapters/{chapter_id}/next`

Get the next chapter in sequence. Returns `404` if there is no next chapter.

#### Get previous chapter

<span className="badge badge--get">GET</span> `/api/v1/videos/{video_id}/chapters/{chapter_id}/previous`

Get the previous chapter in sequence. Returns `404` if at the beginning.

---

## Thumbnail generation

Thumbnails are generated asynchronously via Celery. The endpoint returns immediately with a task ID.

### Generate thumbnails

<span className="badge badge--post">POST</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/thumbnails`

Generate thumbnails for a video.

**Auth required:** Yes  
The video must have status `ready`.

### Request body

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `timestamps` | float[] | — | Custom timestamps in seconds. If omitted, auto-generates at 20%, 40%, 60%, 80%, 100% of duration |
| `resolutions` | string[] | `["small","medium","large"]` | Resolutions to generate |
| `set_default` | integer | `2` | Index of thumbnail to set as default (0-based) |

**Resolution sizes:**

| Name | Dimensions | Use case |
|------|-----------|----------|
| `small` | 320×180 | Video cards |
| `medium` | 640×360 | List views |
| `large` | 1280×720 | Detail pages |
| `xlarge` | 1920×1080 | Fullscreen |

### Response

```json
{
  "video_id": "video-uuid",
  "thumbnails": [],
  "task_id": "celery-task-id",
  "default_thumbnail_url": null,
  "processing_time_ms": null
}
```

The `thumbnails` array is empty until processing completes. Use the task ID to track progress or wait for webhook delivery.

---

### List thumbnails

<span className="badge badge--get">GET</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/thumbnails`

Get all generated thumbnails for a video.

**Auth required:** Yes

```json
[
  {
    "thumbnail_id": "thumb-uuid",
    "url": "https://cdn.ainative.studio/thumbs/abc.jpg",
    "webp_url": "https://cdn.ainative.studio/thumbs/abc.webp",
    "timestamp": 120.0,
    "resolution": "640x360",
    "size_bytes": 45231,
    "is_default": true
  }
]
```

---

### Update default thumbnail

<span className="badge badge--put">PUT</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/default-thumbnail`

Set a specific thumbnail as the default. The default thumbnail is shown in video cards, lists, and previews.

**Auth required:** Yes

```json
{
  "thumbnail_url": "https://cdn.ainative.studio/thumbs/abc.jpg"
}
```

---

### Delete thumbnail

<span className="badge badge--delete">DELETE</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/thumbnails/{thumbnail_id}`

Delete a specific thumbnail. Cannot delete the default thumbnail — set another as default first.

**Auth required:** Yes

---

## Video transcoding

Transcode videos to HLS format with multiple quality levels for adaptive bitrate streaming. Processing runs asynchronously via Celery.

### Start transcoding

<span className="badge badge--post">POST</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/transcode`

Queue a transcoding job. Returns `202 Accepted` immediately.

**Auth required:** Yes  
The video must have status `ready`.

### Request body

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `qualities` | string[] | `["360p","720p","1080p"]` | Quality levels to generate |
| `format` | string | `hls` | Output format (only HLS supported) |
| `priority` | string | `normal` | Processing priority: `normal` or `high` |

**Quality specifications:**

| Quality | Resolution | Bitrate | Use case |
|---------|-----------|---------|----------|
| `360p` | 640×360 | 800 kbps | Mobile / slow connections |
| `720p` | 1280×720 | 2500 kbps | Standard HD |
| `1080p` | 1920×1080 | 5000 kbps | Full HD |

**Estimated processing times:**
- 10-minute video: 5–8 minutes for all three qualities
- 1-hour video: 30–45 minutes for all three qualities

### Response

```json
{
  "job_id": "transcode_video-uuid_1745568000",
  "video_id": "video-uuid",
  "status": "queued",
  "progress": 0,
  "estimated_time": "8m 30s",
  "qualities_requested": ["360p", "720p", "1080p"]
}
```

---

### Get transcoding status

<span className="badge badge--get">GET</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/transcode/status`

Poll for transcoding progress. Poll every 5–10 seconds during processing. Stop when status is `completed` or `failed`.

**Auth required:** Yes

| Query parameter | Type | Description |
|-----------------|------|-------------|
| `job_id` | string | Job ID from the start response. If omitted, returns status of the most recent job. |

### Response

```json
{
  "job_id": "transcode_video-uuid_1745568000",
  "status": "processing",
  "progress": 62,
  "current_quality": "720p",
  "completed_qualities": ["360p"],
  "failed_qualities": [],
  "estimated_time_remaining": "3m 15s",
  "outputs": [
    {
      "quality": "360p",
      "url": "https://cdn.ainative.studio/hls/video-uuid/360p/playlist.m3u8",
      "bitrate": "800k",
      "resolution": "640x360",
      "segment_count": 42,
      "size_bytes": 24300000,
      "status": "completed"
    }
  ],
  "master_playlist_url": null,
  "error": null
}
```

**Status values:**

| Status | Description |
|--------|-------------|
| `queued` | Waiting to start |
| `processing` | Currently transcoding |
| `completed` | All qualities done |
| `failed` | Transcoding failed — check `error` field |

When `status` is `completed`, `master_playlist_url` contains the adaptive bitrate master playlist URL.

---

### Cancel transcoding

<span className="badge badge--delete">DELETE</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/transcode/{job_id}`

Cancel an in-progress transcoding job. Partial results may be preserved. Cannot cancel already-completed jobs.

**Auth required:** Yes

---

### Retry failed quality

<span className="badge badge--post">POST</span> `/api/v1/projects/{project_id}/database/videos/{video_id}/transcode/retry/{quality}`

Retry transcoding for a single quality level that failed. Useful when only one quality fails and others succeed.

**Auth required:** Yes

`quality` must be one of: `360p`, `720p`, `1080p`.
