A video created through the Content API, or by expansion's summary_video output, is an editable plan of scenes, narration scripts, and subtitles. It is not an mp4 yet. After a person has reviewed it in the TutorFlow video editor and generated its narration, render it with the routes below. Rendering uses the same render engine and price as the TutorFlow editor; see Pricing.
| Method | Path | Scope | Success |
|---|---|---|---|
POST | /v1/content/classrooms/{classroomId}/videos/{videoId}/render | content:generate | 202 |
GET | /v1/content/classrooms/{classroomId}/videos/{videoId}/render | content:read | 200 |
POST | /v1/content/classrooms/{classroomId}/videos/{videoId}/render/cancel | content:generate | 200 with { "status": "ok" } |
Start a render
curl -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/videos/$VIDEO_ID/render" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Idempotency-Key: video:$VIDEO_ID:render:v1"Response 202:
{
"status": "accepted",
"renderStatus": "RENDERING",
"estimatedSeconds": 180,
"totalDurationSeconds": 120
}- Send an
Idempotency-Key. A retry with the same key replays this response instead of starting and charging a second render. Use a new key, such asvideo:{videoId}:render:v2, when you mean to render again. - A video that is already rendering returns
409content_video_render_in_progress. Wait for it, or cancel it first to restart on purpose. - Every scene needs generated narration audio, not only a
script. Narration audio is generated in the TutorFlow video editor; the Content API does not generate it. A video with a scene that has no narration audio returns400before anything is charged, so have a person open the video in the editor, generate narration, and review it before you render. 402means the organization does not have enough AI Credits.- A render started through the API sends no email. It reports through the
video.render.completedandvideo.render.failedwebhooks, which are also sent for renders started in TutorFlow.
Read the render status
curl "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/videos/$VIDEO_ID/render" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY"{
"renderStatus": "COMPLETED",
"renderError": null,
"progress": 100,
"videoUrl": "https://cdn.tutorflow.io/orgs/.../videos/rendered/...mp4?Expires=...&Signature=...",
"videoUrlExpiresAt": "2026-09-30T15:30:00.000Z",
"thumbnailUrl": "https://cdn.tutorflow.io/...",
"updatedAt": "2026-09-30T09:30:00.000Z"
}| Field | Notes |
|---|---|
renderStatus | IDLE, RENDERING, COMPLETED, or FAILED. |
renderError | On FAILED, the fixed message "The render failed. Start it again, or contact TutorFlow support if it keeps failing." Otherwise null. |
progress | 0 to 99 while rendering when the engine reports it, 100 when complete, otherwise null. |
videoUrl | A link to the mp4, usually signed. null until renderStatus is COMPLETED. |
videoUrlExpiresAt | When the signed link stops working, 6 hours after it was issued, or null when the link is not signed. Read the status again for a new link. |
thumbnailUrl | A link to the thumbnail when TutorFlow stores it for this classroom, otherwise null. |
updatedAt | When the video last changed. |
Poll every 10 to 15 seconds while renderStatus is RENDERING, or wait for the webhook. estimatedSeconds from the start response is a reasonable first wait.
Download the mp4 promptly and store your own copy if you need it for longer than the link lasts. videoUrl is the only usable link to the rendered file; the video object itself does not return the file's storage key.
Cancel
POST .../render/cancel stops a running render and answers { "status": "ok" }. The render was charged when it started, and canceling does not refund it; see Pricing.
A render loop
# curl + jq: start, then poll until the render finishes
curl -sS -X POST "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/videos/$VIDEO_ID/render" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY" \
-H "Idempotency-Key: video:$VIDEO_ID:render:v1"
while true; do
STATUS_JSON="$(curl -sS "$TUTORFLOW_API_BASE_URL/v1/content/classrooms/$CLASSROOM_ID/videos/$VIDEO_ID/render" \
-H "Authorization: Bearer $TUTORFLOW_CONTENT_API_KEY")"
RENDER_STATUS="$(printf '%s' "$STATUS_JSON" | jq -r '.renderStatus')"
echo "renderStatus: $RENDER_STATUS"
if [ "$RENDER_STATUS" != "RENDERING" ]; then break; fi
sleep 15
done
printf '%s\n' "$STATUS_JSON" | jq '{renderStatus, videoUrl, videoUrlExpiresAt}'The same loop in Node.js and Python is in Examples.