Videos
A video moves through four stages: create (credits charged), generate (poll status), render (free, produces the MP4) and publish (free). This guide walks the whole lifecycle; publishing and scheduling covers the last stage in depth.
Models and cost
Pick the model in POST /videos. Credits are charged at creation, not at render:
| Model | Credits | What you get |
|---|---|---|
storyboard | 20 | Static AI scenes with motion effects |
motion_lite | 50 | Animated transitions between scenes |
motion_pro | 100 | Cinematic AI motion video |
GET /options?kind=models returns the same catalog with costs, so an agent can decide programmatically.
1. Create (POST /videos, scope videos:write)
Send a finished narration script and a voice id (from GET /voices). Optional fields: style, language, name, enableBackgroundMusic, masterStyle, globalNegativePrompt.
curl -s -X POST https://faceless.so/api/v1/videos \
-H "Authorization: Bearer $FACELESS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"script": "Did you know the ocean has lakes and rivers of its own? ...", "voiceId": "EXAVITQu4vr4xnSDxMaL", "model": "storyboard"}'
Response (201):
{
"success": true,
"data": {
"id": "665f1b2a9c31a2b3c4d5e801",
"name": "Did you kn...",
"model": "storyboard",
"status": "processing",
"creditsUsed": 20,
"statusUrl": "/api/v1/videos/665f1b2a9c31a2b3c4d5e801/status"
}
}
Common errors: 402 insufficient_credits (top up before retrying), 403 usage_limit_reached (plan project limit, upgrade to create more), 429 rate_limited (10 creations per 60s per key; wait for Retry-After).
2. Poll generation (GET /videos/{id}/status, scope videos:read)
Generation is asynchronous and typically takes a few minutes. Poll every 10 to 30 seconds until status is completed or failed:
curl -s https://faceless.so/api/v1/videos/665f1b2a9c31a2b3c4d5e801/status \
-H "Authorization: Bearer $FACELESS_API_KEY"
{
"success": true,
"data": { "id": "665f1b2a9c31a2b3c4d5e801", "status": "processing", "percentCompleted": 62, "readyForEditing": false, "errorMessages": [], "renderedVideoUrl": null }
}
On failed, read errorMessages for the reason. With the CLI, faceless videos create --wait and faceless videos status <id> handle the polling.
3. Render (POST /videos/{id}/render, scope videos:write)
Rendering assembles the final MP4 in the cloud. It is free (credits were charged at creation) and requires generation to be completed first.
curl -s -X POST https://faceless.so/api/v1/videos/665f1b2a9c31a2b3c4d5e801/render \
-H "Authorization: Bearer $FACELESS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"codec": "h264"}'
The 202 response contains a renderId. Poll GET /renders/{renderId} (scope videos:read) every 5 to 15 seconds; renders typically finish in under two minutes:
curl -s https://faceless.so/api/v1/renders/abcd1234efgh \
-H "Authorization: Bearer $FACELESS_API_KEY"
{
"success": true,
"data": { "renderId": "abcd1234efgh", "status": "done", "overallProgress": 1, "url": "https://exports.faceless.so/renders/665f1b2a9c31a2b3c4d5e801/1722333444555.mp4" }
}
status is in-progress (with overallProgress 0 to 1), done (with the MP4 url) or error. The MP4 url is a direct download; it also appears as renderedVideoUrl on the video.
4. Publish
Publish immediately with POST /posts, or set per-platform metadata with PATCH /videos/{id} and schedule with POST /posts/schedule. Both are free; see publishing and scheduling.
Thumbnails
Every video that finishes generating gets three AI thumbnail variants, and the best one is selected automatically. This is free and on by default; you do not have to ask for it.
Read them from GET /videos/{id}:
{
"thumbnailUrl": "https://exports.faceless.so/thumbnails/665f.../1/a1b2c3d4.jpg",
"thumbnails": {
"status": "ready",
"generation": 1,
"aspect": "9:16",
"selectedVariantId": "a1b2c3d4-0000-4000-8000-000000000001",
"selectedBy": "ai",
"variants": [
{ "id": "a1b2...001", "url": "https://...a1b2c3d4.jpg", "hookText": "OCEAN RIVERS", "source": "ai", "score": 88, "grade": "S" }
]
}
}
thumbnailUrl is always the effective thumbnail - whatever is actually in use. status is one of idle (never generated), queued, generating, ready, partial (fewer than three variants survived, which still gives you a usable thumbnail) or failed. A thumbnail failure never fails the video.
Choose a different variant with POST /videos/{id}/thumbnail (scope videos:write):
curl -s -X POST https://faceless.so/api/v1/videos/$VIDEO_ID/thumbnail \
-H "Authorization: Bearer $FACELESS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"variantId": "a1b2c3d4-0000-4000-8000-000000000002"}'
The response is the full video object with the new thumbnailUrl. Your choice is recorded as a user pick, which means later automatic regenerations will not overwrite it.
Where the thumbnail is used. It is pushed to YouTube as the custom thumbnail after upload, and sent to Instagram as the Reels cover_url. Both are resolved at post time, so changing the pick after scheduling a post changes what gets published. YouTube shows custom thumbnails in search, on your channel page and in suggested videos; the Shorts feed shows a frame from the video itself regardless. YouTube also rejects custom thumbnails on unverified channels, in which case the upload still succeeds without one.
Note on list responses. GET /videos now returns the effective thumbnail in thumbnailUrl, falling back to the first b-roll frame only when there is no thumbnail. It previously always returned the first b-roll frame.
Captioning existing footage (POST /videos/captions)
To caption a video or audio file you already have (no AI visuals, no credit cost), send its public URL. Processing is asynchronous; poll GET /videos/{id}/status like any other video.
curl -s -X POST https://faceless.so/api/v1/videos/captions \
-H "Authorization: Bearer $FACELESS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"videoUrl": "https://example.com/clip.mp4", "language": "English"}'
Provide videoUrl or audioUrl (an audio file becomes a captioned video). Rate limit: 10 per 300s.
Managing videos
GET /videos(scope videos:read): paginated list, newest first, withrenderedVideoUrlwhen available.?archived=truefor archived only.GET /videos/{id}: one video with script, voice, model, status and post metadata.PATCH /videos/{id}(scope videos:write): rename, or set per-platform post metadata before scheduling.POST /videos/{id}/thumbnail(scope videos:write): choose which thumbnail variant the video uses (see above).DELETE /videos/{id}(scope videos:write): permanent, no credit refund.
A 404 not_found on any of these means the id does not exist or belongs to another team.