List Videos
GET /v1/videos
List all videos in your workspace with pagination and optional status filtering.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | No | 1 | Page number (minimum 1) |
limit | integer | No | 20 | Results per page (1–100) |
status | string | No | — | Filter by status: draft, in_progress, or completed |
projectId | string | No | — | Return only videos in the given project folder. 404 if the folder doesn't belong to the workspace. |
uncategorized | boolean | No | — | If true, return only videos with no project folder. Overrides projectId. |
backup | string | No | — | Filter by package backup state: none returns videos that have never had a package built, taken returns those that have. See Package backups. |
approval | string | No | — | Filter by the latest approval decision: pending, approved, rejected or withdrawn. |
kb | string | No | — | Filter by Knowledge Base state: published, not_published (no article, or only drafts) or none (no article was ever created). See Knowledge Base state. |
languageGroup | string | No | — | Return every language of one instruction. See Language families. |
Example
curl "https://api.vidocu.ai/v1/videos?page=1&limit=10&status=completed" \
-H "Authorization: Bearer vdo_live_your_key_here"
Response 200 OK
{
"videos": [
{
"id": "vid_1708300000000_abc1234",
"projectId": "0f7c1d8a-2c4b-4a9c-9f2a-1b8a3a5e0c11",
"name": "Product Demo Q1",
"status": "completed",
"videoUrl": "https://storage.vidocu.ai/...",
"thumbnailUrl": "https://storage.vidocu.ai/...",
"videoDuration": 120.5,
"language": "en",
"exportedVideoUrl": "https://storage.vidocu.ai/...",
"exportedSrtUrl": "https://storage.vidocu.ai/...",
"exportedVoiceoverUrl": "https://storage.vidocu.ai/...",
"exportedAt": "2024-02-19T10:30:00.000Z",
"createdAt": "2024-02-18T14:00:00.000Z",
"updatedAt": "2024-02-19T10:30:00.000Z"
}
],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5
}
}
Video object fields
| Field | Type | Description |
|---|---|---|
id | string | Unique video ID |
projectId | string | null | Parent project folder ID (null when uncategorized) |
name | string | Display name |
status | string | draft, in_progress, or completed |
videoUrl | string | Source video URL |
thumbnailUrl | string | null | Generated thumbnail URL |
videoDuration | number | null | Duration in seconds |
language | string | null | Detected language code |
exportedVideoUrl | string | null | Exported video URL (after export) |
exportedSrtUrl | string | null | Exported SRT file URL (after export) |
exportedVoiceoverUrl | string | null | Combined voiceover MP3 URL (after export, if voiceover was generated) |
exportedAt | string | null | Export timestamp |
locked | boolean | Whether the video's content is frozen (see Lock state) |
lock | object | null | Who locked it and when, when locked is true |
packageDownload | object | null | The last package backup, or null if none was ever built. See Package backups. |
languageGroup | string | null | The language family this package belongs to, or null if it was never translated. See Language families. |
translatedFrom | string | null | The package this one was translated from, or null. |
createdAt | string | Creation timestamp |
updatedAt | string | Last update timestamp |
Results are sorted by updatedAt descending (most recent first).
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Invalid query parameter values |
| 401 | authentication_error | Invalid or missing API key |
Cursor pagination
page works, but it drifts on a collection that changes while you walk it: a video created between
page 1 and page 2 shifts everything down, so you see an item twice or miss it entirely. Anything
polling on a schedule hits that.
Every list response includes pagination.nextCursor. Pass it back as cursor to resume exactly
after the last row you saw, regardless of what was inserted meanwhile:
# first page
curl "https://api.vidocu.ai/v1/videos?limit=50" -H "Authorization: Bearer ..."
# next page — cursor replaces page
curl "https://api.vidocu.ai/v1/videos?limit=50&cursor=eyJ0IjoiMjAyNi0wOC0wOVQxMDowMDowMC4wMDBaIiwiaWQiOiJ2aWRfMiJ9" \
-H "Authorization: Bearer ..."
nextCursor is null on the last page. Treat the value as opaque - it's a token to hand back, not
a structure to build. The same applies to GET /v1/jobs.
Package backups
A package is the offline evidence bundle for one video: the exported MP4, the article PDF, the training material and a manifest recording lock status, publisher, approvers and content hashes. packageDownload records the last time one was produced.
| Field | Type | Description |
|---|---|---|
downloadedAt | string | When the package finished building and its link was sent |
downloadedBy | string | User ID of the person who asked for it |
downloadedByName | string | null | Their display name at that moment |
null means no package has ever been built for this video.
The stamp is written when the ZIP finishes building, not when someone clicks the emailed link. The build is the observable event, and it is the one that answers "has a backup been taken" — an email nobody opened does not un-take it.
The filter exists for one question in particular: which published content has no offline backup yet.
curl "https://api.vidocu.ai/v1/videos?status=completed&backup=none" \
-H "Authorization: Bearer vdo_live_your_key_here"
Knowledge Base state
kb answers a question the Knowledge Base's own listings structurally cannot: which content never got there. A video with no article does not appear on any article listing, so its absence is invisible exactly where somebody would look for it.
| Value | Returns |
|---|---|
published | Videos with at least one live Knowledge Base article |
not_published | Videos that never went live there — no article at all, or only drafts |
none | The narrower case: no article was ever created from the video |
State is resolved from the articles themselves on every request rather than cached on the video, so it cannot go stale after an article is archived or deleted.
# Approved, signed off, and never published to the Knowledge Base
curl "https://api.vidocu.ai/v1/videos?approval=approved&kb=not_published" \
-H "Authorization: Bearer vdo_live_your_key_here"
Language families
Translating a package creates a second package: its own id, its own approval, its own lock, its own Knowledge Base article. That is deliberate — a work instruction and its translation are two documents a quality system signs off separately, and freezing one should not freeze the other.
What connects them is languageGroup: the id of the first package in the family, carried by every member including the source. Ask for it and you get the whole set.
# Every language of one instruction, the source included
curl "https://api.vidocu.ai/v1/videos?languageGroup=vid_abc123" \
-H "Authorization: Bearer vdo_live_your_key_here"
The source's own id works as the group id, so you can ask with the id you already have rather than fetching the video first to read its languageGroup.
Each member carries its own language, so pairing a family with an ERP record is a match on languageGroup and a filter on language.
Two things are worth knowing before you build against this:
- The training is shared, not copied. Every package in a family points at the same training module, which carries its own per-language translations. Somebody who passed the assessment in German has passed it — the Training Planner has one row per person per training, not one per language.
languageGroupisnullon packages translated before this shipped. The field is written when a package is translated, and no migration invented families for older translations. A package withnullis a family of one as far as the API is concerned; translate it again and it starts a real family.