API reference
Generate videos, check status and manage renders over REST.
Base URL
https://reeloop.ai/api/v1All endpoints are JSON. Authenticate every request with a Bearer token (see Authentication). Errors use the standard envelope { "error": { "code", "message" } } and every response carries X-RateLimit-Limit/Remaining/Reset headers.
Generate a video
POST /api/v1/generate - two explicit modes. Omit mode to use the default ("director"); passing any value other than "instant" or "director" returns a 400 invalid_mode error:
- Director Mode (default) - generates a free draft (script + shot plan) and parks the job at
awaiting_approval. No credits are used yet. You review the draft, then approve it to start the paid render. - Instant Mode - pass
"mode": "instant"to charge credits and start the render immediately (webhook/automation flows).
curl -X POST https://reeloop.ai/api/v1/generate \
-H "Authorization: Bearer fl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"topic": "5 facts about the ocean nobody knows",
"style": "documentary",
"voice": "charlotte",
"language": "en",
"duration": "30",
"mode": "instant"
}'Director Mode response (0 credits consumed):
{
"jobId": "7c9e2a1d-…",
"mode": "director",
"status": "awaiting_approval",
"draft": { "hook": "…", "slides": […], "shots": […], "creditCost": 3 }
}Instant Mode response:
{ "jobId": "7c9e2a1d-…", "status": "processing" }Choose a video engine (optional)
By default every video renders on Reeloop's standard engine at no extra cost. Paid plans can opt into a higher-fidelity engine by passing videoModel with a registry id:
{ "topic": "…", "videoModel": "seedance-2.5" }GET /api/account/plan returns videoFeature.models - the exact ids your account can currently select, each with its resolution (480p/720p/1080p) and creditsPer30s rate; credits scale with the chosen engine, so check that response rather than hardcoding a rate. videoModel only applies to a plain video (no talking-avatar, no UGC mode, no 4K, no Scale/Hybrid videoMode) - combining it with any of those returns 400 model_not_applicable. Omit the field entirely for the default engine, unchanged from before this field existed.
Approve or reject a draft (Director Mode)
POST /api/v1/jobs/{jobId}/approve - approve a draft parked at awaiting_approval. This charges the draft's credit cost and starts the render. Optionally pass draft edits in the body:
{ "hook": "…", "voiceoverScript": "…", "shots": […] }Response:
{ "jobId": "7c9e2a1d-…", "status": "processing" }Approving from the dashboard review UI works identically - same endpoint family. Editing voiceoverScript (or pronunciations) re-synthesizes the voiceover - and therefore the captions - to match; an untouched script reuses the draft's cached audio for free. The body also accepts slides/shots (index-coupled - resulting lengths must match, 1-18 scenes), pronunciations (respellings spoken by the TTS only), and captionStyle.
POST /api/v1/jobs/{jobId}/reject - discard a draft parked at awaiting_approval. No credit was ever consumed, so there is nothing to refund; the job simply closes.
Edit a draft before approving (free, versioned)
GET /api/v1/status/{jobId} on an awaiting_approval job returns the editable draft plus draftRevision - the optimistic-concurrency cursor for saves.
PATCH /api/v1/jobs/{jobId}/draft - persist edits as the draft's next revision without approving. Free - never triggers TTS. Body: { "baseRevision": 1, "edits": { "hook": "…", "voiceoverScript": "…", "slides": […], "shots": […], "pronunciations": […] } }. A stale baseRevision returns 409 revision_conflict with the server's currentRevision to re-base on. Supports Idempotency-Key.
GET /api/v1/jobs/{jobId}/draft/revisions - the append-only version history (newest first); ?revision=N returns one revision's full payload.
Export captions and script
GET /api/v1/jobs/{jobId}/captions?format=srt|vtt - a subtitle file assembled from the stored word-timing transcript. Free. Timings approximate the burned-in captions (the renderer runs its own transcription pass on the audio). 410 when the transcript predates permanent retention.
GET /api/v1/jobs/{jobId}/script - the approved script, per-scene prompts, style bible and pronunciation map as JSON. Free.
Check render status
GET /api/v1/status/{jobId} - poll until renderStatus is done or failed.
{
"jobId": "7c9e2a1d-…",
"status": "completed",
"renderStatus": "done",
"finalVideoUrl": "https://…/media/….mp4",
"videoUrls": ["https://…/scene-1.mp4", "…"],
"aiGenerated": true,
"provenance": { "contentId": "…", "aiGenerated": true, "marking": "…" }
}The two state fields (read this before integrating)
The API exposes two state machines, and you need both:
| Field | Values | Meaning |
|---|---|---|
status | awaiting_approval, processing, completed, failed | The JOB lifecycle. completed only means "no longer running" - not that a video exists. |
renderStatus | null, "rendering", "done", "failed" | The RENDER pipeline outcome. This is the field to check for the video. |
The trap: a render whose final assembly fails ends with status: "completed" and renderStatus: "failed" (credits refunded automatically). Treat the video as ready only when renderStatus === "done" and finalVideoUrl is non-null. Every other combination means "keep waiting" or "it failed".
Failed render example:
{
"jobId": "7c9e2a1d-…",
"status": "completed",
"renderStatus": "failed",
"finalVideoUrl": null
}Generation runs in the background (a finished short is typically ready within a few minutes - real measured demos run ~3-4.5 min). Prefer webhooks over polling for production use.
AI-content marking (EU AI Act, Art. 50)
Every finished video is marked as AI-generated, in layers, to improve detectability across downloads and publishing workflows:
- Embedded in the MP4 - an XMP packet in a
udtabox declaresaiGenerated="true"plus a stablecontentId. Readable withffprobe/exiftool/any XMP parser, and it survives storage and direct download. - In the API - the status payload returns
aiGenerated: trueand aprovenanceblock (contentId,marking). - At publish time - platform-native "AI-generated" disclosure toggles.
To verify a file you downloaded:
exiftool -xmp -b video.mp4 # shows aiGenerated="true" + the contentIdWhat each layer does: the downloaded MP4 carries the embedded XMP marker; the API status response carries the provenance fields; and the native AI-generated flag is sent to the platform at publish time. Be aware that a platform which re-encodes your upload (transcodes it) can strip the file-level XMP metadata - which is exactly why the marking is layered rather than only in the file. Signed C2PA Content Credentials remain on the roadmap.
List videos
GET /api/v1/videos?limit=20&cursor=<ISO timestamp> - cursor-paginated list of your library, newest first.
Publish
POST /api/v1/publish - publish a finished video to your connected accounts:
{ "jobId": "7c9e2a1d-…", "platforms": ["youtube", "tiktok", "instagram"], "caption": "…" }