Courses
A course turns one source - a document or an existing video - into a set of modules, each of which becomes exactly one video.
The pipeline runs in three steps, and each is asynchronous:
- Create → the source is extracted and split into modules
- Storyboard → a module gets the scenes its video will be made of
- Generate → a module is rendered into a video
Every step returns a jobId. Poll GET /v1/jobs/:id exactly as you would for any other
async operation.
Create a course
POST /v1/courses
Give either a fileUrl + filename, or a sourceVideoId - not both.
| Field | Type | Description |
|---|---|---|
fileUrl | string | Public URL of the document (PDF, DOCX, PPTX, Keynote, TXT) |
filename | string | Required with fileUrl - its extension decides which extractor runs |
sourceVideoId | string | Build from an existing video instead of a document |
instructions | string | Guidance for the planner: audience, tone, what to emphasise |
curl -X POST https://api.vidocu.ai/v1/courses \
-H "Authorization: Bearer vdo_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"fileUrl": "https://example.com/onboarding-handbook.pdf",
"filename": "onboarding-handbook.pdf",
"instructions": "For new warehouse staff. Keep each module under two minutes."
}'
Response 202 Accepted
{ "courseId": "dvp_1786480000000_a1b2c3d4", "jobId": "doc_plan_...", "status": "extracting" }
When that job completes, GET /v1/courses/:id shows the modules the planner proposed.
The document is fetched by our servers, so it must be publicly reachable - internal and private addresses are refused.
List and read
GET /v1/courses · GET /v1/courses/:id
{
"id": "dvp_...",
"title": "Onboarding handbook",
"status": "plan_ready",
"source": { "filename": "onboarding-handbook.pdf", "kind": "pdf", "videoId": null },
"moduleCount": 4,
"modules": [
{
"id": "dm_...",
"title": "Receiving a delivery",
"objective": "Check in a pallet against its work order",
"status": "ready_to_generate",
"sourcePages": [3, 4, 5],
"targetDurationSec": 95,
"sceneCount": 7,
"videoId": null
}
]
}
GET /v1/courses supports status, page/limit and cursor.
Storyboard
GET /v1/courses/:id/storyboard returns every module's scenes, plus two fields worth reading:
omissions- source content the storyboard deliberately left out. Reviewable rather than silent, so you can tell "it missed this" from "it chose to skip this".warnings- non-blocking review hints.
POST /v1/courses/:id/storyboard builds one module's storyboard:
curl -X POST https://api.vidocu.ai/v1/courses/dvp_.../storyboard \
-H "Authorization: Bearer vdo_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"moduleId": "dm_..."}'
Returns 409 if that module is already being worked on - only one step runs per module at a time.
Generate
POST /v1/courses/:id/generate
Renders one module into a video. It needs a storyboard first. Same body, same 409 rule.
When the job completes, the module's videoId points at an ordinary video - so
export, translate and everything else work on it
normally.
Update and delete
PATCH /v1/courses/:id changes title, userInstructions, or projectId (the folder its videos
are filed under). Modules and status are owned by the pipeline and can't be set by hand - editing
them would desync a running job.
DELETE /v1/courses/:id removes the course plan. Videos already generated from its modules are
kept - they're ordinary videos with their own lifecycle. The response tells you how many:
{ "success": true, "id": "dvp_...", "deleted": true, "generatedVideosKept": 2 }
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing or conflicting source, or a bad moduleId |
| 401 | authentication_error | Invalid or missing API key |
| 403 | insufficient_scope | Token lacks courses:read / courses:write |
| 404 | not_found | Course or module not found, or it belongs to another workspace |
| 409 | conflict | A step is already running for that module |