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:

ModelCreditsWhat you get
storyboard20Static AI scenes with motion effects
motion_lite50Animated transitions between scenes
motion_pro100Cinematic 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.

bash
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):

json
{
  "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:

bash
curl -s https://faceless.so/api/v1/videos/665f1b2a9c31a2b3c4d5e801/status \
  -H "Authorization: Bearer $FACELESS_API_KEY"
json
{
  "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.

bash
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:

bash
curl -s https://faceless.so/api/v1/renders/abcd1234efgh \
  -H "Authorization: Bearer $FACELESS_API_KEY"
json
{
  "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}:

json
{
  "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):

bash
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.

bash
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, with renderedVideoUrl when available. ?archived=true for 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.