# reelsdk > Video API for apps that can't run ffmpeg (Supabase Edge Functions, Vercel, Cloudflare Workers, no-code tools). One JSON request in, finished mp4 out. Base URL: https://api.reelsdk.com Auth: `Authorization: Bearer rk_live_...` on every /v1 call. /f/ downloads need no auth. OpenAPI 3.1: https://api.reelsdk.com/openapi.json Docs: https://reelsdk.com/docs ## Sync vs async Create calls wait up to 60 s for the result. Change it with `?wait=0..300`, `Prefer: wait=N` or `Prefer: respond-async`. - 200 `{ "job": {...} }`: finished, download URLs in `job.outputs`. - 202 `{ "job": {...} }` with `Location` and `Retry-After: 5`: still running. Poll `GET /v1/jobs/{id}?wait=60` until it is 200, or pass `webhook_url`. - A failed job answers 422 or 500 with `{ "error": {...}, "job": {...} }`. ## Endpoints ### POST /v1/shorts: topic or script to a finished short Writes the script, makes one AI image per scene, voices it, adds music and word-level captions. Takes 60-120 s, so expect 202 and poll. {"topic": "why octopuses have three hearts", "duration_s": 45, "aspect": "9:16", "style": "cinematic"} Fields: topic (max 500 chars) or script (max 3000 chars and 90 s of narration at the voice's pace, about 250 words with the default English voice; read verbatim, wins over topic), duration_s 15..90 (default 45), aspect 9:16|1:1|16:9, style cinematic|photoreal|anime|3d-cartoon|watercolor|comic|dark-documentary|oil-painting, language en|es|fr|ja|zh|ko, voice (en: 40 aura-2 voices, default luna; es: 10, default aquila), music auto|none|lofi|chill|upbeat|cinematic|dramatic|dark|energetic|inspiring, captions false|bold|karaoke|minimal|{...style}, webhook_url. Outputs: video, poster, transcript, srt, vtt. `job.result` = { title, post_caption, scenes: [{ narration, image }] }. `job.stage` = planning, voice, images, rendering while it runs (voice and images are made in parallel). ### POST /v1/slideshow: images (+ voiceover, music, captions) to mp4 {"images": ["https://example.com/1.jpg", "https://example.com/2.jpg"], "voiceover": {"text": "Two photos, one story.", "voice": "luna"}, "captions": "bold"} voiceover is a URL or { text, voice }. Captions need a voiceover. images[].seconds is that image's slot on the timeline; without it the images share the voiceover. Outputs: video (+ transcript, srt, vtt, ass with captions). ### POST /v1/captions: burn word-level captions into a video {"video_url": "https://example.com/talk.mp4", "style": "karaoke"} Outputs: video, transcript, srt, vtt, ass. Pass words [{ word, start, end }] to skip transcription, or prompt (max 500 chars) with names and terms to spell correctly. ### POST /v1/ffmpeg: run ffmpeg with your own arguments {"inputs": {"in.mp4": "https://example.com/in.mp4"}, "args": ["-i", "in.mp4", "-vf", "scale=720:-2", "out.mp4"], "outputs": ["out.mp4"]} No shell, the job dir is the cwd. Outputs are keyed by filename. Inputs and outputs are plain file names in the job dir: no URLs or protocols (http:, tcp:, concat:...), no absolute paths or "..", no movie/amovie filters (add another -i), no -f tee, no glob patterns. Bundled fonts: fontfile=/usr/share/fonts/reelapi/. ### GET /v1/jobs/{id} Job status. `?wait=N` holds the request until the job finishes or N seconds pass. ### POST /v1/uploads?filename=clip.mp4 Raw request body (max 100 MB, Content-Length required): images, video, audio, srt/vtt/ass, json or txt. Returns { "url", "expires_at" }; use the url as any input URL for 7 days. ### POST /v1/uploads/multipart?filename=talk.mp4&bytes=: files over 100 MB, up to 5 GB Returns 201 { upload_id, url, part_size, parts, expires_at }. PUT each part's raw bytes with Content-Length to /v1/uploads/multipart/{upload_id}/{part} (1-based; every part is exactly part_size, 64 MiB, except the last) and keep the { part, etag } it returns. Then POST /v1/uploads/multipart/{upload_id}/complete with {"parts": [{"part": 1, "etag": "..."}]} for the same 201 as /v1/uploads; the url works from then on. DELETE /v1/uploads/multipart/{upload_id} aborts (204, or 409 UPLOAD_COMPLETED after complete). upload_id is valid for 24 h and only for your key; the declared bytes count against the daily upload allowance at the start, and an abort on the same UTC day gives them back once. ### GET /f/{key} Outputs and uploads. Range requests supported. ## Job { id, op, status: queued|running|succeeded|failed, stage, created_at, finished_at, outputs: { name: url }, result, duration_s, error: { code, message } | null, usage: { output_seconds, audio_seconds, tts_characters, images }, poll_url } ## Webhooks `webhook_url` gets a POST with the job JSON when the job finishes, header `Reel-Signature: t=,v1=." keyed with your webhook secret>`. 3 attempts, each with `Idempotency-Key: :`. ## Limits JSON body 2 MB. Inputs 500 MB each and 1 GB per job, media up to 15 min. Jobs run at most 10 min. Outputs and uploads expire after 7 days. Per key: 3 jobs at once, 500 jobs and 20 GB of uploads per UTC day unless your key says otherwise. Over a limit you get 429 RATE_LIMITED with Retry-After (seconds). ## Credit Keys from the dashboard (https://reelsdk.com/dashboard) spend prepaid credit; new accounts get $1.00. A job reserves up to its maximum price when it starts and pays what it actually used when it ends; failed jobs are free, and `job.cost_usd` shows the charge. Without enough credit a create call answers 402 INSUFFICIENT_CREDIT with `balance_usd`, `required_usd` and `billing_url`; top up and retry. ## Errors Body: { "error": { "code", "message" } } - 400 INVALID_REQUEST (error.issues lists { path, message }), INVALID_JSON, UNSUPPORTED_LANGUAGE, UNSUPPORTED_FILE_TYPE - 401 UNAUTHORIZED, 402 INSUFFICIENT_CREDIT, 403 HTTPS_REQUIRED (plain http), 404 NOT_FOUND, 405 METHOD_NOT_ALLOWED, 413 PAYLOAD_TOO_LARGE, 429 RATE_LIMITED - 422 your inputs: INPUT_FETCH_FAILED, INPUT_TOO_LARGE, INPUT_UNSUPPORTED, FFMPEG_FAILED, INVALID_SPEC, CONTENT_REJECTED, OUTPUT_TOO_LARGE - 500 ours, safe to retry: INTERNAL, TIMEOUT, BUSY, ENGINE_ERROR, STORE_FAILED, TTS_FAILED, PLAN_FAILED, IMAGES_FAILED