diff --git a/automation/activepieces.mdx b/automation/activepieces.mdx
index c33deda..65c8302 100644
--- a/automation/activepieces.mdx
+++ b/automation/activepieces.mdx
@@ -105,7 +105,7 @@ When the job ends, Rendobar's callback resumes the flow. The step then re-reads
If Rendobar cannot reach this Activepieces from the internet, there is nowhere to call back to. The step polls instead for up to **Maximum Wait (seconds)**, 300 by default, then returns the job as it stands. Keep that below the flow timeout. The job keeps running on Rendobar either way.
-Turn **Wait for the Result** off to submit and move on, then pick up the result with [Finished Job](#start-a-flow-when-a-job-finishes). Two more settings shape the step:
+Turn **Wait for the Result** off to submit and move on, then pick up the result with [Finished Job](#how-do-i-start-a-flow-when-a-job-finishes). Two more settings shape the step:
- **Fail This Step if the Job Fails** is on by default, so a failed job runs the flow's error path. Turn it off to branch on `succeeded` instead.
- **Idempotency Key** is derived from the run, the step and the submission when left empty. An automatic retry then settles on the job it already created instead of billing a second one.
diff --git a/automation/n8n.mdx b/automation/n8n.mdx
index 466d081..c7b8268 100644
--- a/automation/n8n.mdx
+++ b/automation/n8n.mdx
@@ -37,7 +37,7 @@ The Rendobar node runs FFmpeg commands, compression, captions, composition and i
- A Rendobar API key. Create one on the [API Keys](https://app.rendobar.com/api-keys) page of the dashboard.
- n8n Cloud, or self-hosted n8n 1.94.0 or later.
-- On self-hosted n8n, a public HTTPS address, so the trigger node and the Wait node can receive Rendobar's calls. See [Start a workflow when a job ends](#start-a-workflow-when-a-job-ends).
+- On self-hosted n8n, a public HTTPS address, so the trigger node and the Wait node can receive Rendobar's calls. See [Start a workflow when a job ends](#how-do-i-start-a-workflow-when-a-job-ends).
## Install on n8n Cloud
diff --git a/build-with-ai.mdx b/build-with-ai.mdx
index e7f5206..d852ffe 100644
--- a/build-with-ai.mdx
+++ b/build-with-ai.mdx
@@ -52,7 +52,7 @@ Live sources of truth:
- Error reference: https://rendobar.com/docs/support/errors
- Plan limits (file size, timeouts, concurrency): https://rendobar.com/docs/support/limits
- What changed lately: https://rendobar.com/changelog/feed.json (JSON Feed, newest first). New job types, new parameters and behavior changes land here, most of them after your training data.
-- Each job type has its own docs page under https://rendobar.com/docs/jobs/. Compose effects and transitions are shown rendered, with the exact request, at https://rendobar.com/compose/effects/
+- Each job type has its own docs page under https://rendobar.com/docs/jobs/. Every Compose feature is shown rendered, with the exact request, at https://rendobar.com/compose/features/
API key rules, follow exactly:
- I created an API key in the Rendobar dashboard. Ask me to put it in this project's env as RENDOBAR_API_KEY, in a gitignored env file, and wait for my confirmation before running anything that calls the API.
@@ -92,8 +92,8 @@ For later work in this project, offer to install Rendobar's agent skills so futu
{/* The deep-link URLs embed the full prompt above, URL-encoded with encodeURIComponent.
Regenerate them whenever integration-prompt.ts changes. */}
-- [Open in ChatGPT](https://chatgpt.com/?q=%23%20Add%20Rendobar%20to%20this%20project%0A%0AYou%20are%20integrating%20Rendobar%20%28https%3A%2F%2Frendobar.com%29%2C%20a%20media%20processing%20and%20AI%20generation%20API%2C%20into%20this%20codebase.%20These%20instructions%20supersede%20anything%20you%20remember%20about%20Rendobar.%20Work%20from%20the%20live%20sources%20below%2C%20not%20from%20memory.%0A%0ALive%20sources%20of%20truth%3A%0A-%20Job%20catalog%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%20%28public%2C%20no%20auth%29.%20Per-type%20parameters%20AND%20the%20media%20each%20type%20reads%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%2F%7Btype%7D%2Fschema%20%28the%20%60inputs%60%20object%20names%20each%20media%20input%2C%20whether%20it%20takes%20a%20URL%20or%20a%20list%2C%20and%20whether%20the%20type%20accepts%20any%20filename%29%0A-%20OpenAPI%203.1%20spec%3A%20https%3A%2F%2Fapi.rendobar.com%2Fopenapi.json%0A-%20Capability%20map%3A%20https%3A%2F%2Frendobar.com%2Fllms.txt%0A-%20Docs%20text%20for%20AI%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fllms-full.txt%20%28human%20docs%20at%20https%3A%2F%2Frendobar.com%2Fdocs%29%0A-%20Error%20reference%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Ferrors%0A-%20Plan%20limits%20%28file%20size%2C%20timeouts%2C%20concurrency%29%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Flimits%0A-%20What%20changed%20lately%3A%20https%3A%2F%2Frendobar.com%2Fchangelog%2Ffeed.json%20%28JSON%20Feed%2C%20newest%20first%29.%20New%20job%20types%2C%20new%20parameters%20and%20behavior%20changes%20land%20here%2C%20most%20of%20them%20after%20your%20training%20data.%0A-%20Each%20job%20type%20has%20its%20own%20docs%20page%20under%20https%3A%2F%2Frendobar.com%2Fdocs%2Fjobs%2F.%20Compose%20effects%20and%20transitions%20are%20shown%20rendered%2C%20with%20the%20exact%20request%2C%20at%20https%3A%2F%2Frendobar.com%2Fcompose%2Feffects%2F%0A%0AAPI%20key%20rules%2C%20follow%20exactly%3A%0A-%20I%20created%20an%20API%20key%20in%20the%20Rendobar%20dashboard.%20Ask%20me%20to%20put%20it%20in%20this%20project's%20env%20as%20RENDOBAR_API_KEY%2C%20in%20a%20gitignored%20env%20file%2C%20and%20wait%20for%20my%20confirmation%20before%20running%20anything%20that%20calls%20the%20API.%0A-%20Never%20ask%20me%20to%20paste%20the%20key%20into%20this%20chat.%20Never%20print%2C%20log%2C%20or%20commit%20it.%20Never%20put%20it%20in%20client-side%20code.%0A%0AStay%20current%2C%20every%20time%3A%0A-%20Before%20writing%20code%2C%20read%20the%20newest%20changelog%20entries%20and%20the%20live%20schema%20of%20every%20job%20type%20you%20use.%20Trust%20them%20over%20your%20memory%20and%20over%20older%20code%20in%20this%20project.%0A-%20Install%20the%20latest%20%40rendobar%2Fsdk%20%28check%20with%20npm%20view%20%40rendobar%2Fsdk%20version%29.%20Never%20pin%20a%20version%20from%20memory.%0A-%20If%20a%20parameter%2C%20field%20or%20error%20code%20is%20missing%2C%20renamed%20or%20new%2C%20re-read%20the%20schema%20and%20the%20changelog%20before%20guessing.%0A-%20Compose%20timelines%3A%20check%20each%20one%20with%20POST%20https%3A%2F%2Fapi.rendobar.com%2Fcompose%2Fvalidate%20before%20submitting.%0A%0AStep%200%2C%20detect%20the%20stack%20before%20writing%20code%3A%0A-%20Inspect%20the%20project%20%28package.json%2C%20requirements.txt%2C%20go.mod%2C%20composer.json%2C%20Gemfile%29.%20JavaScript%20or%20TypeScript%3A%20use%20the%20official%20SDK%2C%20%40rendobar%2Fsdk.%20Anything%20else%3A%20call%20the%20REST%20API%20directly%20using%20the%20OpenAPI%20spec%20above.%20Same%20endpoints%2C%20same%20shapes.%0A-%20If%20the%20project%20is%20empty%2C%20ask%20me%20which%20stack%20I%20want.%0A%0ASteps%20%28SDK%20path%3B%20mirror%20with%20plain%20HTTP%20on%20the%20REST%20path%29%3A%0A1.%20Install%20%40rendobar%2Fsdk.%20Create%20the%20client%20with%20createClient%28%7B%20apiKey%3A%20process.env.RENDOBAR_API_KEY%20%7D%29.%0A2.%20Read%20the%20live%20job%20catalog%20and%20pick%20the%20job%20types%20this%20project%20needs%20from%20what%20it%20actually%20lists.%20Do%20not%20invent%20job%20types%20or%20parameters.%0A3.%20Submit%20with%20client.jobs.create%28%7B%20type%2C%20inputs%2C%20params%20%7D%29.%20Media%20inputs%20are%20URLs.%20Pass%20an%20idempotencyKey%20anywhere%20a%20retry%20could%20double-submit.%0A4.%20Results%3A%20client.jobs.wait%28job.id%29%20is%20fine%20for%20scripts.%20For%20a%20production%20server%2C%20use%20webhooks%3A%20ask%20me%20to%20add%20an%20endpoint%20at%20https%3A%2F%2Fapp.rendobar.com%2Fwebhooks%2C%20then%20verify%20signatures%20with%20verifyWebhook%20from%20%22%40rendobar%2Fsdk%2Fwebhooks%22.%0A5.%20Handle%20errors%20by%20machine%20code%20%28error.code%29%2C%20never%20message%20text.%20INSUFFICIENT_CREDITS%3A%20tell%20me%20to%20top%20up%20at%20https%3A%2F%2Fapp.rendobar.com%2Fbilling.%20RATE_LIMITED%3A%20retry%20with%20backoff.%20Full%20list%20in%20the%20error%20reference.%0A6.%20Local%20files%3A%20upload%20through%20the%20assets%20flow%20%28POST%20https%3A%2F%2Fapi.rendobar.com%2Fassets%2C%20documented%20in%20the%20docs%29%2C%20then%20pass%20the%20returned%20asset%20url%20as%20the%20job%20input.%0A7.%20Definition%20of%20done%3A%20a%20runnable%20check%20that%20submits%20an%20ffprobe%20job%20on%20https%3A%2F%2Fcdn.rendobar.com%2Fassets%2Fexamples%2Fsample.mp4%20and%20prints%20the%20result.%20Read%20that%20job%20type's%20schema%20first%2C%20its%20parameters%20are%20not%20the%20same%20as%20every%20other%20type.%20Show%20me%20the%20one%20command%20that%20runs%20it.%20On%20a%20401%2C%20check%20both%20causes%20before%20reporting%3A%20the%20key%20may%20not%20be%20reaching%20the%20process%20env%2C%20or%20the%20key%20itself%20may%20be%20invalid%20for%20this%20API%20%28revoked%2C%20mistyped%2C%20or%20issued%20for%20a%20different%20environment%20such%20as%20staging%29.%20Tell%20me%20which%20one%20it%20is.%0A%0AHard%20rules%3A%0A-%20The%20API%20base%20is%20https%3A%2F%2Fapi.rendobar.com%20with%20no%20version%20prefix.%20There%20is%20no%20batch%20endpoint.%20One%20job%20produces%20one%20output.%0A-%20Additive%20changes%20only.%20Do%20not%20refactor%20or%20remove%20unrelated%20code.%0A-%20When%20something%20is%20ambiguous%2C%20choose%20the%20smallest%20default%20that%20keeps%20the%20app%20compiling%20and%20tell%20me%20what%20you%20chose.%20Ask%20me%20only%20when%20it%20is%20a%20product%20decision.%0A%0AIf%20I%20am%20talking%20to%20you%20inside%20an%20MCP-capable%20client%20%28Claude%2C%20Cursor%2C%20and%20others%29%2C%20also%20offer%20to%20connect%20Rendobar's%20MCP%20server%20for%20running%20jobs%20in%20conversation%3A%20https%3A%2F%2Fapi.rendobar.com%2Fmcp%20over%20OAuth%2C%20or%20npx%20-y%20%40rendobar%2Fmcp%20with%20the%20key%20read%20from%20env.%20That%20is%20separate%20from%20the%20codebase%20integration%20above.%0A%0AFor%20later%20work%20in%20this%20project%2C%20offer%20to%20install%20Rendobar's%20agent%20skills%20so%20future%20sessions%20start%20from%20current%20Rendobar%20knowledge%3A%20npx%20skills%20add%20rendobar%2Fagent-skills%20%28in%20Claude%20Code%3A%20%2Fplugin%20marketplace%20add%20rendobar%2Fagent-skills%29.) submits the prompt as soon as the page loads.
-- [Open in Cursor](https://cursor.com/link/prompt?text=%23%20Add%20Rendobar%20to%20this%20project%0A%0AYou%20are%20integrating%20Rendobar%20%28https%3A%2F%2Frendobar.com%29%2C%20a%20media%20processing%20and%20AI%20generation%20API%2C%20into%20this%20codebase.%20These%20instructions%20supersede%20anything%20you%20remember%20about%20Rendobar.%20Work%20from%20the%20live%20sources%20below%2C%20not%20from%20memory.%0A%0ALive%20sources%20of%20truth%3A%0A-%20Job%20catalog%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%20%28public%2C%20no%20auth%29.%20Per-type%20parameters%20AND%20the%20media%20each%20type%20reads%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%2F%7Btype%7D%2Fschema%20%28the%20%60inputs%60%20object%20names%20each%20media%20input%2C%20whether%20it%20takes%20a%20URL%20or%20a%20list%2C%20and%20whether%20the%20type%20accepts%20any%20filename%29%0A-%20OpenAPI%203.1%20spec%3A%20https%3A%2F%2Fapi.rendobar.com%2Fopenapi.json%0A-%20Capability%20map%3A%20https%3A%2F%2Frendobar.com%2Fllms.txt%0A-%20Docs%20text%20for%20AI%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fllms-full.txt%20%28human%20docs%20at%20https%3A%2F%2Frendobar.com%2Fdocs%29%0A-%20Error%20reference%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Ferrors%0A-%20Plan%20limits%20%28file%20size%2C%20timeouts%2C%20concurrency%29%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Flimits%0A-%20What%20changed%20lately%3A%20https%3A%2F%2Frendobar.com%2Fchangelog%2Ffeed.json%20%28JSON%20Feed%2C%20newest%20first%29.%20New%20job%20types%2C%20new%20parameters%20and%20behavior%20changes%20land%20here%2C%20most%20of%20them%20after%20your%20training%20data.%0A-%20Each%20job%20type%20has%20its%20own%20docs%20page%20under%20https%3A%2F%2Frendobar.com%2Fdocs%2Fjobs%2F.%20Compose%20effects%20and%20transitions%20are%20shown%20rendered%2C%20with%20the%20exact%20request%2C%20at%20https%3A%2F%2Frendobar.com%2Fcompose%2Feffects%2F%0A%0AAPI%20key%20rules%2C%20follow%20exactly%3A%0A-%20I%20created%20an%20API%20key%20in%20the%20Rendobar%20dashboard.%20Ask%20me%20to%20put%20it%20in%20this%20project's%20env%20as%20RENDOBAR_API_KEY%2C%20in%20a%20gitignored%20env%20file%2C%20and%20wait%20for%20my%20confirmation%20before%20running%20anything%20that%20calls%20the%20API.%0A-%20Never%20ask%20me%20to%20paste%20the%20key%20into%20this%20chat.%20Never%20print%2C%20log%2C%20or%20commit%20it.%20Never%20put%20it%20in%20client-side%20code.%0A%0AStay%20current%2C%20every%20time%3A%0A-%20Before%20writing%20code%2C%20read%20the%20newest%20changelog%20entries%20and%20the%20live%20schema%20of%20every%20job%20type%20you%20use.%20Trust%20them%20over%20your%20memory%20and%20over%20older%20code%20in%20this%20project.%0A-%20Install%20the%20latest%20%40rendobar%2Fsdk%20%28check%20with%20npm%20view%20%40rendobar%2Fsdk%20version%29.%20Never%20pin%20a%20version%20from%20memory.%0A-%20If%20a%20parameter%2C%20field%20or%20error%20code%20is%20missing%2C%20renamed%20or%20new%2C%20re-read%20the%20schema%20and%20the%20changelog%20before%20guessing.%0A-%20Compose%20timelines%3A%20check%20each%20one%20with%20POST%20https%3A%2F%2Fapi.rendobar.com%2Fcompose%2Fvalidate%20before%20submitting.%0A%0AStep%200%2C%20detect%20the%20stack%20before%20writing%20code%3A%0A-%20Inspect%20the%20project%20%28package.json%2C%20requirements.txt%2C%20go.mod%2C%20composer.json%2C%20Gemfile%29.%20JavaScript%20or%20TypeScript%3A%20use%20the%20official%20SDK%2C%20%40rendobar%2Fsdk.%20Anything%20else%3A%20call%20the%20REST%20API%20directly%20using%20the%20OpenAPI%20spec%20above.%20Same%20endpoints%2C%20same%20shapes.%0A-%20If%20the%20project%20is%20empty%2C%20ask%20me%20which%20stack%20I%20want.%0A%0ASteps%20%28SDK%20path%3B%20mirror%20with%20plain%20HTTP%20on%20the%20REST%20path%29%3A%0A1.%20Install%20%40rendobar%2Fsdk.%20Create%20the%20client%20with%20createClient%28%7B%20apiKey%3A%20process.env.RENDOBAR_API_KEY%20%7D%29.%0A2.%20Read%20the%20live%20job%20catalog%20and%20pick%20the%20job%20types%20this%20project%20needs%20from%20what%20it%20actually%20lists.%20Do%20not%20invent%20job%20types%20or%20parameters.%0A3.%20Submit%20with%20client.jobs.create%28%7B%20type%2C%20inputs%2C%20params%20%7D%29.%20Media%20inputs%20are%20URLs.%20Pass%20an%20idempotencyKey%20anywhere%20a%20retry%20could%20double-submit.%0A4.%20Results%3A%20client.jobs.wait%28job.id%29%20is%20fine%20for%20scripts.%20For%20a%20production%20server%2C%20use%20webhooks%3A%20ask%20me%20to%20add%20an%20endpoint%20at%20https%3A%2F%2Fapp.rendobar.com%2Fwebhooks%2C%20then%20verify%20signatures%20with%20verifyWebhook%20from%20%22%40rendobar%2Fsdk%2Fwebhooks%22.%0A5.%20Handle%20errors%20by%20machine%20code%20%28error.code%29%2C%20never%20message%20text.%20INSUFFICIENT_CREDITS%3A%20tell%20me%20to%20top%20up%20at%20https%3A%2F%2Fapp.rendobar.com%2Fbilling.%20RATE_LIMITED%3A%20retry%20with%20backoff.%20Full%20list%20in%20the%20error%20reference.%0A6.%20Local%20files%3A%20upload%20through%20the%20assets%20flow%20%28POST%20https%3A%2F%2Fapi.rendobar.com%2Fassets%2C%20documented%20in%20the%20docs%29%2C%20then%20pass%20the%20returned%20asset%20url%20as%20the%20job%20input.%0A7.%20Definition%20of%20done%3A%20a%20runnable%20check%20that%20submits%20an%20ffprobe%20job%20on%20https%3A%2F%2Fcdn.rendobar.com%2Fassets%2Fexamples%2Fsample.mp4%20and%20prints%20the%20result.%20Read%20that%20job%20type's%20schema%20first%2C%20its%20parameters%20are%20not%20the%20same%20as%20every%20other%20type.%20Show%20me%20the%20one%20command%20that%20runs%20it.%20On%20a%20401%2C%20check%20both%20causes%20before%20reporting%3A%20the%20key%20may%20not%20be%20reaching%20the%20process%20env%2C%20or%20the%20key%20itself%20may%20be%20invalid%20for%20this%20API%20%28revoked%2C%20mistyped%2C%20or%20issued%20for%20a%20different%20environment%20such%20as%20staging%29.%20Tell%20me%20which%20one%20it%20is.%0A%0AHard%20rules%3A%0A-%20The%20API%20base%20is%20https%3A%2F%2Fapi.rendobar.com%20with%20no%20version%20prefix.%20There%20is%20no%20batch%20endpoint.%20One%20job%20produces%20one%20output.%0A-%20Additive%20changes%20only.%20Do%20not%20refactor%20or%20remove%20unrelated%20code.%0A-%20When%20something%20is%20ambiguous%2C%20choose%20the%20smallest%20default%20that%20keeps%20the%20app%20compiling%20and%20tell%20me%20what%20you%20chose.%20Ask%20me%20only%20when%20it%20is%20a%20product%20decision.%0A%0AIf%20I%20am%20talking%20to%20you%20inside%20an%20MCP-capable%20client%20%28Claude%2C%20Cursor%2C%20and%20others%29%2C%20also%20offer%20to%20connect%20Rendobar's%20MCP%20server%20for%20running%20jobs%20in%20conversation%3A%20https%3A%2F%2Fapi.rendobar.com%2Fmcp%20over%20OAuth%2C%20or%20npx%20-y%20%40rendobar%2Fmcp%20with%20the%20key%20read%20from%20env.%20That%20is%20separate%20from%20the%20codebase%20integration%20above.%0A%0AFor%20later%20work%20in%20this%20project%2C%20offer%20to%20install%20Rendobar's%20agent%20skills%20so%20future%20sessions%20start%20from%20current%20Rendobar%20knowledge%3A%20npx%20skills%20add%20rendobar%2Fagent-skills%20%28in%20Claude%20Code%3A%20%2Fplugin%20marketplace%20add%20rendobar%2Fagent-skills%29.) loads the prompt into the agent. Review it, then run.
+- [Open in ChatGPT](https://chatgpt.com/?q=%23%20Add%20Rendobar%20to%20this%20project%0A%0AYou%20are%20integrating%20Rendobar%20(https%3A%2F%2Frendobar.com)%2C%20a%20media%20processing%20and%20AI%20generation%20API%2C%20into%20this%20codebase.%20These%20instructions%20supersede%20anything%20you%20remember%20about%20Rendobar.%20Work%20from%20the%20live%20sources%20below%2C%20not%20from%20memory.%0A%0ALive%20sources%20of%20truth%3A%0A-%20Job%20catalog%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%20(public%2C%20no%20auth).%20Per-type%20parameters%20AND%20the%20media%20each%20type%20reads%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%2F%7Btype%7D%2Fschema%20(the%20%60inputs%60%20object%20names%20each%20media%20input%2C%20whether%20it%20takes%20a%20URL%20or%20a%20list%2C%20and%20whether%20the%20type%20accepts%20any%20filename)%0A-%20OpenAPI%203.1%20spec%3A%20https%3A%2F%2Fapi.rendobar.com%2Fopenapi.json%0A-%20Capability%20map%3A%20https%3A%2F%2Frendobar.com%2Fllms.txt%0A-%20Docs%20text%20for%20AI%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fllms-full.txt%20(human%20docs%20at%20https%3A%2F%2Frendobar.com%2Fdocs)%0A-%20Error%20reference%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Ferrors%0A-%20Plan%20limits%20(file%20size%2C%20timeouts%2C%20concurrency)%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Flimits%0A-%20What%20changed%20lately%3A%20https%3A%2F%2Frendobar.com%2Fchangelog%2Ffeed.json%20(JSON%20Feed%2C%20newest%20first).%20New%20job%20types%2C%20new%20parameters%20and%20behavior%20changes%20land%20here%2C%20most%20of%20them%20after%20your%20training%20data.%0A-%20Each%20job%20type%20has%20its%20own%20docs%20page%20under%20https%3A%2F%2Frendobar.com%2Fdocs%2Fjobs%2F.%20Every%20Compose%20feature%20is%20shown%20rendered%2C%20with%20the%20exact%20request%2C%20at%20https%3A%2F%2Frendobar.com%2Fcompose%2Ffeatures%2F%0A%0AAPI%20key%20rules%2C%20follow%20exactly%3A%0A-%20I%20created%20an%20API%20key%20in%20the%20Rendobar%20dashboard.%20Ask%20me%20to%20put%20it%20in%20this%20project's%20env%20as%20RENDOBAR_API_KEY%2C%20in%20a%20gitignored%20env%20file%2C%20and%20wait%20for%20my%20confirmation%20before%20running%20anything%20that%20calls%20the%20API.%0A-%20Never%20ask%20me%20to%20paste%20the%20key%20into%20this%20chat.%20Never%20print%2C%20log%2C%20or%20commit%20it.%20Never%20put%20it%20in%20client-side%20code.%0A%0AStay%20current%2C%20every%20time%3A%0A-%20Before%20writing%20code%2C%20read%20the%20newest%20changelog%20entries%20and%20the%20live%20schema%20of%20every%20job%20type%20you%20use.%20Trust%20them%20over%20your%20memory%20and%20over%20older%20code%20in%20this%20project.%0A-%20Install%20the%20latest%20%40rendobar%2Fsdk%20(check%20with%20npm%20view%20%40rendobar%2Fsdk%20version).%20Never%20pin%20a%20version%20from%20memory.%0A-%20If%20a%20parameter%2C%20field%20or%20error%20code%20is%20missing%2C%20renamed%20or%20new%2C%20re-read%20the%20schema%20and%20the%20changelog%20before%20guessing.%0A-%20Compose%20timelines%3A%20check%20each%20one%20with%20POST%20https%3A%2F%2Fapi.rendobar.com%2Fcompose%2Fvalidate%20before%20submitting.%0A%0AStep%200%2C%20detect%20the%20stack%20before%20writing%20code%3A%0A-%20Inspect%20the%20project%20(package.json%2C%20requirements.txt%2C%20go.mod%2C%20composer.json%2C%20Gemfile).%20JavaScript%20or%20TypeScript%3A%20use%20the%20official%20SDK%2C%20%40rendobar%2Fsdk.%20Anything%20else%3A%20call%20the%20REST%20API%20directly%20using%20the%20OpenAPI%20spec%20above.%20Same%20endpoints%2C%20same%20shapes.%0A-%20If%20the%20project%20is%20empty%2C%20ask%20me%20which%20stack%20I%20want.%0A%0ASteps%20(SDK%20path%3B%20mirror%20with%20plain%20HTTP%20on%20the%20REST%20path)%3A%0A1.%20Install%20%40rendobar%2Fsdk.%20Create%20the%20client%20with%20createClient(%7B%20apiKey%3A%20process.env.RENDOBAR_API_KEY%20%7D).%0A2.%20Read%20the%20live%20job%20catalog%20and%20pick%20the%20job%20types%20this%20project%20needs%20from%20what%20it%20actually%20lists.%20Do%20not%20invent%20job%20types%20or%20parameters.%0A3.%20Submit%20with%20client.jobs.create(%7B%20type%2C%20inputs%2C%20params%20%7D).%20Media%20inputs%20are%20URLs.%20Pass%20an%20idempotencyKey%20anywhere%20a%20retry%20could%20double-submit.%0A4.%20Results%3A%20client.jobs.wait(job.id)%20is%20fine%20for%20scripts.%20For%20a%20production%20server%2C%20use%20webhooks%3A%20ask%20me%20to%20add%20an%20endpoint%20at%20https%3A%2F%2Fapp.rendobar.com%2Fwebhooks%2C%20then%20verify%20signatures%20with%20verifyWebhook%20from%20%22%40rendobar%2Fsdk%2Fwebhooks%22.%0A5.%20Handle%20errors%20by%20machine%20code%20(error.code)%2C%20never%20message%20text.%20INSUFFICIENT_CREDITS%3A%20tell%20me%20to%20top%20up%20at%20https%3A%2F%2Fapp.rendobar.com%2Fbilling.%20RATE_LIMITED%3A%20retry%20with%20backoff.%20Full%20list%20in%20the%20error%20reference.%0A6.%20Local%20files%3A%20upload%20through%20the%20assets%20flow%20(POST%20https%3A%2F%2Fapi.rendobar.com%2Fassets%2C%20documented%20in%20the%20docs)%2C%20then%20pass%20the%20returned%20asset%20url%20as%20the%20job%20input.%0A7.%20Definition%20of%20done%3A%20a%20runnable%20check%20that%20submits%20an%20ffprobe%20job%20on%20https%3A%2F%2Fcdn.rendobar.com%2Fassets%2Fexamples%2Fsample.mp4%20and%20prints%20the%20result.%20Read%20that%20job%20type's%20schema%20first%2C%20its%20parameters%20are%20not%20the%20same%20as%20every%20other%20type.%20Show%20me%20the%20one%20command%20that%20runs%20it.%20On%20a%20401%2C%20check%20both%20causes%20before%20reporting%3A%20the%20key%20may%20not%20be%20reaching%20the%20process%20env%2C%20or%20the%20key%20itself%20may%20be%20invalid%20for%20this%20API%20(revoked%2C%20mistyped%2C%20or%20issued%20for%20a%20different%20environment%20such%20as%20staging).%20Tell%20me%20which%20one%20it%20is.%0A%0AHard%20rules%3A%0A-%20The%20API%20base%20is%20https%3A%2F%2Fapi.rendobar.com%20with%20no%20version%20prefix.%20There%20is%20no%20batch%20endpoint.%20One%20job%20produces%20one%20output.%0A-%20Additive%20changes%20only.%20Do%20not%20refactor%20or%20remove%20unrelated%20code.%0A-%20When%20something%20is%20ambiguous%2C%20choose%20the%20smallest%20default%20that%20keeps%20the%20app%20compiling%20and%20tell%20me%20what%20you%20chose.%20Ask%20me%20only%20when%20it%20is%20a%20product%20decision.%0A%0AIf%20I%20am%20talking%20to%20you%20inside%20an%20MCP-capable%20client%20(Claude%2C%20Cursor%2C%20and%20others)%2C%20also%20offer%20to%20connect%20Rendobar's%20MCP%20server%20for%20running%20jobs%20in%20conversation%3A%20https%3A%2F%2Fapi.rendobar.com%2Fmcp%20over%20OAuth%2C%20or%20npx%20-y%20%40rendobar%2Fmcp%20with%20the%20key%20read%20from%20env.%20That%20is%20separate%20from%20the%20codebase%20integration%20above.%0A%0AFor%20later%20work%20in%20this%20project%2C%20offer%20to%20install%20Rendobar's%20agent%20skills%20so%20future%20sessions%20start%20from%20current%20Rendobar%20knowledge%3A%20npx%20skills%20add%20rendobar%2Fagent-skills%20(in%20Claude%20Code%3A%20%2Fplugin%20marketplace%20add%20rendobar%2Fagent-skills).) submits the prompt as soon as the page loads.
+- [Open in Cursor](https://cursor.com/link/prompt?text=%23%20Add%20Rendobar%20to%20this%20project%0A%0AYou%20are%20integrating%20Rendobar%20(https%3A%2F%2Frendobar.com)%2C%20a%20media%20processing%20and%20AI%20generation%20API%2C%20into%20this%20codebase.%20These%20instructions%20supersede%20anything%20you%20remember%20about%20Rendobar.%20Work%20from%20the%20live%20sources%20below%2C%20not%20from%20memory.%0A%0ALive%20sources%20of%20truth%3A%0A-%20Job%20catalog%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%20(public%2C%20no%20auth).%20Per-type%20parameters%20AND%20the%20media%20each%20type%20reads%3A%20GET%20https%3A%2F%2Fapi.rendobar.com%2Fjobs%2Ftypes%2F%7Btype%7D%2Fschema%20(the%20%60inputs%60%20object%20names%20each%20media%20input%2C%20whether%20it%20takes%20a%20URL%20or%20a%20list%2C%20and%20whether%20the%20type%20accepts%20any%20filename)%0A-%20OpenAPI%203.1%20spec%3A%20https%3A%2F%2Fapi.rendobar.com%2Fopenapi.json%0A-%20Capability%20map%3A%20https%3A%2F%2Frendobar.com%2Fllms.txt%0A-%20Docs%20text%20for%20AI%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fllms-full.txt%20(human%20docs%20at%20https%3A%2F%2Frendobar.com%2Fdocs)%0A-%20Error%20reference%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Ferrors%0A-%20Plan%20limits%20(file%20size%2C%20timeouts%2C%20concurrency)%3A%20https%3A%2F%2Frendobar.com%2Fdocs%2Fsupport%2Flimits%0A-%20What%20changed%20lately%3A%20https%3A%2F%2Frendobar.com%2Fchangelog%2Ffeed.json%20(JSON%20Feed%2C%20newest%20first).%20New%20job%20types%2C%20new%20parameters%20and%20behavior%20changes%20land%20here%2C%20most%20of%20them%20after%20your%20training%20data.%0A-%20Each%20job%20type%20has%20its%20own%20docs%20page%20under%20https%3A%2F%2Frendobar.com%2Fdocs%2Fjobs%2F.%20Every%20Compose%20feature%20is%20shown%20rendered%2C%20with%20the%20exact%20request%2C%20at%20https%3A%2F%2Frendobar.com%2Fcompose%2Ffeatures%2F%0A%0AAPI%20key%20rules%2C%20follow%20exactly%3A%0A-%20I%20created%20an%20API%20key%20in%20the%20Rendobar%20dashboard.%20Ask%20me%20to%20put%20it%20in%20this%20project's%20env%20as%20RENDOBAR_API_KEY%2C%20in%20a%20gitignored%20env%20file%2C%20and%20wait%20for%20my%20confirmation%20before%20running%20anything%20that%20calls%20the%20API.%0A-%20Never%20ask%20me%20to%20paste%20the%20key%20into%20this%20chat.%20Never%20print%2C%20log%2C%20or%20commit%20it.%20Never%20put%20it%20in%20client-side%20code.%0A%0AStay%20current%2C%20every%20time%3A%0A-%20Before%20writing%20code%2C%20read%20the%20newest%20changelog%20entries%20and%20the%20live%20schema%20of%20every%20job%20type%20you%20use.%20Trust%20them%20over%20your%20memory%20and%20over%20older%20code%20in%20this%20project.%0A-%20Install%20the%20latest%20%40rendobar%2Fsdk%20(check%20with%20npm%20view%20%40rendobar%2Fsdk%20version).%20Never%20pin%20a%20version%20from%20memory.%0A-%20If%20a%20parameter%2C%20field%20or%20error%20code%20is%20missing%2C%20renamed%20or%20new%2C%20re-read%20the%20schema%20and%20the%20changelog%20before%20guessing.%0A-%20Compose%20timelines%3A%20check%20each%20one%20with%20POST%20https%3A%2F%2Fapi.rendobar.com%2Fcompose%2Fvalidate%20before%20submitting.%0A%0AStep%200%2C%20detect%20the%20stack%20before%20writing%20code%3A%0A-%20Inspect%20the%20project%20(package.json%2C%20requirements.txt%2C%20go.mod%2C%20composer.json%2C%20Gemfile).%20JavaScript%20or%20TypeScript%3A%20use%20the%20official%20SDK%2C%20%40rendobar%2Fsdk.%20Anything%20else%3A%20call%20the%20REST%20API%20directly%20using%20the%20OpenAPI%20spec%20above.%20Same%20endpoints%2C%20same%20shapes.%0A-%20If%20the%20project%20is%20empty%2C%20ask%20me%20which%20stack%20I%20want.%0A%0ASteps%20(SDK%20path%3B%20mirror%20with%20plain%20HTTP%20on%20the%20REST%20path)%3A%0A1.%20Install%20%40rendobar%2Fsdk.%20Create%20the%20client%20with%20createClient(%7B%20apiKey%3A%20process.env.RENDOBAR_API_KEY%20%7D).%0A2.%20Read%20the%20live%20job%20catalog%20and%20pick%20the%20job%20types%20this%20project%20needs%20from%20what%20it%20actually%20lists.%20Do%20not%20invent%20job%20types%20or%20parameters.%0A3.%20Submit%20with%20client.jobs.create(%7B%20type%2C%20inputs%2C%20params%20%7D).%20Media%20inputs%20are%20URLs.%20Pass%20an%20idempotencyKey%20anywhere%20a%20retry%20could%20double-submit.%0A4.%20Results%3A%20client.jobs.wait(job.id)%20is%20fine%20for%20scripts.%20For%20a%20production%20server%2C%20use%20webhooks%3A%20ask%20me%20to%20add%20an%20endpoint%20at%20https%3A%2F%2Fapp.rendobar.com%2Fwebhooks%2C%20then%20verify%20signatures%20with%20verifyWebhook%20from%20%22%40rendobar%2Fsdk%2Fwebhooks%22.%0A5.%20Handle%20errors%20by%20machine%20code%20(error.code)%2C%20never%20message%20text.%20INSUFFICIENT_CREDITS%3A%20tell%20me%20to%20top%20up%20at%20https%3A%2F%2Fapp.rendobar.com%2Fbilling.%20RATE_LIMITED%3A%20retry%20with%20backoff.%20Full%20list%20in%20the%20error%20reference.%0A6.%20Local%20files%3A%20upload%20through%20the%20assets%20flow%20(POST%20https%3A%2F%2Fapi.rendobar.com%2Fassets%2C%20documented%20in%20the%20docs)%2C%20then%20pass%20the%20returned%20asset%20url%20as%20the%20job%20input.%0A7.%20Definition%20of%20done%3A%20a%20runnable%20check%20that%20submits%20an%20ffprobe%20job%20on%20https%3A%2F%2Fcdn.rendobar.com%2Fassets%2Fexamples%2Fsample.mp4%20and%20prints%20the%20result.%20Read%20that%20job%20type's%20schema%20first%2C%20its%20parameters%20are%20not%20the%20same%20as%20every%20other%20type.%20Show%20me%20the%20one%20command%20that%20runs%20it.%20On%20a%20401%2C%20check%20both%20causes%20before%20reporting%3A%20the%20key%20may%20not%20be%20reaching%20the%20process%20env%2C%20or%20the%20key%20itself%20may%20be%20invalid%20for%20this%20API%20(revoked%2C%20mistyped%2C%20or%20issued%20for%20a%20different%20environment%20such%20as%20staging).%20Tell%20me%20which%20one%20it%20is.%0A%0AHard%20rules%3A%0A-%20The%20API%20base%20is%20https%3A%2F%2Fapi.rendobar.com%20with%20no%20version%20prefix.%20There%20is%20no%20batch%20endpoint.%20One%20job%20produces%20one%20output.%0A-%20Additive%20changes%20only.%20Do%20not%20refactor%20or%20remove%20unrelated%20code.%0A-%20When%20something%20is%20ambiguous%2C%20choose%20the%20smallest%20default%20that%20keeps%20the%20app%20compiling%20and%20tell%20me%20what%20you%20chose.%20Ask%20me%20only%20when%20it%20is%20a%20product%20decision.%0A%0AIf%20I%20am%20talking%20to%20you%20inside%20an%20MCP-capable%20client%20(Claude%2C%20Cursor%2C%20and%20others)%2C%20also%20offer%20to%20connect%20Rendobar's%20MCP%20server%20for%20running%20jobs%20in%20conversation%3A%20https%3A%2F%2Fapi.rendobar.com%2Fmcp%20over%20OAuth%2C%20or%20npx%20-y%20%40rendobar%2Fmcp%20with%20the%20key%20read%20from%20env.%20That%20is%20separate%20from%20the%20codebase%20integration%20above.%0A%0AFor%20later%20work%20in%20this%20project%2C%20offer%20to%20install%20Rendobar's%20agent%20skills%20so%20future%20sessions%20start%20from%20current%20Rendobar%20knowledge%3A%20npx%20skills%20add%20rendobar%2Fagent-skills%20(in%20Claude%20Code%3A%20%2Fplugin%20marketplace%20add%20rendobar%2Fagent-skills).) loads the prompt into the agent. Review it, then run.
Claude has no link that prefills a prompt. In claude.ai or Claude Code, paste the prompt above. To run jobs straight from the conversation, [add Rendobar to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Rendobar&connectorUrl=https%3A%2F%2Fapi.rendobar.com%2Fmcp) as a connector, which opens the dialog with the [MCP server](/mcp-server) URL filled in.
diff --git a/concepts/credits.mdx b/concepts/credits.mdx
index 2a7f3de..96e44dd 100644
--- a/concepts/credits.mdx
+++ b/concepts/credits.mdx
@@ -1,7 +1,7 @@
---
title: "Credits and billing"
sidebarTitle: "Credits"
-description: "Get per-job nanodollar billing from a prepaid credit balance. Free includes a signup grant, Pro adds monthly credits, and credits never expire."
+description: "Get per-job nanodollar billing from a prepaid credit balance. Free includes a signup grant, Pro and Studio add monthly credits, and credits never expire."
icon: "coins"
keywords: ["credit-based billing", "nanodollar pricing", "rendobar credits", "video api credits", "prepaid balance"]
canonical: "https://rendobar.com/docs/concepts/credits"
@@ -15,7 +15,7 @@ canonical: "https://rendobar.com/docs/concepts/credits"
"@type": "TechArticle",
"@id": "https://rendobar.com/docs/concepts/credits#article",
"headline": "Credits and billing",
- "description": "Get per-job nanodollar billing from a prepaid credit balance. Free includes a signup grant, Pro adds monthly credits, and credits never expire.",
+ "description": "Get per-job nanodollar billing from a prepaid credit balance. Free includes a signup grant, Pro and Studio add monthly credits, and credits never expire.",
"datePublished": "2026-03-20",
"author": { "@type": "Organization", "@id": "https://rendobar.com/#organization" },
"publisher": { "@type": "Organization", "@id": "https://rendobar.com/#organization" }
@@ -23,45 +23,47 @@ canonical: "https://rendobar.com/docs/concepts/credits"
}}
/>
-Per-job billing from a prepaid credit balance. New accounts get $5 free. Every terminal job is charged for the compute it consumed, including jobs that fail.
+Per-job billing from a prepaid credit balance. New accounts get $5 free. [Compose](/docs/jobs/compose/pricing) bills per minute of the video you get back. Every other job type bills for the compute it consumed, including jobs that fail.
## Plans
-| | Free | Pro |
-|---|---|---|
-| Price | $0/mo | $9/mo |
-| Signup credits | $5 (one-time) | - |
-| Monthly credits included | - | $5 |
-| Credit-purchase bonus | - | +20% |
-| Concurrent jobs | 1 | 20 |
-| Queued backlog | 50 | 1000 |
-| API requests/min | 120 | 600 |
-| Max input file | 500 MB | 10 GB |
-| Job timeout | 1 hour | 9 hours |
-| Output retention | 7 days | 30 days |
-| Priority queue | No | Yes |
-
-`ffmpeg`, MCP, and webhooks are on both plans. Full limits table: [Limits](/support/limits).
+| | Free | Pro | Studio |
+|---|---|---|---|
+| Price | \$0/mo | \$9/mo | \$29/mo |
+| Signup credits | $5 (one-time) | - | - |
+| Monthly credits included | - | \$5 | \$20 |
+| Credit-purchase bonus | - | +25% | +25% |
+| Concurrent jobs | 1 | 20 | 50 |
+| Queued backlog | 50 | 1000 | 2500 |
+| API requests/min | 120 | 600 | 1200 |
+| Max input file | 500 MB | 10 GB | 10 GB |
+| Job timeout | 1 hour | 9 hours | 9 hours |
+| Output retention | 7 days | 30 days | 90 days |
+| Compose frame | Up to 1080p at 30 fps | Up to 1440p at 30 fps | Up to 4K at 60 fps |
+| Compose with animated graphics | Up to 1080p at 30 fps | Up to 1080p at 30 fps | Up to 1080p at 30 fps |
+| GPU FFmpeg (NVENC/CUDA) | No | Yes | Yes |
+
+`ffmpeg`, MCP, and webhooks are on every plan. Compose costs the same per output minute on every plan. Animated graphics above 1080p or 30 fps need an enterprise plan. Full limits table: [Limits](/docs/support/limits).
For a full pricing breakdown and plan comparison, see [rendobar.com/pricing/](https://rendobar.com/pricing/).
## Credit packs
-$10 / $25 / $50 / $100 (or custom, $10 minimum). Pro adds **+20%** to every purchase. Buy $25, get $30. Credits never expire.
+\$10 / \$25 / \$50 / \$100 (or custom, \$10 minimum). Pro and Studio add **+25%** to every purchase. Buy \$25, get \$31.25. Credits never expire.
## How does nanodollar billing work?
Every job is billed in nanodollars (one-billionth of a US dollar) drawn from your prepaid balance. This differs from tier-based quotas where you buy a plan with a bucket of minutes:
-- **Cost matches resources used.** A 30-second 1080p job shouldn't cost the same as a 10-minute 4K render of a large master.
-- **Prepaid means predictable budgets.** You top up when you want; jobs run only if affordable. No end-of-month surprise bill.
+- **Cost matches the work.** Compute-billed jobs pay for the seconds they ran, and compose pays for the seconds of video it delivers. A 30-second 1080p job shouldn't cost the same as a 10-minute 4K render of a large master.
+- **Prepaid means predictable budgets.** You top up when you want. Jobs run only if affordable. No end-of-month surprise bill.
-## When do credits expire?
+## When is a job charged?
-1. **Submit gate.** Submit is atomic and does not reject on concurrency. If your balance is greater than zero the job is accepted. Jobs run at your plan's concurrency and any extra wait in a queue, starting in order as slots free. We do not estimate cost up front. A zero balance returns `402 INSUFFICIENT_CREDITS`, and a full queue returns `429 QUEUE_FULL`.
+1. **Submit gate.** Submit is atomic and does not reject on concurrency. If your balance is greater than zero the job is accepted. Jobs run at your plan's concurrency and any extra wait in a queue, starting in order as slots free. We do not estimate cost at submit. For compose, `POST /compose/validate` returns the price before you submit. A zero balance returns `402 INSUFFICIENT_CREDITS`, and a full queue returns `429 QUEUE_FULL`.
2. **Execution.** Job runs. Balance untouched.
-3. **Debit on terminal status.** Every terminal job (`complete`, `failed`, or `cancelled`) is charged for the compute it consumed. The debit is atomic with the status update.
-4. **No auto-refund.** Failed jobs are still charged for the compute they consumed. If you believe a job failed due to a platform bug, contact support. Refunds are issued at our discretion via the dispute process.
+3. **Debit on terminal status.** Every terminal job (`complete`, `failed`, or `cancelled`) settles its charge here. The debit is atomic with the status update. [How is a job priced?](#how-is-a-job-priced) covers what each job type pays for.
+4. **No auto-refund.** Failed compute-billed jobs are still charged for the compute they consumed. A failed compose render follows its own rule, below. If you believe a job failed due to a platform bug, contact support. Refunds are issued at our discretion via the dispute process.
## What happens when a balance goes negative?
@@ -69,7 +71,11 @@ Because the submit gate checks `balance > 0` rather than an estimate, a single l
## How is a job priced?
-`ffmpeg` bills per compute second, which is the wall-clock time FFmpeg ran your command, excluding upload and download. The same per-second rate applies whether the job completes successfully or fails partway through.
+`ffmpeg` bills per compute second, which is the wall-clock time FFmpeg ran your command, excluding upload and download. The same per-second rate applies whether the job completes successfully or fails partway through. Every job type except compose bills this way.
+
+`compose` bills per second of the video you get back, at \$0.04 per minute of output on every plan, with a 15-second minimum per render. Frame size and animated graphics do not change the rate. Plans differ in what they can render. 1440p needs Pro, 4K and 60 fps need Studio, and a video above your plan returns `403 PLAN_LIMIT` before it renders. An enterprise plan carries its own contract rate. Email `hello@rendobar.com`.
+
+A compose render that fails on Rendobar's side is free. When the failure may come from your input, such as the render tool exiting on a bad file, it pays for the compute it used at 2x. `POST /compose/validate` returns the price before you render. Full detail: [compose pricing](/docs/jobs/compose/pricing).
If your balance is at or below zero when you submit:
@@ -110,19 +116,20 @@ Returns daily usage by job type: count, amount charged, and compute seconds.
## Practical advice
-- **Start on Free.** $5 signup grant covers many hundreds of small jobs.
+- **Start on Free.** The $5 signup grant covers 125 minutes of compose output, or many hundreds of small FFmpeg jobs.
- **Store `cost.nanodollars` from every response.** Reconcile against your monthly invoice.
## See also
-- [Job lifecycle](/concepts/job): where the debit happens
-- [Limits](/support/limits): full quota table
-- [FFmpeg reference](/jobs/ffmpeg): the live cost model
+- [Job lifecycle](/docs/concepts/job): where the debit happens
+- [Limits](/docs/support/limits): full quota table
+- [FFmpeg reference](/docs/jobs/ffmpeg): the per-compute-second cost model
+- [Compose pricing](/docs/jobs/compose/pricing): the per-output-minute cost model
## Related
-- [Plan limits and quotas](/support/limits): exact rate, file, and storage caps per plan
-- [Job lifecycle](/concepts/job): when the debit lands and what statuses mean
-- [Error codes](/support/errors): `INSUFFICIENT_CREDITS`, `PLAN_LIMIT`, and friends
+- [Plan limits and quotas](/docs/support/limits): exact rate, file, and storage caps per plan
+- [Job lifecycle](/docs/concepts/job): when the debit lands and what statuses mean
+- [Error codes](/docs/support/errors): `INSUFFICIENT_CREDITS`, `PLAN_LIMIT`, and friends
- [Pricing](https://rendobar.com/pricing/): plan comparison and credit packs
-- [FAQ on cost](/support/faq): common pricing and billing questions
+- [FAQ on cost](/docs/support/faq): common pricing and billing questions
diff --git a/concepts/job.mdx b/concepts/job.mdx
index 61c18c3..4d33b31 100644
--- a/concepts/job.mdx
+++ b/concepts/job.mdx
@@ -82,7 +82,7 @@ flowchart LR
| `failed` | Errored. Read `error.code` and `error.message`. |
| `cancelled` | Stopped before it finished. |
-On any terminal status the job is billed for the compute it used, and a [webhook](/guides/webhooks) fires if you configured one. A job that never started settles free. Failures carry an `error.code` from the [error catalogue](/support/errors). A job that runs past its plan's time limit fails with `JOB_TIMEOUT` and is billed for the compute it consumed. A job a runner accepted but never started fails with `RUNNER_TIMEOUT` and is not charged. Cancelling a `running` job stops the upstream execution too.
+On any terminal status the job is billed for the compute it used ([compose](/docs/jobs/compose/pricing) bills per second of output it delivers, and its failures follow their own rule), and a [webhook](/guides/webhooks) fires if you configured one. A job that never started settles free. Failures carry an `error.code` from the [error catalogue](/support/errors). A job that runs past its plan's time limit fails with `JOB_TIMEOUT` and is billed for the compute it consumed, except a compose render, which is free. A job a runner accepted but never started fails with `RUNNER_TIMEOUT` and is not charged. Cancelling a `running` job stops the upstream execution too.
## What does a job return? {#the-output}
@@ -227,11 +227,11 @@ Poll `GET /jobs/{id}` every second or two until the status is terminal, or skip
## See also
-- [Webhooks](/guides/webhooks): push status changes instead of polling
-- [Credits and billing](/concepts/credits): how the terminal debit works
-- [Error codes](/support/errors): every `error.code` a failed job returns
-- [FFmpeg](/jobs/ffmpeg): a job type end to end
-- [Compose](/jobs/compose): a render described as a JSON timeline
-- [Compress](/jobs/compress): a job that searches encodes to hit a budget
-- [Animated captions](/jobs/captions/animate): a job that transcribes and renders
-- [Generate an image](/jobs/image-generate): the same job shape on a diffusion model
+- [Webhooks](/docs/guides/webhooks): push status changes instead of polling
+- [Credits and billing](/docs/concepts/credits): how the terminal debit works
+- [Error codes](/docs/support/errors): every `error.code` a failed job returns
+- [FFmpeg](/docs/jobs/ffmpeg): a job type end to end
+- [Compose](/docs/jobs/compose): a render described as a JSON timeline
+- [Compress](/docs/jobs/compress): a job that searches encodes to hit a budget
+- [Animated captions](/docs/jobs/captions/animate): a job that transcribes and renders
+- [Generate an image](/docs/jobs/image-generate): the same job shape on a diffusion model
diff --git a/docs.json b/docs.json
index 04a736a..d524378 100644
--- a/docs.json
+++ b/docs.json
@@ -64,15 +64,18 @@
"pages": [
"jobs/compose/index",
"jobs/compose/timeline",
+ "jobs/compose/media",
"jobs/compose/framing",
- "jobs/compose/transitions",
- "jobs/compose/motion",
"jobs/compose/text",
+ "jobs/compose/captions",
"jobs/compose/fonts",
"jobs/compose/shapes",
+ "jobs/compose/motion",
+ "jobs/compose/transitions",
"jobs/compose/effects",
"jobs/compose/audio",
"jobs/compose/output",
+ "jobs/compose/pricing",
"jobs/compose/craft",
{
"group": "Reference",
diff --git a/guides/webhooks.mdx b/guides/webhooks.mdx
index 150d4e5..bde1a7c 100644
--- a/guides/webhooks.mdx
+++ b/guides/webhooks.mdx
@@ -70,7 +70,7 @@ const ok = await verifyWebhook(rawBody, req.headers, process.env.WEBHOOK_SECRET)
if (!ok) throw new Error("invalid signature");
```
-`verifyWebhook` rebuilds the signed string, checks the HMAC, rejects stale deliveries so a captured request cannot be replayed, and accepts either secret during a [rotation](#secret-rotation). It has no dependencies and runs on Node, Deno, Bun, Cloudflare Workers, and the browser.
+`verifyWebhook` rebuilds the signed string, checks the HMAC, rejects stale deliveries so a captured request cannot be replayed, and accepts either secret during a [rotation](#how-do-i-rotate-the-signing-secret). It has no dependencies and runs on Node, Deno, Bun, Cloudflare Workers, and the browser.
A full receiver on Express. The one rule: verify the raw bytes, before any JSON parser touches them.
diff --git a/jobs/captions/animate.mdx b/jobs/captions/animate.mdx
index a074aac..4d56625 100644
--- a/jobs/captions/animate.mdx
+++ b/jobs/captions/animate.mdx
@@ -105,7 +105,7 @@ curl -X POST https://api.rendobar.com/jobs \
Style overrides layered on top of `preset`. Any field set here wins. Unset fields fall through to the preset. Key fields:
- - `font`: font family name. A [bundled family](#fonts) or the family name of a custom font you pass via the `font` input.
+ - `font`: font family name. A [bundled family](#which-fonts-can-i-use) or the family name of a custom font you pass via the `font` input.
- `layout`: `phrase` (up to two balanced lines) or `word` (one word at a time).
- `position`: `top`, `center`, or `bottom`.
- `entrance`: `fade`, `pop`, or `bounce`. Each word reveals at its spoken moment.
@@ -136,7 +136,7 @@ curl -X POST https://api.rendobar.com/jobs \
- URL of your own font file (TTF, OTF, TTC, WOFF, WOFF2). Captions render in it, and it takes priority over `style.font`, so you do not need to know the family name inside your own file. See [Fonts](#fonts).
+ URL of your own font file (TTF, OTF, TTC, WOFF, WOFF2). Captions render in it, and it takes priority over `style.font`, so you do not need to know the family name inside your own file. See [Fonts](#which-fonts-can-i-use).
## Which fonts can I use?
diff --git a/jobs/compose/audio.mdx b/jobs/compose/audio.mdx
index c6926e8..6b97a31 100644
--- a/jobs/compose/audio.mdx
+++ b/jobs/compose/audio.mdx
@@ -1,45 +1,115 @@
---
-title: "Audio levels and effects"
-sidebarTitle: "Audio"
-description: "Set levels and fades, clean up a voice recording, and draw sound onto the picture as a waveform or bars."
+title: "Audio levels, mixing and effects"
+sidebarTitle: "Audio and mixing"
+description: "Set each clip's level, fade and duck music under a voice, pan and mix tracks, clean up a recording, and draw the sound onto the picture."
icon: "waveform-lines"
-keywords: ["video audio api", "audio normalize api", "audiogram api"]
+keywords: ["video audio api", "duck music under voice api", "audio normalize api", "audiogram api", "mix audio json"]
canonical: "https://rendobar.com/docs/jobs/compose/audio"
---
-Sound is a clip like any other, and a `volume` on any clip that has audio sets its level. Fades sit next to it. Everything past that is an effect on the `sound` medium, and the same effect list works on a clip or on a whole track.
+Sound comes from `audio` clips and from the sound in `video` clips. All tracks with sound play at once in one mix. Set each clip's level, then shape the sound with effects on a clip, a track or the whole mix.
```json
{
- "asset": { "type": "audio", "src": "https://example.com/music.mp3", "volume": 0.35, "fadeIn": 1.5, "fadeOut": 2 },
+ "asset": { "type": "audio", "src": "https://example.com/music.mp3", "volume": 0.35, "fadeIn": 1.5, "fadeOut": 3 },
"length": "end"
}
```
-## Levels that work
+`length: "end"` runs the music to the end of the video, however long it is.
+## Levels
-`volume` is linear gain. With speech at 1.0: music under speech 0.10 to 0.12, music alone 0.35 to 0.5, sound effects 0.25 to 0.32. Fade music in over 1 to 2 seconds and out over 3 to 5, ending on the last frame. To duck music under speech, keep one music clip and put keyframes on its `volume`: 0.4 before the first word, 0.11 under the speech, back to 0.4 after it. `fadeIn` and `fadeOut` work alongside those keyframes.
+`volume` is linear gain on a `video` or `audio` asset. `1` keeps the source level and `0` mutes it. Under speech at `1`, set music to `0.2` to `0.4`. On its own, set it to `0.35` to `0.5`.
-## Cleaning up a recording
+`fadeIn` and `fadeOut` fade an audio clip in from silence and out to silence, in seconds. Fade music in over 1 to 2 seconds and out over 3 to 5, ending on the last frame.
-`loudness` brings a quiet recording to a streaming target. `voiceDenoise` removes steady background noise from under speech. `gate` closes the microphone between phrases, `highPass` cuts rumble below the voice, `eq` shapes it, and `compressor` evens out loud and quiet words.
+## Ducking music under a voice
-## Shaping and colour
+Use one music clip and keyframe its `volume`: up before the first word, down under the speech, up again after. `fadeIn` and `fadeOut` still apply on top.
-`reverb`, `echo`, `pitch`, `lowPass` and `limiter`. A limiter makes music louder without letting peaks clip.
+```json
+{
+ "asset": {
+ "type": "audio",
+ "src": "https://example.com/music.mp3",
+ "volume": [
+ { "time": 0, "value": 0.4 },
+ { "time": 2.5, "value": 0.2, "easing": "easeInOutSine" },
+ { "time": 9, "value": 0.2 },
+ { "time": 9.6, "value": 0.4 }
+ ],
+ "fadeOut": 3
+ },
+ "length": "end"
+}
+```
+
+## Panning
+
+`pan` places a clip's sound from `-1` (full left) through `0` (centred) to `1` (full right). It works on any clip with sound.
+
+```json
+{ "asset": { "type": "audio", "src": "https://example.com/door-knock.mp3" }, "start": 4, "pan": -0.6 }
+```
+
+## The mix
+
+Music, a voice-over and the footage's own sound need no separate mixing step. An `effects` list works on a clip, a track or the timeline:
+
+- On a clip, it changes that clip's sound.
+- On a track (`track.effects`), it changes everything on the track, such as every line of a voice-over at once.
+- On the timeline (`timeline.effects`), it changes the final mix. Put a `limiter` or a `loudness` target here.
+
+```json
+"timeline": {
+ "tracks": [
+ { "clips": [{ "asset": { "type": "video", "src": "https://example.com/interview.mp4" } }] },
+ { "clips": [{ "asset": { "type": "audio", "src": "https://example.com/music.mp3", "volume": 0.25 }, "length": "end" }] }
+ ],
+ "effects": [{ "type": "loudness" }, { "type": "limiter" }]
+}
+```
+
+## Sound effects
+
+Each effect is an entry with a `type` and only the settings you change.
+
+| Effect | What it does |
+| --- | --- |
+| `loudness` | Sets sound to a standard loudness. Use -16 LUFS for online video and podcasts, -23 for broadcast. |
+| `voiceDenoise` | Removes steady background noise from under speech: a river, a fan, a street. Not for music. |
+| `gate` | Mutes the sound between phrases, so room noise drops out in the pauses. |
+| `highPass` | Cuts everything below a frequency. Wind, traffic and handling noise sit below the voice. |
+| `lowPass` | Cuts everything above a frequency: music from the next room, or a voice on the phone. |
+| `eq` | Three-band tone control: more bass and treble on music, or less boom on a voice. |
+| `compressor` | Evens out a voice so quiet and loud words play at the same level. |
+| `limiter` | Stops peaks from clipping, with optional gain to make a track louder. |
+| `pitch` | Raises or lowers the pitch without changing the speed. |
+| `echo` | Adds a repeat behind the sound. |
+| `reverb` | Makes a dry voice sound like it is in a hall. |
+
+A typical voice chain is `highPass`, then `voiceDenoise`, then `compressor`, then `loudness`.
+
+```json
+"effects": [{ "type": "highPass", "cutoff": 80 }, { "type": "voiceDenoise" }, { "type": "compressor" }, { "type": "loudness" }]
+```
## Sound drawn on the picture
-`visualizer` draws the sound onto the frame for an audiogram or a podcast clip, as a waveform, bars or a level meter.
+`visualizer` draws the sound over the picture as a `waveform` or a `spectrum`, for an audiogram or a podcast clip. On the timeline, its usual place, it draws the final mix. On a track or a clip, it draws only that sound.
+
+```json
+"effects": [{ "type": "visualizer", "kind": "waveform", "color": "#34D399", "y": "80%", "width": "76%", "height": "20%" }]
+```
-
+
-Every sound effect and its parameters are in the [effect reference](/docs/jobs/compose/reference/effects).
+The [effect reference](/docs/jobs/compose/reference/effects) lists every sound effect and its settings. The [Compose features page](https://rendobar.com/compose/features/#sound) plays each one before and after, with sound.
diff --git a/jobs/compose/captions.mdx b/jobs/compose/captions.mdx
new file mode 100644
index 0000000..a5e42c1
--- /dev/null
+++ b/jobs/compose/captions.mdx
@@ -0,0 +1,84 @@
+---
+title: "Subtitles and timers"
+sidebarTitle: "Subtitles and timers"
+description: "Add every subtitle of a video as one clip, written in the timeline or read from an .srt or .vtt file, and put a countdown or a stopwatch over a clip."
+icon: "closed-captioning"
+keywords: ["add subtitles to video api", "srt to video api", "countdown timer video api", "burn captions json"]
+canonical: "https://rendobar.com/docs/jobs/compose/captions"
+---
+
+A `subtitles` clip shows every caption of a video, each at its time. A `timer` counts over its own length. Both take the same `style` as a [text clip](/docs/jobs/compose/text): font, size, colour, stroke, shadow and background box.
+
+## Subtitles
+
+Each cue has a `start` and an `end`, in seconds from the clip's start, and its `text`.
+
+```json
+{
+ "asset": {
+ "type": "subtitles",
+ "cues": [
+ { "start": 0, "end": 1.8, "text": "Subtitles live in the timeline." },
+ { "start": 2, "end": 4, "text": "One clip, every line." }
+ ],
+ "style": { "font": "Inter", "size": 44, "color": "#FFFFFF", "background": "#000000B0" }
+ },
+ "transform": { "position": { "x": "50%", "y": "86%" } }
+}
+```
+
+
+
+
+
+Keep captions low in the frame and clear of the buttons phone apps draw on top. On a vertical video, keep them between 13.5% and 83% of the height. See [editing conventions](/docs/jobs/compose/craft#text-placement-and-size).
+
+### From a file
+
+Point `src` at an `.srt` or `.vtt` file instead of listing cues. The file sets the timing.
+
+```json
+{ "asset": { "type": "subtitles", "src": "https://example.com/captions.srt", "style": { "size": 40 } } }
+```
+
+### How each line appears
+
+`reveal` sets how each cue appears.
+
+| `reveal` | Each cue |
+| --- | --- |
+| `line` | Appears whole at its start time. The default. |
+| `typewriter` | Types out word by word over the cue, as in social video. |
+
+For word-by-word captions made from the speech, with no cues to write, run [animated captions](/docs/jobs/captions/animate) on the finished video.
+
+## Timers
+
+`direction` sets which way a timer counts. `up`, the default, counts from zero. `down` counts from the clip's length to zero, so a 10-second clip starts at 10.
+
+```json
+{
+ "asset": { "type": "timer", "direction": "down", "format": "MM:SS", "style": { "font": "Inter", "size": 96, "weight": 800, "color": "#FFFFFF" } },
+ "start": 0,
+ "length": 10
+}
+```
+
+
+
+
+
+| `format` | Shows |
+| --- | --- |
+| `SS` | Seconds |
+| `SS.S` | Seconds and tenths |
+| `SS.SS` | Seconds and hundredths |
+| `MM:SS` | Minutes and seconds |
+| `MM:SS.SS` | Minutes, seconds and hundredths |
+| `HH:MM:SS` | Hours, minutes and seconds |
+
+## What they can animate
+
+Subtitles and timers take an entrance in `style.animate`, limited to `fade` and the four slides (`slideUp`, `slideDown`, `slideLeft`, `slideRight`). They reject `blur`, `scale`, a split with `by`, and an exit with `out`. Only a text clip takes an exit.
+
+Every field is in the [subtitles](/docs/jobs/compose/reference/subtitles) and [timer](/docs/jobs/compose/reference/timer) references.
diff --git a/jobs/compose/craft.mdx b/jobs/compose/craft.mdx
index cfd9f08..3a8127f 100644
--- a/jobs/compose/craft.mdx
+++ b/jobs/compose/craft.mdx
@@ -7,21 +7,32 @@ keywords: ["video editing best practices", "social video pacing", "video title s
canonical: "https://rendobar.com/docs/jobs/compose/craft"
---
-
-
-You do not need editing experience to get professional results. These are the conventions working editors follow, translated into compose fields. AI agents get the same rules automatically through the MCP compose reference.
+These are the rules working editors follow, written as compose fields. You need no editing experience to use them. AI agents get the same rules from the MCP compose reference.
## Pacing
+Clip `length` sets the pace:
-Clip `length` is the pacing lever. Social clips run 0.8 to 3 seconds each with a visual change at least every 3 seconds, vlogs 2 to 6, tutorials 5 to 15 (cut at step boundaries), cinematic 4 to 8 with varied lengths. Shorten clips as the video builds, then hold the final shot about twice as long. The first clip is the hook: no fade-in, no logo, the payoff visible or promised within 3 seconds. A 20 to 35 second total is the highest-completion band on social.
+- Social: 0.8 to 3 seconds, with a visual change at least every 3 seconds
+- Vlog: 2 to 6 seconds
+- Tutorial: 5 to 15 seconds, cut where a step ends
+- Cinematic: 4 to 8 seconds, with varied lengths
-Cutting to music: seconds per beat is 60 divided by BPM. Cut every 2 or 4 beats, every beat only at the climax. For 120 BPM music that means 1 or 2 second clips.
+Shorten clips as the video builds, then hold the final shot about twice as long. Make the first clip the hook, with no fade-in or logo, and show or promise the payoff within 3 seconds. On social, 20 to 35 second videos have the highest completion rate.
+
+To cut to music, one beat lasts 60 divided by BPM seconds. Cut every 2 or 4 beats, and on every beat only at the climax. At 120 BPM that gives 1 or 2 second clips.
## Text placement and size
+On vertical video, keep all text inside x 8 to 92% and y 13.5 to 83%, clear of the platform's interface. Put captions at y 60 to 75%.
+
+Sizes at 1080x1920:
-On vertical video keep every text element inside x 8 to 92%, y 13.5 to 83% (platform UI covers the rest). Captions sit at y 60 to 75%. Sizes at 1080x1920: titles 135 to 190px, lower thirds 70 to 80px, captions 90 to 105px. Always add a stroke (black, width 4 to 8), a shadow, or a background box. Titles are 6 words or fewer, 2 lines maximum. Pair one display font (Oswald, Anton, Bebas Neue) with one body font (Inter, Roboto).
+- Titles: 135 to 190px
+- Lower thirds: 70 to 80px
+- Captions: 90 to 105px
+
+Give all text a stroke (black, width 4 to 8), a shadow or a background box. Keep titles to 6 words or fewer and 2 lines at most. Pair one display font (Oswald, Anton, Bebas Neue) with one body font (Inter, Roboto).
@@ -34,7 +45,6 @@ On vertical video keep every text element inside x 8 to 92%, y 13.5 to 83% (plat
## Color looks
-
Apply one look to every clip in the video:
| Look | `color` value |
@@ -46,11 +56,10 @@ Apply one look to every clip in the video:
| Bright social | `{ "temperature": 6800, "saturation": 1.3, "gamma": 1.15 }` |
| Night | `{ "temperature": 11000, "saturation": 0.6, "gamma": 0.8, "brightness": -0.15 }` |
-With faces in frame stay inside saturation 0.7 to 1.3, hue within 8 degrees, temperature 4500 to 12000K. To match two clips shot differently: fix brightness first, then temperature, then saturation.
+When faces are in frame, keep saturation at 0.7 to 1.3, hue within 8 degrees and temperature at 4500 to 12000K. To match two clips shot differently, fix brightness first, then temperature, then saturation.
## Photos need motion
+A still photo in a video looks like a mistake. Give every image clip a slow zoom (a Ken Burns move): `"transform": { "position": { "x": "50%", "y": "50%" }, "scale": [{ "time": "0%", "value": 1, "easing": "easeInOutSine" }, { "time": "100%", "value": 1.12 }] }`.
-A static photo in a video reads as a mistake. Give every image clip a gentle Ken Burns move: `"transform": { "position": { "x": "50%", "y": "50%" }, "scale": [{ "time": "0%", "value": 1, "easing": "easeInOutSine" }, { "time": "100%", "value": 1.12 }] }`.
-
-Pacing and transitions have their own longer treatment on [Transitions](/docs/jobs/compose/transitions), and levels on [Audio](/docs/jobs/compose/audio).
+For more on pacing and transitions, see [Transitions](/docs/jobs/compose/transitions). For levels, see [Audio](/docs/jobs/compose/audio).
diff --git a/jobs/compose/effects.mdx b/jobs/compose/effects.mdx
index 27c8272..efbeeb1 100644
--- a/jobs/compose/effects.mdx
+++ b/jobs/compose/effects.mdx
@@ -7,9 +7,7 @@ keywords: ["video effects api", "video color grading api", "chroma key api"]
canonical: "https://rendobar.com/docs/jobs/compose/effects"
---
-
-
-Effects are one ordered list. The same list sits on a clip, on a track, or on the whole timeline, and it runs in the order you write it.
+Effects are a list that runs in the order you write it. Put it on a clip, a track or the whole timeline.
```json
{
@@ -23,7 +21,7 @@ Effects are one ordered list. The same list sits on a clip, on a track, or on th
}
```
-On a **track**, the list covers every clip on it. On the **timeline**, it covers the finished video, which is where one grade for the whole edit belongs.
+On a track, it applies to every clip. On the timeline, it applies to the finished video, so put a grade for the whole edit there.
```json
"timeline": {
@@ -34,33 +32,53 @@ On a **track**, the list covers every clip on it. On the **timeline**, it covers
}
```
-Every entry takes an optional `start` and `length`, in seconds or as a percent of what it sits on, so an effect can run for part of a clip. Numeric parameters take keyframes, so they can move while it runs.
+Set an optional `start` and `length`, in seconds or as a percent of what the effect sits on, to run it for part of a clip. Keyframe any numeric parameter to change it over time.
```json
{ "type": "blur", "radius": [{ "time": "0%", "value": 14, "easing": "easeOutCubic" }, { "time": 1, "value": 0 }] }
```
-There are 34 effects, for picture and for sound, each with its own parameters in real units. The full list, with a rendered before and after for each one, is the [effects reference](https://rendobar.com/compose/effects/). The groups:
+There are 34 built-in effects, 23 for the picture and 11 for sound, with parameters in real units. The [Compose features page](https://rendobar.com/compose/features/#picture) shows each one before and after. Picture effects:
-| Group | Examples |
+| Effect | What it does |
| --- | --- |
-| Colour and looks | `color`, `look`, `lut`, `denoise`, `glow`, `grain`, `vignette`, `sharpen`, `blur` |
-| Stylise | `shake`, `glitch`, `mosaic`, `posterize`, `rgbShift`, `scanLines`, `sketch`, `trails` |
-| Layers and masking | `chromaKey`, `mask`, `blurRegion`, `outline`, `dropShadow` |
-| Sound | `eq`, `compressor`, `limiter`, `gate`, `highPass`, `lowPass`, `loudness`, `pitch`, `echo`, `reverb`, `voiceDenoise` |
-| Sound made visible | `visualizer` |
-
-Two of them read a file, which is fetched and checked like any clip source: `lut` takes a `.cube` grade, and `mask` takes a picture that decides what of the clip shows.
+| `color` | Grade by hand: brightness, contrast, saturation, gamma, hue and temperature. Every setting is optional. |
+| `look` | A one-word look: greyscale, boost, muted, lighten, darken, contrast, negative, sepia or oldFilm. |
+| `lut` | Grade with a `.cube` LUT, the file a colourist or a grading app exports. |
+| `blur` | Soften the picture. 5 to 15 makes a backdrop that keeps text over it readable. |
+| `sharpen` | Sharpen soft footage. |
+| `denoise` | Remove the grain a small camera sensor adds in low light. |
+| `glow` | Make the bright parts bloom. Best on lights against a dark frame. |
+| `grain` | Add film grain, so digital footage looks less clinical. |
+| `vignette` | Darken the corners to pull the eye to the middle. |
+| `shake` | Make a steady shot look handheld. The picture zooms in a little so its edges never show. |
+| `glitch` | Blocks of the picture jump sideways and the colours tear. Give it a short length. |
+| `pixelate` | Large square pixels over the whole picture. |
+| `posterize` | Flatten the picture into a few steps of each colour, like a screen print. |
+| `rgbShift` | Pull the red and blue channels apart for an analogue look. |
+| `scanLines` | The dark lines of a CRT monitor. |
+| `sketch` | Turn the picture into a pencil drawing. |
+| `trails` | Moving lights and people leave streaks. Needs a camera that does not move. |
+| `chromaKey` | Remove a green screen or any solid colour, so the track below shows through. |
+| `mask` | Show the clip only where an image says: white or opaque keeps it, black or clear cuts it. |
+| `blurRegion` | Blur one part of the picture: a face, a number plate, a screen. |
+| `outline` | Draw a line around a layer with transparency, such as a keyed subject or a logo. |
+| `dropShadow` | Add a soft shadow under a layer with transparency, so it stands out from the background. |
+| `visualizer` | Draw the sound over the picture as a waveform or a spectrum. |
+
+The 11 sound effects are on [Audio and mixing](/docs/jobs/compose/audio#sound-effects).
+
+Two effects read a file. `lut` takes a `.cube` grade and `mask` takes an image that sets which parts of the clip show. The file is fetched and checked like any clip source.
```json
{ "type": "mask", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png", "channel": "alpha" }
```
-An effect that changes the picture is refused on a clip that has none, and one that changes the sound is refused on a silent clip, so a mistake is a 400 rather than a render that ignores it. Text and shapes draw their own outline and shadow, so `outline` and `dropShadow` are refused on them.
+A picture effect on a clip with no picture, or a sound effect on a silent clip, returns a 400. Text and shapes draw their own outline and shadow, so you cannot put `outline` or `dropShadow` on them.
## Colour
-One `color` effect carries brightness, contrast, saturation, temperature and the rest, so there is one place to correct a picture rather than nine overlapping filters. `look` applies a promoted recipe in one word, and `lut` takes a `.cube` file you supply.
+`color` holds brightness, contrast, saturation, temperature and the rest in one effect. `look` applies a ready-made look in one word. `lut` takes a `.cube` file you supply.
@@ -91,7 +109,7 @@ One `color` effect carries brightness, contrast, saturation, temperature and the
## Stylise
-`glitch`, `pixelate`, `posterize`, `rgbShift`, `scanLines`, `sketch`, `trails` and `shake`. Used for a beat or a scene change rather than a whole video.
+`glitch`, `pixelate`, `posterize`, `rgbShift`, `scanLines`, `sketch`, `trails` and `shake`. Use them for a beat or a scene change, not across a whole video.
@@ -122,7 +140,7 @@ One `color` effect carries brightness, contrast, saturation, temperature and the
## Layers
-`chromaKey` removes a solid background, `mask` cuts a clip to a shape, `blurRegion` hides a face or a plate, and `outline` and `dropShadow` edge a layer that has transparency.
+`chromaKey` removes a solid background. `mask` cuts a clip to a shape. `blurRegion` hides a face or a number plate. `outline` and `dropShadow` add an edge or a shadow to a layer with transparency.
@@ -162,13 +180,14 @@ One `color` effect carries brightness, contrast, saturation, temperature and the
## Anything the engine can do
-
-`{ "type": "engine" }` reaches the rest of the render engine by its own names, for the case a promoted effect does not cover. Every service and property is checked before the job starts, so a typo is a 400 and not a failed render.
+If no built-in effect fits, `{ "type": "engine" }` reaches more than 300 other filters by name. A typo in a filter or parameter returns a 400 before the job starts.
```json
-{ "type": "engine", "service": "avfilter.colorize", "params": { "av.hue": 200, "av.saturation": 0.6 } }
+{ "type": "engine", "filter": "colorize", "params": { "hue": 200, "saturation": 0.6 } }
```
-An engine entry that duplicates a promoted effect is refused, so there is one way to ask for each thing. The names come from the engine and can change when it is upgraded, which the promoted names never do.
+FFmpeg filters use their FFmpeg name and option names, so `colorize` takes `hue`, `saturation` and `lightness`. Other filters use a camelCase name, such as `cartoon` with `triplevel` and `diffspace`. `GET /compose/effects/engine` lists every filter an engine entry can name, with its parameters. `GET /compose/effects` does the same for the built-in effects.
+
+An engine entry that repeats a built-in effect is refused. The engine's filter list can change when the engine is upgraded. The built-in effects do not.
-Every effect and every parameter is in the [effect reference](/docs/jobs/compose/reference/effects). Sound effects are on [Audio levels and effects](/docs/jobs/compose/audio).
+The [effect reference](/docs/jobs/compose/reference/effects) lists every effect and parameter. Sound effects are on [Audio and mixing](/docs/jobs/compose/audio).
diff --git a/jobs/compose/fonts.mdx b/jobs/compose/fonts.mdx
index 6bfff83..9434407 100644
--- a/jobs/compose/fonts.mdx
+++ b/jobs/compose/fonts.mdx
@@ -7,17 +7,15 @@ keywords: ["custom fonts video api", "arabic text video api", "video font fallba
canonical: "https://rendobar.com/docs/jobs/compose/fonts"
---
-
-
Name any [Google Fonts](https://fonts.google.com) family in `style.font` and it
-is fetched at render time. Nothing to upload, no configuration.
+loads at render time, with no upload or setup.
```json
"style": { "font": "Bebas Neue", "size": 96, "weight": 700 }
```
-Seven families are bundled into the render image and resolve instantly, with no
-fetch: `Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`,
+Seven families ship in the render image and need no fetch:
+`Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`,
`Bebas Neue`.
@@ -26,9 +24,8 @@ fetch: `Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`,
## Your own font files
-
-Put the file URLs in the job's `fonts` input, each under a family name **you**
-choose, then reference that name in `style.font`.
+List the file URLs in the job's `fonts` input, each under a family name you
+choose. Then use that name in `style.font`.
```json
{
@@ -52,20 +49,18 @@ choose, then reference that name in `style.font`.
}
```
-There is no limit on how many you declare. TTF, OTF, TTC, WOFF and WOFF2 all
-work, and an [uploaded asset's](/sdk#uploads) content URL works in
-`src` like any other input URL.
+You can declare any number. TTF, OTF, TTC, WOFF and WOFF2 all work, and `src`
+takes an [uploaded asset's](/docs/sdk#uploads) content URL like any other URL.
-Naming your own font is the point. The alternative is to reference the family
-name buried inside the file, which you have to discover with a font inspector
-and which is frequently not what the file is called. Omit `family` and that
-embedded name is read for you, so `{ "src": "..." }` on its own is also valid.
+Choosing a name means you never need the family name stored inside the file,
+which often differs from the file name and takes a font inspector to find. Omit
+`family` and the engine reads the stored name, so `{ "src": "..." }` alone is
+valid.
-A family you declare shadows a bundled one of the same name, exactly the way an
-`@font-face` rule beats a system font in CSS. Declare a font called `Inter` and
-yours is the one that renders.
+A family you declare overrides a bundled one with the same name, as an
+`@font-face` rule overrides a system font in CSS.
-For a single file, `inputs.font` is the shorthand. It is addressed as `custom`:
+For a single file, use the `inputs.font` shorthand and refer to it as `custom`:
```json
"inputs": { "font": "https://cdn.example.com/BrandDisplay.ttf" },
@@ -74,17 +69,15 @@ For a single file, `inputs.font` is the shorthand. It is addressed as `custom`:
## Fallback stacks
-
-`style.font` takes a CSS-style list, and it falls through in order:
+`style.font` takes a CSS-style list and tries each entry in order:
```json
"style": { "font": "Display, Bebas Neue, sans-serif" }
```
-This is a failure policy, not a preference. If your CDN is unreachable or
-a Google family cannot be fetched, the render continues with the next entry
-instead of failing the job or silently substituting whatever the system happens
-to have. Every substitution is reported on the job:
+If your CDN is down or a Google family cannot be fetched, the render uses the
+next entry. The job does not fail or fall back to a random system font, and it
+reports every substitution:
```json
"steps": [
@@ -99,16 +92,15 @@ to have. Every substitution is reported on the job:
]
```
-A value naming nothing resolvable is rejected at submit, which catches the
-common mistake of pasting a font URL into `style.font` instead of into `fonts`.
+If `style.font` names no font that can be found, the API rejects the job at
+submit. This catches a font URL pasted into `style.font` instead of `fonts`.
## Weights and languages
-
-`weight` runs 100 to 1000. Variable fonts honour it exactly; a static family
-snaps to its nearest available cut, rounding up on a tie.
+`weight` runs from 100 to 1000. A variable font uses the exact value. A static
+family uses its nearest available weight, rounding up on a tie.
Latin, Cyrillic, Greek, Vietnamese, Turkish, Arabic, Hebrew, Thai, Devanagari,
-Bengali, Tamil, Amharic, Khmer, Chinese, Japanese and Korean all render, with
-right-to-left joining and conjunct shaping handled by the engine. Colour emoji
-does not render in text assets.
+Bengali, Tamil, Amharic, Khmer, Chinese, Japanese and Korean all render. The
+engine handles right-to-left joining and conjunct shaping. Colour emoji does not
+render in text assets.
diff --git a/jobs/compose/framing.mdx b/jobs/compose/framing.mdx
index 3c9dc74..3488849 100644
--- a/jobs/compose/framing.mdx
+++ b/jobs/compose/framing.mdx
@@ -1,13 +1,13 @@
---
title: "Placing and framing a clip"
sidebarTitle: "Framing"
-description: "Set a clip's position, scale, rotation, box, crop and opacity, and see every one of them before and after on one shot."
+description: "Set where a clip sits in the frame: position, anchor, scale, box, fit, rotation, flip, crop and corner pin, each shown before and after with its JSON."
icon: "crop-simple"
keywords: ["video position api", "picture in picture api", "crop video json"]
canonical: "https://rendobar.com/docs/jobs/compose/framing"
---
-Every clip takes the same set of fields, whatever it holds. Each one below is the same shot on the same backdrop, changed in exactly one way.
+Every clip takes these fields, whatever it holds. Most live in `transform`. Each example changes one thing on the same shot and backdrop. Position, scale, rotation, size and the corner pin all take keyframes. See [Keyframes and easing](/docs/jobs/compose/motion).
## Where it sits
@@ -20,7 +20,11 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
-`position` is the clip's centre by default, as a percent of the frame or in pixels. `anchor` changes what the point refers to.
+```json
+"transform": { "position": { "x": "76%", "y": "26%" } }
+```
+
+`position` places the clip's centre by default, as a percent of the frame or in pixels. `anchor` picks a different point of the clip to place.
@@ -31,6 +35,10 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"transform": { "position": { "x": "30%", "y": "30%" }, "anchor": "topLeft" }
+```
+
## How big it is
@@ -42,7 +50,11 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
-`scale` multiplies the clip's natural size. `size` gives it a box of its own, which the picture fills the way [fit](/docs/jobs/compose/output#how-a-picture-fills-its-box) says, so a box plus cover is how a wide shot becomes a tall panel.
+```json
+"transform": { "scale": 0.34 }
+```
+
+`scale` multiplies the clip's natural size. `size` gives the clip its own box, and [fit](#how-a-picture-fills-its-box) sets how the picture fills it. A box with `fit` set to `cover` turns a wide shot into a tall panel.
@@ -53,6 +65,10 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"transform": { "size": { "width": "30%", "height": "88%" }, "fit": "cover" }
+```
+
## Turning and mirroring
@@ -64,6 +80,10 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"transform": { "rotate": -7 }
+```
+
@@ -73,6 +93,10 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"flip": { "horizontal": true }
+```
+
## Trimming the edges
@@ -84,6 +108,10 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"crop": { "top": "18%", "bottom": "18%", "left": "12%", "right": "12%" }
+```
+
@@ -93,9 +121,14 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
+```json
+"transform": { "scale": 0.5 },
+"crop": { "radius": 0.14 }
+```
+
## Pinning to a surface
-`corners` is the other form of a transform: four points instead of a position. It puts footage on a screen or a sign in the shot, and it does not combine with position, scale, rotate or fit.
+`corners` places a clip by four points in place of a position, to put footage on a screen or a sign in the shot. It does not combine with position, scale, rotate or fit. Keyframe the points to follow a moving surface.
@@ -106,26 +139,43 @@ Every clip takes the same set of fields, whatever it holds. Each one below is th
-## How solid, and how fast
+```json
+"transform": {
+ "corners": {
+ "topLeft": { "x": "45.0%", "y": "33.6%" },
+ "topRight": { "x": "62.6%", "y": "32.4%" },
+ "bottomRight": { "x": "61.0%", "y": "54.6%" },
+ "bottomLeft": { "x": "43.4%", "y": "54.0%" }
+ }
+}
+```
-
-
-
-
-
-
-
-
+## How a picture fills its box
-Keyframes on `opacity` are how a clip fades in or out. Anything that moves is on [Keyframes and easing](/docs/jobs/compose/motion).
+`fit` sets what happens when a clip's box has a different shape from its picture, such as a wide photo in a phone-shaped box.
+
+| `fit` | The picture |
+| --- | --- |
+| `contain` | Shows all of it, with bars where it does not reach. The default. |
+| `cover` | Fills the box and cuts off what overflows. |
+| `fill` | Stretches to the box and distorts. |
+| `blur` | Shows all of it over a blurred copy of itself, with no bars. |
-
+
-
-
+
+
+```json
+"transform": { "size": { "width": 304, "height": 540 }, "fit": "cover" }
+```
+
+`blur` fills the whole frame around a clip that fills it, so it does not combine with position, size, scale or rotation.
+
+For opacity, speed and blend modes, see [Media clips](/docs/jobs/compose/media).
+
Every field is in the [clip reference](/docs/jobs/compose/reference/timeline#clip).
diff --git a/jobs/compose/index.mdx b/jobs/compose/index.mdx
index 1dced86..3b7db9b 100644
--- a/jobs/compose/index.mdx
+++ b/jobs/compose/index.mdx
@@ -23,12 +23,362 @@ canonical: "https://rendobar.com/docs/jobs/compose"
}}
/>
-`compose` renders a video from a JSON timeline. Start with one clip, then layer up: more clips, transitions, overlays, and effects. This page builds from the smallest render to the full model. Each step adds one idea.
+
+
+
+
+```json The request that made it expandable
+{
+ "schemaVersion": 1,
+ "output": {
+ "format": "mp4",
+ "resolution": { "width": 1920, "height": 1080 },
+ "fps": 30
+ },
+ "timeline": {
+ "tracks": [
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#F3EAD7"
+ },
+ "length": 1.8,
+ "start": 0
+ },
+ {
+ "type": "transition",
+ "transition": "wipe",
+ "map": "https://cdn.rendobar.com/compose/player/media/sk1-draw.jpg",
+ "duration": 1.6
+ },
+ {
+ "asset": {
+ "type": "image",
+ "src": "https://cdn.rendobar.com/compose/player/media/sk1.png"
+ },
+ "transform": {
+ "fit": "cover",
+ "scale": [
+ { "time": 0, "value": 1, "easing": "linear" },
+ { "time": 3.2, "value": 1.05 }
+ ]
+ },
+ "length": 3.2
+ },
+ {
+ "type": "transition",
+ "transition": "crossfade",
+ "duration": 0.4
+ },
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#F3EAD7"
+ },
+ "length": 1.9
+ },
+ {
+ "type": "transition",
+ "transition": "wipe",
+ "map": "https://cdn.rendobar.com/compose/player/media/sk2-draw.jpg",
+ "duration": 1.4
+ },
+ {
+ "asset": {
+ "type": "image",
+ "src": "https://cdn.rendobar.com/compose/player/media/sk2.png"
+ },
+ "transform": {
+ "fit": "cover",
+ "scale": [
+ { "time": 0, "value": 1, "easing": "linear" },
+ { "time": 3.2, "value": 1.05 }
+ ]
+ },
+ "length": 3.2
+ },
+ {
+ "type": "transition",
+ "transition": "crossfade",
+ "duration": 0.4
+ },
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#F3EAD7"
+ },
+ "length": 1.9
+ },
+ {
+ "type": "transition",
+ "transition": "wipe",
+ "map": "https://cdn.rendobar.com/compose/player/media/sk3-draw.jpg",
+ "duration": 1.4
+ },
+ {
+ "asset": {
+ "type": "image",
+ "src": "https://cdn.rendobar.com/compose/player/media/sk3.png"
+ },
+ "transform": {
+ "fit": "cover",
+ "scale": [
+ { "time": 0, "value": 1, "easing": "linear" },
+ { "time": 3.2, "value": 1.05 }
+ ]
+ },
+ "length": 3.2
+ },
+ {
+ "type": "transition",
+ "transition": "crossfade",
+ "duration": 0.4
+ },
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#F3EAD7"
+ },
+ "length": 0.4
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#FBF6EA",
+ "radius": 0.5
+ },
+ "transform": {
+ "size": { "width": 720, "height": 96 },
+ "position": { "x": 960, "y": 112 }
+ },
+ "start": 0.2,
+ "length": 9.8,
+ "opacity": [
+ { "time": 0, "value": 0, "easing": "easeOutQuad" },
+ { "time": 0.4, "value": 1 },
+ { "time": 9.4, "value": 1, "easing": "easeInQuad" },
+ { "time": 9.8, "value": 0 }
+ ]
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "text",
+ "text": "The Last Keeper · Part 1",
+ "style": {
+ "font": "Lora",
+ "size": 48,
+ "weight": 600,
+ "italic": true,
+ "color": "#2B2118",
+ "animate": { "type": "fade", "duration": 0.5 }
+ }
+ },
+ "transform": { "position": { "x": 960, "y": 112 } },
+ "start": 0.3,
+ "length": 9.7,
+ "opacity": [
+ { "time": 0, "value": 1 },
+ { "time": 9.35, "value": 1, "easing": "easeInQuad" },
+ { "time": 9.7, "value": 0 }
+ ]
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#FBF6EA",
+ "radius": 0.12,
+ "shadow": {
+ "color": "#00000033",
+ "blur": 24,
+ "x": 0,
+ "y": 8
+ }
+ },
+ "transform": {
+ "size": { "width": 1440, "height": 170 },
+ "position": { "x": 960, "y": 925 }
+ },
+ "start": 1,
+ "length": 2.3,
+ "opacity": [
+ { "time": 0, "value": 0, "easing": "easeOutQuad" },
+ { "time": 0.4, "value": 1 },
+ { "time": 1.9, "value": 1, "easing": "easeInQuad" },
+ { "time": 2.3, "value": 0 }
+ ]
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "text",
+ "text": "He kept the light for forty years.",
+ "style": {
+ "font": "Lora",
+ "size": 84,
+ "weight": 500,
+ "italic": true,
+ "color": "#2B2118",
+ "animate": {
+ "type": "fade",
+ "by": "word",
+ "duration": 0.9,
+ "out": "fade"
+ }
+ }
+ },
+ "transform": { "position": { "x": 960, "y": 925 } },
+ "start": 1.15,
+ "length": 2.15
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#FBF6EA",
+ "radius": 0.12,
+ "shadow": {
+ "color": "#00000033",
+ "blur": 24,
+ "x": 0,
+ "y": 8
+ }
+ },
+ "transform": {
+ "size": { "width": 1440, "height": 170 },
+ "position": { "x": 960, "y": 925 }
+ },
+ "start": 4.4,
+ "length": 2.3,
+ "opacity": [
+ { "time": 0, "value": 0, "easing": "easeOutQuad" },
+ { "time": 0.4, "value": 1 },
+ { "time": 1.9, "value": 1, "easing": "easeInQuad" },
+ { "time": 2.3, "value": 0 }
+ ]
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "text",
+ "text": "Every night he climbed the stairs.",
+ "style": {
+ "font": "Lora",
+ "size": 84,
+ "weight": 500,
+ "italic": true,
+ "color": "#2B2118",
+ "animate": {
+ "type": "fade",
+ "by": "word",
+ "duration": 0.9,
+ "out": "fade"
+ }
+ }
+ },
+ "transform": { "position": { "x": 960, "y": 925 } },
+ "start": 4.55,
+ "length": 2.15
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "shape",
+ "shape": "rect",
+ "color": "#FBF6EA",
+ "radius": 0.12,
+ "shadow": {
+ "color": "#00000033",
+ "blur": 24,
+ "x": 0,
+ "y": 8
+ }
+ },
+ "transform": {
+ "size": { "width": 1440, "height": 170 },
+ "position": { "x": 960, "y": 925 }
+ },
+ "start": 7.7,
+ "length": 2,
+ "opacity": [
+ { "time": 0, "value": 0, "easing": "easeOutQuad" },
+ { "time": 0.4, "value": 1 },
+ { "time": 1.6, "value": 1, "easing": "easeInQuad" },
+ { "time": 2, "value": 0 }
+ ]
+ }
+ ]
+ },
+ {
+ "clips": [
+ {
+ "asset": {
+ "type": "text",
+ "text": "Then, one night, a ship came home.",
+ "style": {
+ "font": "Lora",
+ "size": 84,
+ "weight": 500,
+ "italic": true,
+ "color": "#2B2118",
+ "animate": {
+ "type": "fade",
+ "by": "word",
+ "duration": 0.9,
+ "out": "fade"
+ }
+ }
+ },
+ "transform": { "position": { "x": 960, "y": 925 } },
+ "start": 7.85,
+ "length": 1.85
+ }
+ ]
+ }
+ ]
+ }
+}
+```
-## Your simplest render
+The same request, built up step by step, is the [illustrated story recipe](https://rendobar.com/compose/recipes/illustrated-story/).
+
+`compose` renders a video from a JSON timeline. You start with one clip and add more clips, transitions, overlays and effects as you need them.
+## Your simplest render
-The smallest timeline is one track with one clip. This renders the first three seconds of a video to MP4. The whole API call:
+The smallest timeline is one track with one clip. This call renders the first three seconds of a video to MP4.
@@ -37,7 +387,8 @@ import { createClient, outputUrl } from "@rendobar/sdk";
const client = createClient({ apiKey: "rb_YOUR_KEY" });
-const job = await client.jobs.create({
+// run() creates the job and waits for it to finish.
+const job = await client.jobs.run({
type: "compose",
params: {
schemaVersion: 1,
@@ -65,8 +416,7 @@ const job = await client.jobs.create({
},
});
-const result = await client.jobs.wait(job.id);
-console.log(outputUrl(result));
+console.log(outputUrl(job));
```
```python Python
@@ -144,13 +494,10 @@ curl -X POST https://api.rendobar.com/jobs \
**Assets:** video, image, audio, text, timer, subtitles, shape, animation (Lottie), composition · **Output:** mp4 / webm / gif / mp3 (audio only) / jpg / png (still frame)
-Without `trim`, a clip plays its source from the start for `length` seconds, or for the whole source when `length` is left off. To use a specific segment instead, add `trim: { from, to }` (in source seconds) to the asset. `length` is always the timeline duration. `trim` only chooses which part of the source fills it.
-
-That is the shape of every render. Everything below is one more clip, one more track, or one more field.
+Without `trim`, a clip plays its source from the start for `length` seconds. Leave `length` off and it plays the whole source. To play one segment, add `trim: { from, to }` to the asset, in source seconds. `length` sets how long the clip lasts on the timeline, and `trim` picks which part of the source fills that time.
## Join two clips
-
Put a second clip on the same track and a transition between them.
```json
@@ -161,9 +508,9 @@ Put a second clip on the same track and a transition between them.
]
```
-No timing anywhere. Each clip plays its whole source, the second follows the first, and the transition overlaps them by its own `duration`. You do not need to know how long either file is.
+You set no timing here. Each clip plays its whole source, the second starts after the first, and the transition overlaps them by its `duration`. You do not need to know how long either file is.
-Writing the timing out by hand gives the same render. The second clip starts `duration` seconds before the first ends, which is what the engine works out for you above.
+You can also write the timing out. This gives the same render, with the second clip starting `duration` seconds before the first ends.
```json
"clips": [
@@ -173,53 +520,49 @@ Writing the timing out by hand gives the same render. The second clip starts `du
]
```
-| Transition | Effect |
-| --- | --- |
-| `crossfade` | A dissolves directly into B |
-| `fade` | A dips through black, then B fades up |
-| `slide` | B slides in over A from an edge |
-| `push` | B pushes A off the frame |
-| `wipe` | An edge travels across, in a shape or along your own greyscale map |
-| `zoom` | B grows in from the centre |
-| `none` | A hard cut, no blend |
-
-`slide`, `push` and `wipe` take a `direction`: `left`, `right`, `up` or `down`. A wipe takes a `shape` instead (`circle`, `box`, `barnDoorHorizontal`, `barnDoorVertical`), or a `map`: a greyscale image of your own, where black turns over first and white last.
-
-```json
-{ "type": "transition", "transition": "wipe", "shape": "circle", "duration": 0.8 }
-```
+See [Transitions](/docs/jobs/compose/transitions) for the seven transition kinds, wipe shapes, directions and your own wipe patterns.
## Everything else
-This page is the first render and the shape of a request. Each idea past that has its own page.
-
- Tracks, clips and timing, and why you can leave most of the timing out.
+ Tracks, clips, scenes, and when you can leave timing out.
+
+
+ Video, images, audio and Lottie, with trim, speed, opacity and blend modes.
- Position, scale, rotate, crop, pin and fade a clip, each shown before and after.
+ Position, scale, rotate, crop, fit and pin a clip, each shown before and after.
-
- Crossfade, fade, slide, push, wipe and zoom, each one shown playing.
+
+ Readable titles, entrances and exits, and kinetic titles a word at a time.
+
+
+ Every caption in one clip, from cues or an .srt file, and countdowns.
+
+
+ Google fonts by name, bundled families, your own files, and other scripts.
-
- Keyframes on anything numeric, and all 33 easings.
+
+ Bars, cards, circles and SVG paths, with gradients, outlines and shadows.
-
- Titles, lower thirds, a running clock, and subtitles from a file.
+
+ Animate anything numeric with 33 easings, and turn clips in 3D.
-
- Bars, circles, paths and masks, with fills, outlines and shadows.
+
+ Crossfade, fade, slide, push, wipe and zoom, and your own wipe patterns.
- Colour, looks, stylised filters and layer effects, before and after.
+ Colour, looks, stylised filters, keying and every other filter by name.
+
+
+ Levels, ducking, panning, the mix, voice clean-up and audiograms.
-
- Levels, fades, voice clean-up, and sound drawn on the picture.
+
+ Formats, size presets, frame rate, quality and transparent video.
-
- Frame size, format, quality, and a free check before you render.
+
+ One price per minute of output, and a free check before you render.
The pacing, transition and text conventions working editors follow.
@@ -231,11 +574,10 @@ This page is the first render and the shape of a request. Each idea past that ha
## See also
-
-- [Effects reference](https://rendobar.com/compose/effects/): every effect and transition, rendered before and after
-- [Job output](/concepts/job#the-output): the output shape every job returns
-- [Animated captions](/jobs/captions/animate): word-level captions over a rendered video
-- [Compress](/jobs/compress): shrink the render to a size or quality budget
-- [FFmpeg](/jobs/ffmpeg): drop to a raw command when the timeline cannot express it
-- [Webhooks](/guides/webhooks): receive `job.completed` instead of polling
-- [Plan limits](/support/limits): render duration and file-size caps per plan
+- [Compose features](https://rendobar.com/compose/features/): every engine feature, searchable, most shown before and after
+- [Job output](/docs/concepts/job#the-output): the output shape every job returns
+- [Animated captions](/docs/jobs/captions/animate): word-level captions over a rendered video
+- [Compress](/docs/jobs/compress): shrink the render to a size or quality budget
+- [FFmpeg](/docs/jobs/ffmpeg): drop to a raw command when the timeline cannot express it
+- [Webhooks](/docs/guides/webhooks): receive `job.completed` instead of polling
+- [Plan limits](/docs/support/limits): render duration and file-size caps per plan
diff --git a/jobs/compose/media.mdx b/jobs/compose/media.mdx
new file mode 100644
index 0000000..92b71f7
--- /dev/null
+++ b/jobs/compose/media.mdx
@@ -0,0 +1,126 @@
+---
+title: "Video, image, audio and Lottie clips"
+sidebarTitle: "Media clips"
+description: "Add footage, stills, sound and Lottie animation to a timeline, use part of a source, change its speed and opacity, and blend it into the layer below."
+icon: "photo-film"
+keywords: ["video clip api", "trim video json", "video blend mode api", "lottie in video api", "slow motion video api"]
+canonical: "https://rendobar.com/docs/jobs/compose/media"
+---
+
+A clip's `asset` sets what it plays. Four asset types read a file from a URL. Timing, placement and effects work the same for all four.
+
+| `asset.type` | Plays | Takes |
+| --- | --- | --- |
+| `video` | Footage, with its own sound | `src`, `trim`, `volume` |
+| `image` | A png or jpg still | `src` |
+| `audio` | Music, a voice-over or a sound effect | `src`, `trim`, `volume`, `fadeIn`, `fadeOut` |
+| `animation` | A Lottie `.json` file, with its transparency | `src`, `trim` |
+
+`src` is an https URL, or the URL of a file you uploaded to Rendobar. Text, timers, subtitles, shapes and nested timelines are clips without a file, each on its own page.
+
+## Video
+
+```json
+{
+ "asset": {
+ "type": "video",
+ "src": "https://cdn.rendobar.com/assets/examples/sea.mp4",
+ "trim": { "from": 4, "to": 9 },
+ "volume": 0.6
+ },
+ "start": 0
+}
+```
+
+`volume` is linear gain: `1` keeps the source level and `0` mutes it. Keyframe it to duck music under a voice. See [Audio and mixing](/docs/jobs/compose/audio).
+
+## Images
+
+An `image` shows one frame for the clip's `length`. A still with no motion looks like a mistake, so add a slow push-in: keyframe `transform.scale` from `1` to about `1.15` across the clip. See [Keyframes and easing](/docs/jobs/compose/motion).
+
+```json
+{
+ "asset": { "type": "image", "src": "https://example.com/listing.jpg" },
+ "transform": { "scale": [{ "time": "0%", "value": 1, "easing": "easeInOutSine" }, { "time": "100%", "value": 1.15 }] },
+ "length": 4
+}
+```
+
+## Audio
+
+An `audio` clip has no picture. Put music on its own track under the footage and set its level with `volume`. `fadeIn` and `fadeOut` fade it in from silence and out to silence, in seconds. See [Audio and mixing](/docs/jobs/compose/audio) for levels, ducking and sound effects.
+
+## Lottie animations
+
+An `animation` plays a Lottie `.json` file, as exported by After Effects, Figma or LottieFiles, with a transparent background. It plays once, for its own length. Place and size it with `transform`, like an image.
+
+```json
+{ "asset": { "type": "animation", "src": "https://example.com/logo-reveal.json" }, "transform": { "scale": 0.4, "position": { "x": "50%", "y": "50%" } } }
+```
+
+Export the file self-contained. The API refuses a file that loads images or fonts from elsewhere, because the render only gets files through a checked download.
+
+
+
+
+
+## Using part of a source
+
+`trim` picks which seconds of the source fill the clip. `{ "from": 4, "to": 9 }` plays seconds 4 to 9 of the file. `length` sets how long the clip lasts on the timeline. Without `trim`, the clip plays from the start of the source.
+
+## Speed
+
+`speed` plays a clip faster or slower. `2` is double speed and `0.5` is slow motion. At double speed a clip lasts half as long, and its sound changes pitch.
+
+```json
+{ "asset": { "type": "video", "src": "https://example.com/run.mp4" }, "speed": 0.5 }
+```
+
+
+
+
+
+
+
+
+
+
+Shapes, nested timelines and Lottie animations do not accept `speed`. The first two have no file to speed up, and a Lottie plays at its own frame rate. Avoid slow motion on the outgoing clip of a transition, as it can run out of frames before the transition ends.
+
+## Opacity
+
+`opacity` runs from `0` (invisible) to `1` (solid). Keyframe it to fade a clip in or out.
+
+```json
+{ "asset": { "type": "video", "src": "https://example.com/b-roll.mp4" }, "opacity": [{ "time": 0, "value": 0 }, { "time": 0.5, "value": 1 }] }
+```
+
+
+
+
+
+
+
+
+
+
+## Blend modes
+
+`blendMode` sets how a clip mixes with the layers below, for example a light leak or texture over footage. It only works on a clip above another track.
+
+| `blendMode` | Use it for |
+| --- | --- |
+| `normal` | The clip sits over what is below. The default. |
+| `screen` | Light leaks, flares and glows: black disappears and light adds. |
+| `multiply` | Darkening textures and paper: white disappears and dark adds. |
+| `overlay` | More contrast from a texture without changing the overall brightness. |
+| `add` | Brighter than screen, for strong light. |
+| `lighten` | Keeps whichever of the two is lighter. |
+| `darken` | Keeps whichever of the two is darker. |
+| `difference` | A stylised inverted look. |
+
+```json
+{ "asset": { "type": "video", "src": "https://example.com/light-leak.mp4" }, "blendMode": "screen", "opacity": 0.8 }
+```
+
+The [video](/docs/jobs/compose/reference/video), [image](/docs/jobs/compose/reference/image), [audio](/docs/jobs/compose/reference/audio) and [animation](/docs/jobs/compose/reference/animation) references list every asset field. Clip fields are in the [timeline reference](/docs/jobs/compose/reference/timeline#clip).
diff --git a/jobs/compose/motion.mdx b/jobs/compose/motion.mdx
index ef670d0..7041442 100644
--- a/jobs/compose/motion.mdx
+++ b/jobs/compose/motion.mdx
@@ -1,16 +1,13 @@
---
title: "Keyframes and easing"
sidebarTitle: "Motion"
-description: "Add keyframes to position, scale, rotation, opacity, volume and any numeric effect parameter, with 33 easings."
+description: "Add keyframes to position, scale, rotation, 3D turns, opacity, volume and any numeric effect parameter, with 33 easings."
icon: "chart-line"
keywords: ["video keyframes api", "ken burns effect api", "video easing curves"]
canonical: "https://rendobar.com/docs/jobs/compose/motion"
---
-One shape covers a Ken Burns push, a fade, a colour change and ducking music under a voice.
-
-
-Any numeric field that moves takes a list instead of a value: `position`, `scale`, `rotate`, `opacity`, `volume`, a shape's `color` and `radius`, a box's `size`, and any numeric effect parameter.
+To animate a numeric field, give it a list of keyframes instead of a value. [Where keyframes are accepted](#where-keyframes-are-accepted) lists every field that takes one.
```json
"transform": {
@@ -21,11 +18,10 @@ Any numeric field that moves takes a list instead of a value: `position`, `scale
}
```
-- `time` is seconds from the clip's start, or a percent of its length, so `"100%"` means the last frame without knowing how long the clip is.
-- `easing` shapes the move **from that keyframe to the next one**, so it goes on the first of a pair. The last keyframe's easing is never used.
-- 33 easings: `step`, `linear`, `smooth`, and the `easeIn`, `easeOut` and `easeInOut` families over Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic and Bounce.
+- `time` is seconds from the clip's start or a percent of its length. `"100%"` is the last frame, whatever the clip's length.
+- `easing` sets the curve from that keyframe to the next, so put it on the first of a pair. The last keyframe's easing is never used. There are 33 easings, listed below.
-A Ken Burns push-in is the example above. A fade is the same shape on `opacity`, and ducking music under a voice is the same shape on `volume`.
+The example above is a Ken Burns push-in. Use the same keyframes on `opacity` for a fade, on a shape's `color` for a colour change, or on `volume` to duck music under a voice.
@@ -36,14 +32,44 @@ A Ken Burns push-in is the example above. A fade is the same shape on `opacity`,
-A still photograph becomes a shot. Nothing else changed between the two.
+The scale keyframes are the only difference between the two videos.
## Where keyframes are accepted
-`transform.position`, `transform.scale`, `transform.rotate`, `transform.size`, clip `opacity`, clip `volume`, a shape's `color` and `radius`, and every effect parameter marked animatable in the [effect reference](/docs/jobs/compose/reference/effects).
+`transform.position`, `transform.scale`, `transform.rotate`, `transform.rotateX`, `transform.rotateY`, `transform.size` (a box's size), `transform.corners` (the four corners of a pin), clip `opacity`, `asset.volume` on video and audio, a shape's `color` and `radius`, and every effect parameter marked animatable in the [effect reference](/docs/jobs/compose/reference/effects).
+
+## Turning in 3D
+
+`rotateY` turns a clip around its vertical axis and `rotateX` tips it around its horizontal axis. Both are in degrees and take keyframes. `perspective` is the viewer's distance from the frame in pixels. 600 looks dramatic, 2000 looks subtle, and the default is 1200. The clip turns about its `anchor`.
+
+```json
+"transform": {
+ "scale": 0.62,
+ "rotateY": [
+ { "time": 0, "value": -80, "easing": "easeOutCubic" },
+ { "time": 1.4, "value": -18 }
+ ],
+ "perspective": 900
+}
+```
+
+
+
+
+
+
+
+
+
+
+Use a turn on a card, a picture-in-picture or a product reveal. On full-frame footage, it shows the frame's edges.
+
+
+A 3D turn makes the clip an animated graphic. It costs the same per second, and renders up to 1080p at 30 fps on every self-serve plan. It works only on a top-level track or in `overlays`, not inside a scene or a composition. See [what each plan renders](/docs/jobs/compose/pricing#what-each-plan-renders).
+
## The 33 easings
-`step` holds the value until the next keyframe. `linear` moves at a constant rate. `smooth` eases in and out gently. The remaining 30 are the `easeIn`, `easeOut` and `easeInOut` families over Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic and Bounce.
+`step` holds the value until the next keyframe. `linear` moves at a constant rate. `smooth` curves through every keyframe, so a move across several keyframes has no sharp corners. The other 30 are the `easeIn`, `easeOut` and `easeInOut` families over Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic and Bounce.
-Back, Elastic and Bounce overshoot the value you gave, which is the point of them, so leave room at the edges of the frame when you use one on `position`.
+Back, Elastic and Bounce overshoot the value you set, by design. When you use one on `position`, leave room at the edges of the frame.
diff --git a/jobs/compose/output.mdx b/jobs/compose/output.mdx
index d30e0a9..372d969 100644
--- a/jobs/compose/output.mdx
+++ b/jobs/compose/output.mdx
@@ -1,75 +1,99 @@
---
-title: "Output, size and validation"
+title: "Output format, size and quality"
sidebarTitle: "Output"
-description: "Set the frame size, format, frame rate and quality of a render, and check a timeline for free before you pay for it."
+description: "Set what a compose render returns: mp4, webm, gif, mp3, jpg or png, a platform size preset or any resolution, the frame rate, and the encoder quality."
icon: "file-video"
-keywords: ["video output format api", "video resolution api", "validate video json"]
+keywords: ["video output format api", "video resolution api", "tiktok video size api", "transparent video api", "render gif api"]
canonical: "https://rendobar.com/docs/jobs/compose/output"
---
-
-
-`output` controls the size, frame rate, format and encoder. Give it an explicit `resolution`, or a named `size` preset like `"1080p"`, `"tiktok"` or `"reels"`.
+`output` sets the file you get back. Without it, you get a 1080p mp4 at 30 fps.
```json
-"output": { "format": "mp4", "resolution": { "width": 1080, "height": 1920 }, "fps": 30 }
+"output": { "format": "mp4", "size": "reels", "fps": 30, "quality": "high" }
```
-A 9:16 canvas with `"fit": "cover"` or a `crop` on the clip turns landscape footage into a vertical Reel that fills the frame, with no black bars. `"fit": "blur"` keeps all of the picture instead, over a blurred copy of itself.
+A compose job renders one file. For two sizes of the same edit, submit two jobs. They render at the same time.
-`background` fills whatever the clips do not cover. It takes a hex colour, or `"transparent"`, which png and webm keep as real alpha. mp4 has no transparency, so it refuses that rather than rendering black.
+## Formats
-```json
-{
- "schemaVersion": 1,
- "output": { "format": "webm", "size": "1080p" },
- "timeline": {
- "background": "transparent",
- "tracks": [{ "clips": [{ "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" } }] }]
- }
-}
-```
+| `format` | You get |
+| --- | --- |
+| `mp4` | Video with sound, H.264 and AAC. The default. |
+| `webm` | Video with sound, VP9 and Opus. Keeps a transparent background. |
+| `gif` | An animated image, with no sound. |
+| `mp3` | Sound alone. Needs no frame size. |
+| `jpg` | One frame, as a still. |
+| `png` | One frame, as a still. Keeps a transparent background. |
+
+## Size
-`range` renders part of the timeline instead of all of it, and on `jpg` and `png` its `start` is the frame you get.
+Set a `size` preset or an exact `resolution`. If you set both, `resolution` wins. Every format except `mp3` needs one of them.
```json
-"output": { "format": "png", "size": "1080p", "range": { "start": 4.5 } }
+"output": { "format": "mp4", "resolution": { "width": 1080, "height": 1350 } }
```
-## How a picture fills its box
+| `size` | Pixels |
+| --- | --- |
+| `360p` | 640 x 360 |
+| `720p` | 1280 x 720 |
+| `1080p` | 1920 x 1080 |
+| `1440p` | 2560 x 1440 |
+| `4k` | 3840 x 2160 |
+| `tiktok`, `reels`, `shorts`, `instagram-story` | 1080 x 1920 |
+| `instagram-feed` | 1080 x 1350 |
+| `square` | 1080 x 1080 |
+| `youtube-hd`, `x-landscape`, `linkedin` | 1920 x 1080 |
+| `youtube-4k` | 3840 x 2160 |
-A wide photograph in a phone-shaped box. `contain` shows all of it and leaves bars, `cover` fills the box and cuts what overflows.
+For a vertical Reel from landscape footage, set `"fit": "cover"` to fill the frame, or `"fit": "blur"` to show the whole picture over a blurred copy of itself. See [how a picture fills its box](/docs/jobs/compose/framing#how-a-picture-fills-its-box).
-
-
-
-
-
-
-
-
+## Frame rate
-A compose job renders exactly one file. Two sizes of the same edit are two jobs, which is also how you get them rendered at the same time. `gif` carries no audio, `mp3` is audio only and needs no frame size, and `jpg` and `png` are a single frame.
+`fps` sets the frame rate, 30 by default. Each plan caps frame size and frame rate together: 1080p at 30 fps on Free, 1440p at 30 fps on Pro, and 4K at 60 fps on Studio. Animated graphics render up to 1080p at 30 fps on every self-serve plan. A video above your plan returns `403 PLAN_LIMIT` at submit, and [validation](/docs/jobs/compose/pricing#check-a-timeline-before-you-render) catches it first. See [what each plan renders](/docs/jobs/compose/pricing#what-each-plan-renders).
-## Check it before you render
+## Quality
+`quality` trades picture quality against file size and render time.
-`validate` parses a timeline exactly as a render would and costs nothing. It returns the params as the engine reads them, anything it had to upgrade from an older shape, and warnings for settings the render will ignore.
+| `quality` | For |
+| --- | --- |
+| `draft` | A fast preview |
+| `standard` | Web delivery. The default. |
+| `high` | A sharper master, slower to encode |
+| `max` | The highest fidelity, slowest to encode |
-
+To set the encoder yourself, these override `quality`:
-```ts SDK
-const check = await client.compose.validate(params);
-if (!check.valid) console.error(check.problems);
-```
+| Field | Takes |
+| --- | --- |
+| `videoCodec` | `h264` (the mp4 default) or `vp9`. webm is always vp9. |
+| `audioCodec` | `aac` (the mp4 default), `opus` or `mp3`. webm is always opus. |
+| `crf` | Constant rate factor. Lower means better quality and a larger file: 0 to 51 for h264, 0 to 63 for vp9. Not for gif, mp3 or stills. |
+| `encoderPreset` | `ultrafast` through `veryslow`, h264 only. Slower gives a smaller file at the same quality. |
+
+## Transparent video
-```bash cURL
-curl -X POST https://api.rendobar.com/compose/validate \
- -H "Authorization: Bearer rb_YOUR_KEY" \
- -H "Content-Type: application/json" \
- -d '{ "params": { "schemaVersion": 1, "output": { "format": "mp4", "size": "1080p" }, "timeline": { "tracks": [{ "clips": [{ "asset": { "type": "video", "src": "https://example.com/a.mp4" } }] }] } } }'
+`timeline.background` fills any area the clips do not cover, with a hex colour (black by default) or `"transparent"`. png and webm keep transparency as an alpha channel, so you can render a lower third or a logo animation to lay over other video. mp4, gif and jpg have no transparency and refuse a transparent background instead of rendering it black.
+
+```json
+{
+ "schemaVersion": 1,
+ "output": { "format": "webm", "size": "1080p" },
+ "timeline": {
+ "background": "transparent",
+ "tracks": [{ "clips": [{ "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" }, "length": 3 }] }]
+ }
+}
```
-
+## Part of a timeline, or one frame
+
+`range` renders part of the timeline, from the second in `start` for `length` seconds. Leave out `length` to render to the end. On `jpg` and `png`, `start` is the moment captured, for a poster frame.
+
+```json
+"output": { "format": "png", "size": "1080p", "range": { "start": 4.5 } }
+```
-Every output field is in the [output reference](/docs/jobs/compose/reference/output).
+The [output reference](/docs/jobs/compose/reference/output) lists every output field.
diff --git a/jobs/compose/pricing.mdx b/jobs/compose/pricing.mdx
new file mode 100644
index 0000000..2552730
--- /dev/null
+++ b/jobs/compose/pricing.mdx
@@ -0,0 +1,96 @@
+---
+title: "Compose pricing and validation"
+sidebarTitle: "Pricing and validation"
+description: "See what a compose render costs, one price per minute of output video on every plan with a 15-second minimum, and check a timeline for free before you render."
+icon: "receipt"
+keywords: ["compose pricing", "json to video pricing", "video render cost per minute", "video api price per minute"]
+canonical: "https://rendobar.com/docs/jobs/compose/pricing"
+---
+
+Compose bills per second of the video you get back, not per second of compute. The rate is \$0.04 per minute of output video, on every plan and for every video your plan can render. You can check the price before you render.
+
+Each render bills at least 15 seconds, which is 1 cent. You pay from your [credit balance](/docs/concepts/credits). The \$5 signup credit covers 125 minutes of compose output.
+
+| Render | Price |
+| --- | --- |
+| 10 s (bills the 15 s minimum) | \$0.01 |
+| 30 s | \$0.02 |
+| 2 min | \$0.08 |
+| 10 min | \$0.40 |
+
+Frame size and animated graphics do not change the price. A 2-minute render costs the same at 720p or 4K. Animated text and 3D turns render up to 1080p at 30 fps, and adding them does not change the price.
+
+## What each plan renders
+
+Plans differ in what they can render. The per-minute price is the same.
+
+| Plan | Renders up to |
+| --- | --- |
+| Free (pay-as-you-go) | 1080p at 30 fps, animated graphics included |
+| Pro | 1440p at 30 fps. Animated graphics up to 1080p at 30 fps |
+| Studio | 4K at 60 fps. Animated graphics up to 1080p at 30 fps |
+| Enterprise | Animated graphics above 1080p or 30 fps, at a contract rate. Email `hello@rendobar.com` |
+
+Animated graphics are:
+
+- text that enters or leaves a letter, word or line at a time
+- text with a `blur` or `scale` entrance, or any exit
+- clips turned in 3D with `rotateX` or `rotateY`
+
+A `fade` or a slide of a whole title, a Lottie animation and a 2D transform do not count. Frame size counts by pixels, so a portrait 1080x1920 video counts as 1080p.
+
+Pro costs \$9 a month and includes \$5 of monthly credit. Studio costs \$29 a month and includes \$20 of monthly credit. Their compose discount is a 25% bonus on every credit purchase, so \$25 buys \$31.25 of credit. See [credits and billing](/docs/concepts/credits#plans).
+
+A video above your plan returns `403 PLAN_LIMIT` at submit, before anything renders or is charged. `upgradeTo` in `error.details` names the plan that can render it, or is `null` when only an enterprise plan can. Only video outputs (`mp4`, `webm`, `gif`) are checked. A `jpg`, `png` or `mp3` output renders on every plan.
+
+`GET /account/capabilities` returns your plan's limits: `composeMaxPixels` and `composeMaxFps` for a plain edit, and `composeAnimatedMaxPixels` and `composeAnimatedMaxFps` for one with animated graphics.
+
+## Failed renders
+
+You pay nothing for a render that fails on Rendobar's side, or for a job that never started.
+
+If the failure may come from your input, such as the render tool exiting on a bad file, you pay for the compute the job used at 2x. `GET /jobs/{id}` returns the job's `cost` once it is billed, so you can see what a failed render was charged.
+
+## Check a timeline before you render
+
+`POST /compose/validate` parses a timeline the same way a render does, for free. Call it from your code before you submit. Agents can use the MCP tool `validate_compose`.
+
+
+
+```ts SDK
+const check = await client.compose.validate(params);
+if (!check.valid) console.error(check.problems);
+else console.log(check.price); // { perMinuteUsd: 0.04, minSeconds: 15 }
+```
+
+```bash cURL
+curl -X POST https://api.rendobar.com/compose/validate \
+ -H "Authorization: Bearer rb_YOUR_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "params": { "schemaVersion": 1, "output": { "format": "mp4", "size": "1080p" }, "timeline": { "tracks": [{ "clips": [{ "asset": { "type": "video", "src": "https://example.com/a.mp4" } }] }] } } }'
+```
+
+
+
+The response contains:
+
+| Field | Holds |
+| --- | --- |
+| `valid` | Whether the timeline would be accepted |
+| `problems` | What is wrong, each with the path of its field. A plan limit appears at `["output"]`, with `upgradeTo` set to `"pro"` or `"studio"`, or `null` when only an enterprise plan renders it. |
+| `params` | The timeline as the engine reads it, with every default filled in |
+| `upgraded` | Anything written for an older API version and upgraded to the current shape. Old timelines keep rendering. |
+| `warnings` | Settings the render will ignore or adjust, such as a transition longer than a clip, or a font that falls back |
+| `price` | `{ "perMinuteUsd": 0.04, "minSeconds": 15 }` |
+
+The charge is the video's length in minutes times `perMinuteUsd`, counted by the second, and never less than `minSeconds`.
+
+`GET /compose/effects` lists every built-in effect with its settings. `GET /compose/effects/engine` lists every filter an `engine` entry can name. Build from these live lists instead of a copy. In the SDK, use `client.compose.effects()` and `client.compose.engineEffects()`.
+
+All other job types, [FFmpeg](/docs/jobs/ffmpeg) included, bill per second of compute. See [how a job is priced](/docs/concepts/credits#how-is-a-job-priced).
+
+## See also
+
+- [Output](/docs/jobs/compose/output): formats, size presets, frame rate and quality
+- [Credits and billing](/docs/concepts/credits): balance, credit packs, and when the debit lands
+- [Plan limits](/docs/support/limits): render time, file-size and compose frame caps per plan
diff --git a/jobs/compose/reference/output.mdx b/jobs/compose/reference/output.mdx
index e2f2642..c9d9654 100644
--- a/jobs/compose/reference/output.mdx
+++ b/jobs/compose/reference/output.mdx
@@ -14,11 +14,11 @@ What the render produces. One compose job renders exactly one file.
| Field | Type | What it does |
| --- | --- | --- |
| `format` | `"mp4"` \| `"webm"` \| `"gif"` \| `"mp3"` \| `"jpg"` \| `"png"` | Output container/format. mp4=H.264+AAC, webm=VP9+Opus, gif=animated (no audio), mp3=audio only and needs no size, jpg/png=single frame. default `"mp4"`. |
-| `size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. |
+| `size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. Free renders up to 1080p at 30 fps, Pro 1440p at 30 fps, Studio 4K at 60 fps. |
| `resolution` | object | Explicit output dimensions. Wins over `size`. Every format but mp3 needs this or `size`. |
| `resolution.width` * | integer | Output width in pixels. over 0, max 9007199254740991. |
| `resolution.height` * | integer | Output height in pixels. over 0, max 9007199254740991. |
-| `fps` | number | Output frame rate. Defaults to 30. default `30`, over 0. |
+| `fps` | number | Output frame rate. Defaults to 30. Free renders up to 1080p at 30 fps, Pro 1440p at 30 fps, Studio 4K at 60 fps. default `30`, over 0. |
| `quality` | `"draft"` \| `"standard"` \| `"high"` \| `"max"` | Encoder quality ladder (crf + speed + audio bitrate). draft=fast preview, standard=web delivery (default), high/max=higher fidelity, slower. Explicit `crf`/`encoderPreset` override the ladder. default `"standard"`. |
| `videoCodec` | `"h264"` \| `"vp9"` | The video codec. mp4 takes h264 (default) or vp9. webm is always vp9. gif is always GIF, so it takes none. |
| `audioCodec` | `"aac"` \| `"opus"` \| `"mp3"` | The audio codec. mp4 takes aac (default), opus or mp3. webm is always opus and mp3 always mp3. |
diff --git a/jobs/compose/reference/subtitles.mdx b/jobs/compose/reference/subtitles.mdx
index 6a05021..6c23a9a 100644
--- a/jobs/compose/reference/subtitles.mdx
+++ b/jobs/compose/reference/subtitles.mdx
@@ -17,18 +17,30 @@ Set `asset.type` to `"subtitles"`. These are its own fields, on top of the [clip
| `cues.start` * | number | When the line appears, in seconds from the start of THIS clip. min 0. |
| `cues.end` * | number | When the line goes, in seconds from the start of this clip. over 0. |
| `cues.text` * | string | The line. Keep it to two short rows. |
-| `src` | string | An .srt or .vtt file with the lines, instead of `cues`: what a transcription service, or the caption.extract job, gives you. Its times count from the start of the clip. |
+| `src` | string | An .srt or .vtt file with the lines, instead of `cues`: what a transcription service gives you. Its times count from the start of the clip. |
| `reveal` | `"line"` \| `"typewriter"` | line shows each cue at once, typewriter types it out word by word, the social-video style. default `"line"`. |
-| `style` * | object | |
+| `style` | object | default `{}`. |
| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
-| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.size` | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. Defaults to 64. default `64`, over 0. |
| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.stroke.color` * | string | Outline color, hex like "#000000". pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke.width` * | number | Outline width in pixels. 2 to 4 keeps text readable over busy footage. min 0. |
| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
-| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.animate` | object | How the text enters: \{"type":"fade"\} or one of the slides. A split (`by`), a blur or scale entrance and an exit (`out`) are not available here, because the text changes as it plays. |
+| `style.animate.type` * | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text arrives: fade in place, slide in from the named direction while fading, blur comes into focus, scale grows from small. |
+| `style.animate.duration` | number | Seconds the whole entrance takes. With `by`, the last unit lands at this time. 0.4 to 0.8 reads polished for one piece of text, 0.8 to 1.5 for a split title. default `0.5`, over 0. |
+| `style.animate.by` | `"letter"` \| `"word"` \| `"line"` | Animate the text one letter, word or line at a time: kinetic titles and lower thirds. Omit it and the text moves as one piece. |
+| `style.animate.stagger` | number | Seconds between one unit starting and the next. Omit it and the units spread evenly over the first 40% of `duration`. min 0. |
+| `style.animate.order` | `"start"` \| `"end"` \| `"center"` \| `"edges"` \| `"random"` | Which unit starts first. center and edges start units at the same distance together. random is seeded by the clip, so a re-render is identical. default `"start"`. |
+| `style.animate.out` | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text leaves over the clip's last `duration` seconds, with the same split and order. Omit it to hold until the clip ends. |
| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+| `style.shadow.color` | string | Shadow color, hex. default `"#000000"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.shadow.opacity` | number | Shadow opacity 0..1. default `0.6`, min 0, max 1. |
+| `style.shadow.offsetX` | number | Horizontal shadow offset in pixels. default `4`. |
+| `style.shadow.offsetY` | number | Vertical shadow offset in pixels. default `4`. |
Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/text.mdx b/jobs/compose/reference/text.mdx
index 67a6f82..fc726d5 100644
--- a/jobs/compose/reference/text.mdx
+++ b/jobs/compose/reference/text.mdx
@@ -14,16 +14,28 @@ Set `asset.type` to `"text"`. These are its own fields, on top of the [clip fiel
| Field | Type | What it does |
| --- | --- | --- |
| `text` * | string | The text to render. Keep titles short, 2-6 words reads best. |
-| `style` * | object | |
+| `style` | object | default `{}`. |
| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
-| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.size` | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. Defaults to 64. default `64`, over 0. |
| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.stroke.color` * | string | Outline color, hex like "#000000". pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke.width` * | number | Outline width in pixels. 2 to 4 keeps text readable over busy footage. min 0. |
| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
-| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.animate` | object | How the text enters, and optionally leaves: \{"type":"slideUp","by":"word"\} brings a title in word by word. A split (`by`), a blur or scale entrance and an exit (`out`) render up to 1080p at 30 fps on self-serve plans. Omit for text that is present from its first frame to its last. |
+| `style.animate.type` * | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text arrives: fade in place, slide in from the named direction while fading, blur comes into focus, scale grows from small. |
+| `style.animate.duration` | number | Seconds the whole entrance takes. With `by`, the last unit lands at this time. 0.4 to 0.8 reads polished for one piece of text, 0.8 to 1.5 for a split title. default `0.5`, over 0. |
+| `style.animate.by` | `"letter"` \| `"word"` \| `"line"` | Animate the text one letter, word or line at a time: kinetic titles and lower thirds. Omit it and the text moves as one piece. |
+| `style.animate.stagger` | number | Seconds between one unit starting and the next. Omit it and the units spread evenly over the first 40% of `duration`. min 0. |
+| `style.animate.order` | `"start"` \| `"end"` \| `"center"` \| `"edges"` \| `"random"` | Which unit starts first. center and edges start units at the same distance together. random is seeded by the clip, so a re-render is identical. default `"start"`. |
+| `style.animate.out` | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text leaves over the clip's last `duration` seconds, with the same split and order. Omit it to hold until the clip ends. |
| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+| `style.shadow.color` | string | Shadow color, hex. default `"#000000"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.shadow.opacity` | number | Shadow opacity 0..1. default `0.6`, min 0, max 1. |
+| `style.shadow.offsetX` | number | Horizontal shadow offset in pixels. default `4`. |
+| `style.shadow.offsetY` | number | Vertical shadow offset in pixels. default `4`. |
Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/timeline.mdx b/jobs/compose/reference/timeline.mdx
index f50b640..d2bdda7 100644
--- a/jobs/compose/reference/timeline.mdx
+++ b/jobs/compose/reference/timeline.mdx
@@ -15,18 +15,22 @@ The shape of a compose request, from the outside in. Each asset kind adds its ow
| Field | Type | What it does |
| --- | --- | --- |
-| `schemaVersion` * | `1` | Timeline schema version. Always 1. |
-| `output` * | object | |
+| `schemaVersion` | `1` | Timeline schema version. Always 1, and assumed when omitted. default `1`. |
+| `output` | object | Output file. Omit for a 1080p mp4 at 30 fps. default `{"size":"1080p"}`. |
| `output.format` | `"mp4"` \| `"webm"` \| `"gif"` \| `"mp3"` \| `"jpg"` \| `"png"` | Output container/format. mp4=H.264+AAC, webm=VP9+Opus, gif=animated (no audio), mp3=audio only and needs no size, jpg/png=single frame. default `"mp4"`. |
-| `output.size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. |
+| `output.size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. Free renders up to 1080p at 30 fps, Pro 1440p at 30 fps, Studio 4K at 60 fps. |
| `output.resolution` | object | Explicit output dimensions. Wins over `size`. Every format but mp3 needs this or `size`. |
-| `output.fps` | number | Output frame rate. Defaults to 30. default `30`, over 0. |
+| `output.resolution.width` * | integer | Output width in pixels. over 0, max 9007199254740991. |
+| `output.resolution.height` * | integer | Output height in pixels. over 0, max 9007199254740991. |
+| `output.fps` | number | Output frame rate. Defaults to 30. Free renders up to 1080p at 30 fps, Pro 1440p at 30 fps, Studio 4K at 60 fps. default `30`, over 0. |
| `output.quality` | `"draft"` \| `"standard"` \| `"high"` \| `"max"` | Encoder quality ladder (crf + speed + audio bitrate). draft=fast preview, standard=web delivery (default), high/max=higher fidelity, slower. Explicit `crf`/`encoderPreset` override the ladder. default `"standard"`. |
| `output.videoCodec` | `"h264"` \| `"vp9"` | The video codec. mp4 takes h264 (default) or vp9. webm is always vp9. gif is always GIF, so it takes none. |
| `output.audioCodec` | `"aac"` \| `"opus"` \| `"mp3"` | The audio codec. mp4 takes aac (default), opus or mp3. webm is always opus and mp3 always mp3. |
| `output.crf` | integer | Advanced: constant rate factor (lower = higher quality/larger file). Overrides `quality`. h264 accepts 0-51, vp9 0-63. Does not apply to gif/mp3/jpg/png. min 0, max 63. |
| `output.encoderPreset` | `"ultrafast"` \| `"superfast"` \| `"veryfast"` \| `"faster"` \| `"fast"` \| `"medium"` \| `"slow"` \| `"slower"` \| `"veryslow"` | Advanced: x264 speed/compression preset (slower = smaller file, same quality). Overrides `quality`. h264 only. |
| `output.range` | object | Which part of the timeline to render: a preview, or one section checked without paying for the whole render. A jpg or png is the single frame at `start`, the first frame when there is no range. |
+| `output.range.start` | number | Second of the timeline to start from. For a jpg or png, the moment captured. default `0`, min 0. |
+| `output.range.length` | number | Seconds to render. Leave it out to run to the end. A range past the end stops at the end. A jpg or png is one frame and ignores it. over 0. |
| `timeline` * | object | The edit itself, in ONE of two shapes: \{ tracks \} for precise multi-track control (layers, exact timing), or \{ scenes, overlays \} for sequential scene-by-scene authoring. Not both. |
| `prompt` | string | The natural-language request this timeline was generated from, when AI assist was used. |
| `assets` | string[] | The asset URLs supplied to AI assist, when it was used. |
@@ -40,23 +44,56 @@ The shape of a compose request, from the outside in. Each asset kind adds its ow
| `tracks` | object[] | Layer stack: the FIRST track is the background, each later track composites on top (overlays, picture-in-picture, titles). Within a track, clips play in time; between two clips a transition item blends them. |
| `tracks.name` | string | Optional label for your own reference, not rendered. |
| `tracks.effects` | object[] | Effects on the whole track, after its clips are laid out: grade every clip on it at once, or put a limiter on a music track. |
-| `tracks.clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
+| `tracks.clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Two clips that overlap with no transition between them crossfade over the overlap, and the job says which in the render step's `clipOverlaps`. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
| `scenes` | object[] | Sequential segments played one after another, each with its own clips and an optional transition into it. |
| `scenes.duration` | number | Scene length in seconds. Omit to take it from the longest thing inside: a clip's own length, or the source in it. over 0. |
| `scenes.transition` | object | The transition INTO this scene from the previous one. Omit on the first scene and for hard cuts. |
+| `scenes.transition.transition` * | `"crossfade"` \| `"fade"` \| `"slide"` \| `"push"` \| `"wipe"` \| `"zoom"` \| `"none"` | crossfade dissolves A into B, fade dips through black, slide moves B in over A, push moves B in and A out together, wipe reveals B in place along an edge or a shape, zoom grows B from the center, none is a hard cut. |
+| `scenes.transition.direction` | `"left"` \| `"right"` \| `"up"` \| `"down"` | slide, push and an edge wipe: which way the picture travels. Default left. |
+| `scenes.transition.shape` | `"edge"` \| `"circle"` \| `"box"` \| `"barnDoorHorizontal"` \| `"barnDoorVertical"` | wipe only. edge (default) sweeps a straight line across in `direction`, circle opens an iris from the centre, box opens a rectangle from the centre, barnDoorVertical opens a vertical gap from the middle out to the sides, barnDoorHorizontal opens a horizontal gap up and down. |
+| `scenes.transition.map` | string | wipe only: a wipe of your own, drawn by a greyscale image (png or jpg) at the frame's shape. Black turns over to the next clip first and white last, so a left-to-right gradient is an edge wipe and a radial one an iris. Takes no `shape` or `direction`. |
+| `scenes.transition.duration` * | number | Overlap in seconds. 0.4-0.8 is the professional range. Leave the next clip's `start` out and it is placed this far before the clip before it ends. Two clips placed by hand blend over the overlap they have. over 0. |
| `scenes.clips` * | object[] | Clips layered within this scene: first is the background, later clips composite on top. |
+| `scenes.clips.asset` * | object | |
+| `scenes.clips.start` | number | Seconds into the scene. Omit to start at the scene's beginning. A clip is measured from its own start, so one placed 2s into a 5s scene has 3s of room. min 0. |
+| `scenes.clips.length` | number \| `"end"` | Seconds. Omit it and the clip runs as long as its content does, never past the scene. Use "end" to span the scene whatever the content is, which is also what omitting means for text and images. |
+| `scenes.clips.opacity` | number \| object[] | Clip opacity: a constant 0..1, or keyframes [\{time, value, easing\}] for animated fades. Fade a clip in with [\{time:0,value:0\},\{time:1,value:1\}]. default `1`. |
+| `scenes.clips.transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns). Omit for full-frame. A 3D turn (rotateX, rotateY) works only in `overlays`, not inside a scene. |
+| `scenes.clips.crop` | object | Trim edges off the frame by percent. The kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `scenes.clips.speed` | number | Playback rate: 0.5 = slow motion, 2 = timelapse. Pitch shifts with speed. Not allowed on composition clips. Avoid slow motion on the OUTGOING side of a transition (it can run out of frames). over 0. |
+| `scenes.clips.effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(filter, params): any other filter by name, strictly typed ones only. |
+| `scenes.clips.blendMode` | `"normal"` \| `"multiply"` \| `"screen"` \| `"overlay"` \| `"darken"` \| `"lighten"` \| `"add"` \| `"difference"` | How this clip mixes with the layer below: screen for light leaks/glows, multiply for darkening textures, overlay for contrast looks, add/lighten/darken/difference for effects. Meaningful on overlay tracks. |
+| `scenes.clips.pan` | number | Stereo balance: -1 full left, 0 centered, 1 full right. min -1, max 1. |
+| `scenes.clips.flip` | object | Mirror the clip horizontally and/or vertically. |
| `overlays` | object[] | Clips composited over the WHOLE video regardless of scenes: a logo, a watermark, a persistent title. The whole video is their container, so omitting `length` spans it. `start` defaults to 0. |
| `overlays.asset` * | object | |
| `overlays.start` | number | When the clip begins, in seconds from the start of the video. Omit it and the clip starts after everything already placed on its track, pulled back by any transition sitting immediately before it. The first clip on a track starts at 0. min 0. |
| `overlays.length` | number \| `"end"` | How long the clip plays. Omit it and the clip runs as long as its CONTENT does: the trim span, the source duration divided by speed, or a composition's own end. Use "end" and it runs to the end of the video instead, whatever the source says, which is what a music bed wants. Text and images have no duration of their own, so omitting is the same as "end" for them and a title with no length stays up. A clip that runs to the end must be the last on its track. A video, audio or animation clip that runs past the end of its file holds the last frame, silent, until the clip ends, and the job says which clip in the render step's `clipOverruns`. |
| `overlays.opacity` | number \| object[] | Clip opacity: a constant 0..1, or keyframes [\{time, value, easing\}] for animated fades. Fade a clip in with [\{time:0,value:0\},\{time:1,value:1\}]. default `1`. |
-| `overlays.transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns). Omit for full-frame. |
-| `overlays.crop` | object | Trim edges off the frame by percent; the kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `overlays.transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns), or turn it in 3D with rotateX and rotateY (rendered up to 1080p at 30 fps on self-serve plans). Omit for full-frame. |
+| `overlays.transform.position` | object \| object[] | Where to place the clip, in pixels like \{x:1600,y:900\} or percent like \{x:"75%",y:"75%"\}, or keyframes of them to move it. Omit it and the clip is centred, so a scale of 0.5 alone is a centred picture-in-picture. |
+| `overlays.transform.scale` | number \| object[] | Size relative to the frame: 1 = full frame, 0.3 = a corner PiP, above 1 zooms in (crops edges). Keyframes animate it: [\{time:"0%",value:1,easing:"easeInOutSine"\},\{time:"100%",value:1.15\}] is a Ken Burns push-in. A keyframe's easing shapes the move into the next one. default `1`. |
+| `overlays.transform.rotate` | number \| object[] | Rotation in degrees, clockwise. Keyframes spin it. default `0`. |
+| `overlays.transform.rotateX` | number \| object[] | Tilt about the horizontal axis, in degrees: a number, or keyframes to turn it over time. [\{time:0,value:60\},\{time:0.8,value:0\}] tips the clip up from lying flat. |
+| `overlays.transform.rotateY` | number \| object[] | Turn about the vertical axis, in degrees: a number, or keyframes to turn it over time. [\{time:0,value:-90\},\{time:0.8,value:0\}] swings the clip in like a door opening toward you. |
+| `overlays.transform.perspective` | number | Distance of the viewer from the frame in pixels. Smaller is a stronger 3D effect: 600 is dramatic, 2000 is subtle. Omit for 1200. over 0. |
+| `overlays.transform.anchor` | `"center"` \| `"topLeft"` | What `position` points at: the clip's center (default) or its top-left corner. It is also what the clip turns about. With no position the clip is centred either way. On text, x follows style.align, so anchor only decides whether y is the text's middle or its top edge. default `"center"`. |
+| `overlays.transform.fit` | `"contain"` \| `"cover"` \| `"fill"` \| `"blur"` | How a picture fills its box when their shapes differ: contain shows all of it, cover fills the box and cuts what overflows (a vertical phone clip in a 16:9 frame with no bars), fill stretches it, blur shows all of it over an enlarged, blurred copy that fills the bars, for a clip that fills the frame. The box is `size`, or the frame. default `"contain"`. |
+| `overlays.transform.size` | object \| object[] | The clip's box, in pixels like \{width:480,height:480\} or a percent of the frame. Omit it and the box is the whole frame. A picture fills it the way `fit` says: \{width:400,height:400\} with fit "cover" is a square cut from 16:9 footage. A shape is drawn to it: \{width:"100%",height:"12%"\} is a lower-third bar. Give one side and the clip keeps its own shape, which for a shape is SQUARE IN PIXELS, the only way to a true circle on a 16:9 frame. `scale` then multiplies the box, for zooms. Keyframes resize it: a width from "0%" to "80%" is a progress bar. |
+| `overlays.transform.corners` | object \| object[] | Pins the clip's four corners to four points, in pixels or percent: a picture on a screen, a sign seen in perspective. It is the other form of a transform, so it is not combined with position, scale, rotate or fit. A corner may sit up to a frame beyond each edge. Keyframes move the corners. |
+| `overlays.crop` | object | Trim edges off the frame by percent. The kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `overlays.crop.top` | string | Percent of the frame height to remove from the top. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `overlays.crop.right` | string | Percent of the frame width to remove from the right. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `overlays.crop.bottom` | string | Percent of the frame height to remove from the bottom. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `overlays.crop.left` | string | Percent of the frame width to remove from the left. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `overlays.crop.radius` | number | Rounds the corners of what is kept, 0 to 1. 1 rounds the shorter side fully, a pill. The corners ride the transform, so a scaled-down clip becomes a rounded picture-in-picture. The kept part fills the frame, so radius 1 is only a circle on a square frame. min 0, max 1. |
| `overlays.speed` | number | Playback rate: 0.5 = slow motion, 2 = timelapse. Pitch shifts with speed. Not allowed on composition clips. Avoid slow motion on the OUTGOING side of a transition (it can run out of frames). over 0. |
-| `overlays.effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(service, params): any other engine filter by the engine's own names, strictly typed ones only. |
+| `overlays.effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(filter, params): any other filter by name, strictly typed ones only. |
| `overlays.blendMode` | `"normal"` \| `"multiply"` \| `"screen"` \| `"overlay"` \| `"darken"` \| `"lighten"` \| `"add"` \| `"difference"` | How this clip mixes with the layer below: screen for light leaks/glows, multiply for darkening textures, overlay for contrast looks, add/lighten/darken/difference for effects. Meaningful on overlay tracks. |
| `overlays.pan` | number | Stereo balance: -1 full left, 0 centered, 1 full right. min -1, max 1. |
| `overlays.flip` | object | Mirror the clip horizontally and/or vertically. |
+| `overlays.flip.horizontal` | boolean | Mirror left-right. default `false`. |
+| `overlays.flip.vertical` | boolean | Mirror top-bottom. default `false`. |
## Track
@@ -64,7 +101,7 @@ The shape of a compose request, from the outside in. Each asset kind adds its ow
| --- | --- | --- |
| `name` | string | Optional label for your own reference, not rendered. |
| `effects` | object[] | Effects on the whole track, after its clips are laid out: grade every clip on it at once, or put a limiter on a music track. |
-| `clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
+| `clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Two clips that overlap with no transition between them crossfade over the overlap, and the job says which in the render step's `clipOverlaps`. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
## Clip
@@ -75,22 +112,33 @@ Every clip takes these, whatever its asset.
| `start` | number | When the clip begins, in seconds from the start of the video. Omit it and the clip starts after everything already placed on its track, pulled back by any transition sitting immediately before it. The first clip on a track starts at 0. min 0. |
| `length` | number \| `"end"` | How long the clip plays. Omit it and the clip runs as long as its CONTENT does: the trim span, the source duration divided by speed, or a composition's own end. Use "end" and it runs to the end of the video instead, whatever the source says, which is what a music bed wants. Text and images have no duration of their own, so omitting is the same as "end" for them and a title with no length stays up. A clip that runs to the end must be the last on its track. A video, audio or animation clip that runs past the end of its file holds the last frame, silent, until the clip ends, and the job says which clip in the render step's `clipOverruns`. |
| `opacity` | number \| object[] | Clip opacity: a constant 0..1, or keyframes [\{time, value, easing\}] for animated fades. Fade a clip in with [\{time:0,value:0\},\{time:1,value:1\}]. default `1`. |
-| `transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns). Omit for full-frame. |
+| `transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns), or turn it in 3D with rotateX and rotateY (rendered up to 1080p at 30 fps on self-serve plans). Omit for full-frame. |
| `transform.position` | object \| object[] | Where to place the clip, in pixels like \{x:1600,y:900\} or percent like \{x:"75%",y:"75%"\}, or keyframes of them to move it. Omit it and the clip is centred, so a scale of 0.5 alone is a centred picture-in-picture. |
+| `transform.position.x` * | number \| string | Horizontal: pixels from the left edge of the output frame, like 960, or a percent of its width, like "50%". |
+| `transform.position.y` * | number \| string | Vertical: pixels from the top edge, like 540, or a percent of the frame height, like "80%". |
| `transform.scale` | number \| object[] | Size relative to the frame: 1 = full frame, 0.3 = a corner PiP, above 1 zooms in (crops edges). Keyframes animate it: [\{time:"0%",value:1,easing:"easeInOutSine"\},\{time:"100%",value:1.15\}] is a Ken Burns push-in. A keyframe's easing shapes the move into the next one. default `1`. |
| `transform.rotate` | number \| object[] | Rotation in degrees, clockwise. Keyframes spin it. default `0`. |
+| `transform.rotateX` | number \| object[] | Tilt about the horizontal axis, in degrees: a number, or keyframes to turn it over time. [\{time:0,value:60\},\{time:0.8,value:0\}] tips the clip up from lying flat. |
+| `transform.rotateY` | number \| object[] | Turn about the vertical axis, in degrees: a number, or keyframes to turn it over time. [\{time:0,value:-90\},\{time:0.8,value:0\}] swings the clip in like a door opening toward you. |
+| `transform.perspective` | number | Distance of the viewer from the frame in pixels. Smaller is a stronger 3D effect: 600 is dramatic, 2000 is subtle. Omit for 1200. over 0. |
| `transform.anchor` | `"center"` \| `"topLeft"` | What `position` points at: the clip's center (default) or its top-left corner. It is also what the clip turns about. With no position the clip is centred either way. On text, x follows style.align, so anchor only decides whether y is the text's middle or its top edge. default `"center"`. |
| `transform.fit` | `"contain"` \| `"cover"` \| `"fill"` \| `"blur"` | How a picture fills its box when their shapes differ: contain shows all of it, cover fills the box and cuts what overflows (a vertical phone clip in a 16:9 frame with no bars), fill stretches it, blur shows all of it over an enlarged, blurred copy that fills the bars, for a clip that fills the frame. The box is `size`, or the frame. default `"contain"`. |
| `transform.size` | object \| object[] | The clip's box, in pixels like \{width:480,height:480\} or a percent of the frame. Omit it and the box is the whole frame. A picture fills it the way `fit` says: \{width:400,height:400\} with fit "cover" is a square cut from 16:9 footage. A shape is drawn to it: \{width:"100%",height:"12%"\} is a lower-third bar. Give one side and the clip keeps its own shape, which for a shape is SQUARE IN PIXELS, the only way to a true circle on a 16:9 frame. `scale` then multiplies the box, for zooms. Keyframes resize it: a width from "0%" to "80%" is a progress bar. |
+| `transform.size.width` | number \| string | Width: pixels of the output frame, like 480, or a percent of its width, like "25%". |
+| `transform.size.height` | number \| string | Height: pixels, or a percent of the frame height. |
| `transform.corners` | object \| object[] | Pins the clip's four corners to four points, in pixels or percent: a picture on a screen, a sign seen in perspective. It is the other form of a transform, so it is not combined with position, scale, rotate or fit. A corner may sit up to a frame beyond each edge. Keyframes move the corners. |
-| `crop` | object | Trim edges off the frame by percent; the kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `transform.corners.topLeft` * | object | |
+| `transform.corners.topRight` * | object | |
+| `transform.corners.bottomRight` * | object | |
+| `transform.corners.bottomLeft` * | object | |
+| `crop` | object | Trim edges off the frame by percent. The kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
| `crop.top` | string | Percent of the frame height to remove from the top. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
| `crop.right` | string | Percent of the frame width to remove from the right. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
| `crop.bottom` | string | Percent of the frame height to remove from the bottom. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
| `crop.left` | string | Percent of the frame width to remove from the left. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
| `crop.radius` | number | Rounds the corners of what is kept, 0 to 1. 1 rounds the shorter side fully, a pill. The corners ride the transform, so a scaled-down clip becomes a rounded picture-in-picture. The kept part fills the frame, so radius 1 is only a circle on a square frame. min 0, max 1. |
| `speed` | number | Playback rate: 0.5 = slow motion, 2 = timelapse. Pitch shifts with speed. Not allowed on composition clips. Avoid slow motion on the OUTGOING side of a transition (it can run out of frames). over 0. |
-| `effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(service, params): any other engine filter by the engine's own names, strictly typed ones only. |
+| `effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(filter, params): any other filter by name, strictly typed ones only. |
| `blendMode` | `"normal"` \| `"multiply"` \| `"screen"` \| `"overlay"` \| `"darken"` \| `"lighten"` \| `"add"` \| `"difference"` | How this clip mixes with the layer below: screen for light leaks/glows, multiply for darkening textures, overlay for contrast looks, add/lighten/darken/difference for effects. Meaningful on overlay tracks. |
| `pan` | number | Stereo balance: -1 full left, 0 centered, 1 full right. min -1, max 1. |
| `flip` | object | Mirror the clip horizontally and/or vertically. |
diff --git a/jobs/compose/reference/timer.mdx b/jobs/compose/reference/timer.mdx
index 8e21bb1..a82adab 100644
--- a/jobs/compose/reference/timer.mdx
+++ b/jobs/compose/reference/timer.mdx
@@ -15,16 +15,28 @@ Set `asset.type` to `"timer"`. These are its own fields, on top of the [clip fie
| --- | --- | --- |
| `direction` | `"up"` \| `"down"` | up counts from zero, down counts to zero from the clip's length. default `"up"`. |
| `format` | `"SS"` \| `"SS.S"` \| `"SS.SS"` \| `"MM:SS"` \| `"MM:SS.SS"` \| `"HH:MM:SS"` | What the clock shows. SS.SS is a race clock, MM:SS a countdown to a launch, HH:MM:SS a stream timer. default `"MM:SS"`. |
-| `style` * | object | |
+| `style` | object | default `{}`. |
| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
-| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.size` | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. Defaults to 64. default `64`, over 0. |
| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.stroke.color` * | string | Outline color, hex like "#000000". pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke.width` * | number | Outline width in pixels. 2 to 4 keeps text readable over busy footage. min 0. |
| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
-| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.animate` | object | How the text enters: \{"type":"fade"\} or one of the slides. A split (`by`), a blur or scale entrance and an exit (`out`) are not available here, because the text changes as it plays. |
+| `style.animate.type` * | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text arrives: fade in place, slide in from the named direction while fading, blur comes into focus, scale grows from small. |
+| `style.animate.duration` | number | Seconds the whole entrance takes. With `by`, the last unit lands at this time. 0.4 to 0.8 reads polished for one piece of text, 0.8 to 1.5 for a split title. default `0.5`, over 0. |
+| `style.animate.by` | `"letter"` \| `"word"` \| `"line"` | Animate the text one letter, word or line at a time: kinetic titles and lower thirds. Omit it and the text moves as one piece. |
+| `style.animate.stagger` | number | Seconds between one unit starting and the next. Omit it and the units spread evenly over the first 40% of `duration`. min 0. |
+| `style.animate.order` | `"start"` \| `"end"` \| `"center"` \| `"edges"` \| `"random"` | Which unit starts first. center and edges start units at the same distance together. random is seeded by the clip, so a re-render is identical. default `"start"`. |
+| `style.animate.out` | `"fade"` \| `"slideUp"` \| `"slideDown"` \| `"slideLeft"` \| `"slideRight"` \| `"blur"` \| `"scale"` | How the text leaves over the clip's last `duration` seconds, with the same split and order. Omit it to hold until the clip ends. |
| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+| `style.shadow.color` | string | Shadow color, hex. default `"#000000"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.shadow.opacity` | number | Shadow opacity 0..1. default `0.6`, min 0, max 1. |
+| `style.shadow.offsetX` | number | Horizontal shadow offset in pixels. default `4`. |
+| `style.shadow.offsetY` | number | Vertical shadow offset in pixels. default `4`. |
Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/shapes.mdx b/jobs/compose/shapes.mdx
index e47a670..149dcf1 100644
--- a/jobs/compose/shapes.mdx
+++ b/jobs/compose/shapes.mdx
@@ -1,15 +1,13 @@
---
title: "Shapes and masks"
sidebarTitle: "Shapes"
-description: "Render rectangles, circles, paths and masks with fills, outlines, corners and shadows, and see which pairs are refused."
+description: "Add rectangles, circles, paths and masks with fills, outlines, corners and shadows, and see which combinations the API refuses."
icon: "shapes"
keywords: ["video shape overlay api", "video mask api", "lower third bar api"]
canonical: "https://rendobar.com/docs/jobs/compose/shapes"
---
-
-
-A `shape` asset draws a rect, ellipse, circle, triangle, diamond, or any SVG path you paste in. No image to make, no file to host, and it stays sharp at any output resolution.
+A `shape` asset draws a rect, ellipse, circle, triangle, diamond or any SVG path. You need no image file, and the shape stays sharp at any output resolution.
```json
{
@@ -18,11 +16,11 @@ A `shape` asset draws a rect, ellipse, circle, triangle, diamond, or any SVG pat
}
```
-That is a bar across the lower third, dark enough to read a title on and thin enough to see the footage through. Put it on a track below the text.
+This is a see-through bar across the lower third to hold a title. Put it on a track below the text.
-A shape is placed the way every clip is placed: by the clip's `transform`. `size` is its box, `position` its centre unless `anchor` says otherwise, and `rotate` turns it about that anchor. The asset itself only says what to draw.
+The clip's `transform` places a shape like any other clip. `size` is its box, `position` is its centre unless `anchor` says otherwise, and `rotate` turns it about that anchor. The asset only says what to draw.
-With no `size` a shape fills the frame, which is a scrim or a background in one line. Like text it has no duration of its own, so leaving `length` off runs it to the end of its container.
+With no `size`, a shape fills the frame, which gives you a tint or a background in one line. Like text, it has no duration of its own, so without `length` it runs to the end of its container.
`width` is a percent of the frame's width and `height` a percent of its height. Both also take pixels as plain numbers.
@@ -43,70 +41,11 @@ With no `size` a shape fills the frame, which is a scrim or a background in one
-## Fills and edges
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Paths
-
-
-
-
-
-
-
-
-
-
## Leave the height out to get a circle
+Omit `size.height` and the box is square in pixels. This is the only way to get a true circle or square on a frame that is not square.
-Omit `size.height` and the box is made **square in pixels**. That is the only way to get a true circle or square on a frame that is not square, and it is the one thing about shapes worth memorising.
-
-A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels. Rounding it fully gives a pill, not a circle. Omit the height and the engine derives 17.8% for you.
+A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels, so rounding it fully gives a pill. Omit the height and the engine works out 17.8% for you.
```json
{
@@ -115,12 +54,11 @@ A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels. Rounding it fully giv
}
```
-`shape: "circle"` is `ellipse` with that rule applied, so it rejects an explicit height rather than quietly ignoring one. The same omission works on every kind: a `rect` with no height is a square, a `diamond` with no height is a regular one.
+`shape: "circle"` is an `ellipse` that always uses this rule, so it rejects a height if you set one. Leaving out the height works on every kind: a `rect` becomes a square and a `diamond` becomes a regular one.
## Anchor
-
-`anchor: "topLeft"` points `position` at the box's corner instead of its centre. It is what makes letterbox bars arithmetic-free.
+`anchor: "topLeft"` points `position` at the box's corner instead of its centre. With it, you place letterbox bars without any maths.
```json
{
@@ -131,12 +69,11 @@ A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels. Rounding it fully giv
## Fills, flat and gradient
+`color` takes `#RRGGBB`, or the 8-digit `#RRGGBBAA` for constant transparency, as `#0A0A0ACC` does above.
-`color` takes `#RRGGBB`, or the 8-digit `#RRGGBBAA` for a constant translucency the way `#0A0A0ACC` does above.
-
-Do not reach for the clip's `opacity` instead. `opacity` fades the whole clip, so keep it for fades and put a constant translucency in the colour.
+Keep the clip's `opacity` for fades, since it fades the whole clip. Put constant transparency in the colour.
-Give `color` **two** colours and it is a gradient. `gradient` names the direction: `vertical` (the default), `horizontal`, `diagonal` or `radial`.
+Give `color` two colours to make a gradient. `gradient` sets the direction: `vertical` (the default), `horizontal`, `diagonal` or `radial`.
```json
{
@@ -145,14 +82,22 @@ Give `color` **two** colours and it is a gradient. `gradient` names the directio
}
```
-The gradient maps to the **shape**, not the frame, so a small badge shows the whole ramp instead of a sliver of one. Stops take the 8-digit form too, which is how you get a scrim that fades to nothing: `["#0A0A0A00", "#0A0A0AFF"]`.
+The gradient spans the shape's own box, so a small badge shows the whole gradient. Stops take the 8-digit form too, so you can make a dark overlay that fades to nothing: `["#0A0A0A00", "#0A0A0AFF"]`.
-## Outlines, with a real hole
+
+
+
+
+
+
+
+
+## Outlines, with a real hole
-`stroke` draws a border just inside the shape's edge, its `width` in pixels of the output frame. Give the shape a `color` too and you get a filled shape with a border; `radius` rounds both together.
+`stroke` draws a border along the inside of the shape's edge, with `width` in output pixels. Add a `color` for a filled shape with a border. `radius` rounds both.
-Leave `color` out and only the outline draws. What it surrounds is genuinely transparent, not filled with black, so the footage shows through the middle. That works on every kind except a path.
+Leave out `color` and only the outline draws, with a transparent middle that shows the footage. This works on every kind except a path.
```json
{
@@ -161,15 +106,31 @@ Leave `color` out and only the outline draws. What it surrounds is genuinely tra
}
```
-## Rounded corners
+
+
+
+
+
+
+
+
+## Rounded corners
`radius` runs from `0` (square) to `1` (fully round) on a `rect`. At `1` a wide box becomes a pill and a square one becomes a circle.
-## Drop shadows
+
+
+
+
+
+
+
+
+## Drop shadows
-`shadow` follows the shape, including its outline, its rotation and its animation. Every field has a default, so `"shadow": {}` is a sensible shadow.
+`shadow` follows the shape, including its outline, rotation and animation. Every field has a default, so `"shadow": {}` gives a usable shadow.
```json
{
@@ -184,10 +145,18 @@ Leave `color` out and only the outline draws. What it surrounds is genuinely tra
}
```
-## Rotation turns about the anchor, not the frame
+
+
+
+
+
+
+
+
+## Rotation turns about the anchor, not the frame
-`transform.rotate` is degrees clockwise about the shape's own anchor, so it spins where it sits rather than orbiting the middle of the frame. A diagonal rule is a thin rect and one field:
+`transform.rotate` turns the shape clockwise in degrees about its own anchor, so it spins in place.
```json
{
@@ -196,17 +165,24 @@ Leave `color` out and only the outline draws. What it surrounds is genuinely tra
}
```
-There is no `line` type for the same reason there is no `square` type: a line is a rect with a small height, and `rotate` is what makes it diagonal.
+There is no `line` or `square` type. A line is a rect with a small height, and `rotate` makes it diagonal, as above.
## Softness
+`softness` runs from 0 to 1 and softens the edge. At 0.2 to 0.4, an ellipse looks like a spotlight or a vignette. A full-frame ellipse with `softness: 0.5` and a radial `["#00000000", "#000000CC"]` fill gives you a vignette with no image asset.
-`softness` runs 0 to 1 and feathers the edge. At 0.2 to 0.4 an ellipse reads as a spotlight or a vignette rather than a hard-edged disc. A full-frame ellipse with `softness: 0.5` and a radial `["#00000000", "#000000CC"]` fill is a vignette with no image asset at all.
+
+
+
+
+
+
+
+
## Paths
-
-`shape: "path"` takes SVG path data straight out of a design tool. `viewBox` gives the coordinate space it was drawn in, defaulting to `"0 0 100 100"`, and the path is scaled into the transform's `size` so the same data works at any size on any frame.
+`shape: "path"` takes SVG path data as a design tool exports it. `viewBox` sets the coordinate space it was drawn in and defaults to `"0 0 100 100"`. The engine scales the path to the transform's `size`, so the same data works at any size on any frame.
```json
{
@@ -221,12 +197,20 @@ There is no `line` type for the same reason there is no `square` type: a line is
}
```
-`M L H V C S Q T Z` are supported, absolute and relative. Elliptical arcs (`A`) are refused rather than approximated, so re-export with arcs converted to curves. A path takes a fill, a gradient, a shadow, rotation and softness, but not a `stroke`: outlining arbitrary path data needs true path offsetting.
+`M L H V C S Q T Z` work in absolute and relative form. Elliptical arcs (`A`) are rejected, so re-export with arcs converted to curves. A path takes a fill, a gradient, a shadow, rotation and softness, but not a `stroke`.
-## Animation
+
+
+
+
+
+
+
+
+## Animation
-Everything that moves is [keyframes](#keyframes): `position`, `size` and `rotate` on the transform, `color` and `radius` on the shape. A progress bar is a width that grows.
+You animate a shape with [keyframes](/docs/jobs/compose/motion) on `position`, `size` and `rotate` in the transform, and on `color` and `radius` in the shape. For a progress bar, grow the width.
```json
{
@@ -242,26 +226,24 @@ Everything that moves is [keyframes](#keyframes): `position`, `size` and `rotate
}
```
-A shape that is square in pixels stays square as it grows, so an animated circle does not turn into an ellipse halfway through. A solid colour can animate into a gradient, and a square can round itself off with keyframes on `radius`. An animated `rotate` has no limit: 0 to 720 is two full turns.
+A shape that is square in pixels stays square as it grows, so an animated circle stays a circle. A solid colour can animate into a gradient, and keyframes on `radius` can round off a square. An animated `rotate` has no limit: 0 to 720 is two full turns.
## Combinations the API refuses
-
-Two engine primitives draw shapes and they have different abilities, so a few combinations are rejected at submit time rather than rendered wrong. Each error names the fix.
+The engine has two ways to draw a shape, with different features. The API rejects the combinations below at submit time instead of rendering them wrong. Each error names the fix.
| Not combinable | Why | What to do instead |
| --- | --- | --- |
| `radius` on anything but a `rect` | only rectangles have corners | drop it |
-| `radius` with no `color` | an outline-only shape is drawn by the primitive with no corner rounding | give it a fill |
-| `radius` with `rotate` or `softness` | the primitive that rounds corners cannot turn or feather | drop one of them |
+| `radius` with no `color` | outline-only shapes are drawn the way that cannot round corners | give it a fill |
+| `radius` with `rotate` or `softness` | the way that rounds corners cannot rotate or soften edges | drop one of them |
| `stroke` on a `path` | outlining arbitrary path data needs true path offsetting | draw the outline into the path itself |
| `speed` on a shape | there is no file to play faster | animate it with keyframes |
## What else a shape takes
+[`blendMode`](/docs/jobs/compose/media#blend-modes) mixes a shape with the layer below. Effects apply to it as to any clip, and opacity keyframes fade it in and out. A track of only shapes renders as a base layer, so a solid colour background needs no files at all.
-`blendMode` mixes it with the layer below, effects apply to it like any clip, and opacity keyframes fade it in and out. A track holding only shapes composites as a base layer, so a solid colour background is a timeline that references no files at all.
-
-There is no arbitrary path stroke and no per-corner radius. Anything past that is still an `image` asset.
+Shapes have no path stroke and no per-corner radius. For anything beyond that, use an `image` asset.
Every shape field is in the [shape reference](/docs/jobs/compose/reference/shape).
diff --git a/jobs/compose/text.mdx b/jobs/compose/text.mdx
index 706d82d..603dd20 100644
--- a/jobs/compose/text.mdx
+++ b/jobs/compose/text.mdx
@@ -1,18 +1,17 @@
---
-title: "Text, titles and subtitles"
+title: "Text and titles"
sidebarTitle: "Text"
-description: "Add text to a video as a title, a lower third, a running clock, a subtitles clip or a Lottie animation."
+description: "Add a styled title to a video, keep it readable over any footage, and animate it in and out a letter, word or line at a time."
icon: "font"
-keywords: ["video text overlay api", "burn subtitles api", "lower third video api"]
+keywords: ["video text overlay api", "animated text video api", "kinetic typography api", "lower third video api"]
canonical: "https://rendobar.com/docs/jobs/compose/text"
---
-A text clip carries its own words, its own style and its own box. Three neighbouring clip kinds cover the jobs text is usually asked to do: a clock that counts, a subtitles clip that holds every line, and a Lottie animation exported by a designer.
+A `text` clip renders a styled title. `style` holds `font`, `size`, `weight`, `italic`, `color`, `align`, a `stroke`, a `background` box, a `shadow` and `animate`, which sets the entrance (`fade`, `slideUp`, `slideDown`, `slideLeft`, `slideRight`, `blur` or `scale`), whether it arrives a letter, word or line at a time, and the exit. `font` takes any Google Fonts family by name, a bundled family, or your own file. See [Fonts and languages](/docs/jobs/compose/fonts).
+Captions and timers are separate clips, on [Subtitles and timers](/docs/jobs/compose/captions). Lottie animations are on [Media clips](/docs/jobs/compose/media#lottie-animations).
-A `text` asset renders a styled title. `style` holds the type: `font`, `size`, `weight`, `color`, `align`, a `stroke`, a `background` box, a `shadow`, and an `animate` entrance (`fade`, `slideUp`, `slideDown`, `slideLeft`, `slideRight`). `font` takes any Google Fonts family by name, or a family from your own file. See [Fonts](#fonts).
-
-Where it sits is the clip's `transform`, the same field that places every other clip.
+Place the text with `transform`, like any other clip.
```json
{
@@ -34,11 +33,12 @@ Where it sits is the clip's `transform`, the same field that places every other
}
```
-Colours are hex: `#RGB`, `#RRGGBB`, or `#RRGGBBAA` with alpha.
+Colours are hex: `#RGB`, `#RRGGBB`, or `#RRGGBBAA` with alpha. `weight` runs from 100 to 1000, and `italic: true` slants the text. Text does not wrap to a width, so put a line break (`
+`) in `text` where each new line starts. Keep a title to two to six words.
## Keeping text readable
-White text over bright footage disappears. Three fields fix it, in order of how much they change the look.
+White text gets lost over bright footage. Three fields fix it, in order of how much they change the look.
@@ -89,53 +89,58 @@ White text over bright footage disappears. Three fields fix it, in order of how
-## A clock
-
-
-A `timer` counts over the clip's own length, up or down, in the format you name.
-
-```json
-{ "asset": { "type": "timer", "direction": "down", "format": "MM:SS", "style": { "font": "JetBrains Mono", "size": 96, "color": "#FFFFFF" } }, "start": 0, "length": 10 }
-```
-
-`format` is `SS`, `SS.S`, `SS.SS`, `MM:SS`, `MM:SS.SS` or `HH:MM:SS`. It takes the same `style` as text.
-
-
-
-
+## Kinetic titles
-## Subtitles
-
-
-One `subtitles` clip holds every line, either written in the timeline or read from an `.srt` or `.vtt` file. Times are seconds from the clip's start.
+`by` splits the text so it arrives one unit at a time: `"letter"`, `"word"` or `"line"`. `duration` is when the last unit lands. `stagger` is the gap between the start of one unit and the next. `order` picks which unit goes first: `start`, `end`, `center`, `edges` or `random`. `random` uses a seed from the clip, so re-renders match.
```json
-{ "asset": { "type": "subtitles", "cues": [
- { "start": 0, "end": 1.8, "text": "Subtitles live in the timeline." },
- { "start": 2, "end": 4, "text": "One clip, every line." }
- ], "reveal": "typewriter", "style": { "font": "Inter", "size": 44, "color": "#FFFFFF", "background": "#000000B0" } },
- "transform": { "position": { "x": "50%", "y": "88%" }, "anchor": "topLeft" } }
+{
+ "asset": {
+ "type": "text",
+ "text": "AFTER DARK",
+ "style": {
+ "font": "Inter",
+ "size": 58,
+ "weight": 800,
+ "animate": { "type": "slideUp", "by": "word", "duration": 1.1 }
+ }
+ },
+ "transform": { "position": { "x": "50%", "y": "46%" } },
+ "start": 0,
+ "length": 6
+}
```
-Or point it at a file: `{ "type": "subtitles", "src": "https://example.com/captions.srt" }`. For word-level captions burned from speech instead, see [animated captions](/jobs/captions/animate).
-
-
-
-
+
+
+
+
+
+
+
+
-## Lottie animations
+Split at most one title per shot, on a top-level track or in `overlays`. On body text or captions, a split slows reading.
+## Leaving
-An `animation` asset plays a Lottie `.json` file, the kind After Effects, Figma and LottieFiles export, with a transparent background. It plays once, for its own length, and fits the frame like an image.
+`out` animates the text away over the clip's last `duration` seconds, with any motion `type` takes and the same split and order. Without `out`, the text stays until the clip ends.
```json
-{ "asset": { "type": "animation", "src": "https://example.com/logo-reveal.json" }, "transform": { "scale": 0.4, "position": { "x": "50%", "y": "50%" } } }
+"animate": { "type": "fade", "duration": 0.6, "out": "blur" }
```
-Export it self-contained: a file that pulls images or fonts from elsewhere is refused, because nothing reaches the renderer except through the checked download.
+
+
+
+
+
+
+
+
-
-
-
+
+A split, an exit, or a `blur` or `scale` entrance makes the clip an animated graphic. It costs the same per minute, and renders up to 1080p at 30 fps on every self-serve plan. It works only on a top-level track or in `overlays`, not inside a scene or a composition. A `fade` or a slide of the whole title does not count as one. See [what each plan renders](/docs/jobs/compose/pricing#what-each-plan-renders).
+
-Fonts are their own page: [Fonts and languages](/docs/jobs/compose/fonts). Every text field is in the [text reference](/docs/jobs/compose/reference/text).
+Every text field is in the [text reference](/docs/jobs/compose/reference/text).
diff --git a/jobs/compose/timeline.mdx b/jobs/compose/timeline.mdx
index 0781b66..4162d3c 100644
--- a/jobs/compose/timeline.mdx
+++ b/jobs/compose/timeline.mdx
@@ -1,42 +1,35 @@
---
title: "How the timeline works"
sidebarTitle: "Timeline"
-description: "Build a compose timeline from tracks and clips, and learn when to leave start and length out of it entirely."
+description: "Build a compose timeline from tracks and clips, and learn when you can leave start and length out of a clip."
icon: "layer-group"
keywords: ["video timeline api", "json video tracks", "video clip timing"]
canonical: "https://rendobar.com/docs/jobs/compose/timeline"
---
-A timeline is tracks, and a track is clips. The first track is the background and every track after it draws over the one before. That is the whole model, and the rest of this page is what falls out of it.
+A timeline holds tracks, and each track holds clips.
-
-The model is two ideas:
-
-- **Tracks stack.** The first track is the background. Each later track composites on top of the ones below it: overlays, titles, picture-in-picture.
-- **Clips sit in time.** A clip can carry a `start` and a `length` in seconds, and it can leave both out. Two clips on the same track are joined by a transition. A gap between them plays nothing.
-
-So a multi-track timeline is layers (tracks) of sequences (clips). That is the whole structure.
+- **Tracks stack.** The first track is the background. Each later track draws on top of the ones below it, for overlays, titles and picture-in-picture.
+- **Clips sit in time.** A clip can have a `start` and a `length` in seconds, or leave both out. A transition joins two clips on the same track. A gap between clips plays nothing.
## Timing is optional
+Every clip sits in a container, which is its scene or the whole video.
-Every clip sits in a **container**: the scene it belongs to, or the whole video. Two sentences cover every case.
+- Omit `length` and the clip runs as long as its content, but never past its container.
+- Write `length: "end"` and it runs to the end of its container, however long its content is.
-- **Omit `length`** and the clip runs as long as its **content** does, never past its container.
-- **Write `length: "end"`** and it runs to the end of its **container**, whatever its content says.
+Content length is the source duration, the trim span, the source duration divided by `speed`, or a composition's own end. For example, `speed: 0.5` on a 4 second clip lasts 8 seconds.
-Content means the source's duration, or the trim span, or the source duration divided by `speed` (so `speed: 0.5` on a 4 second clip occupies 8 seconds), or a composition's own end.
+Text and images have no duration of their own, so both rules give the same result. A title with no `length` stays up for the whole video, which suits a watermark.
-Text and images have no duration of their own, so for them the two sentences say the same thing. A title with no `length` stays up for the whole video, which is what a watermark wants.
+`start` works the same way. Omit it and the clip begins after the clips already on its track, moved earlier by any transition right before it. Set a number and the clip starts that many seconds into its container.
-`start` follows the same shape: omit it and the clip begins after everything already placed on its track, pulled back by any transition immediately before it. Give it a number and that is seconds from the start of its container.
-
-Set either field wherever you want something else. Every timeline written with full timing keeps rendering exactly as it did.
+Set either field when you want different timing. Timelines that set every `start` and `length` render as they always did.
## Worked examples
-
-A music bed. The source is three minutes long, the video is not, and `"end"` is how you say so without totalling the clips above it.
+A music bed. The source is three minutes long and the video is shorter. `"end"` stops the music with the video, without you adding up the clips above it.
```json
"tracks": [
@@ -51,7 +44,7 @@ A music bed. The source is three minutes long, the video is not, and `"end"` is
]
```
-A watermark. It has no duration of its own, so it needs nothing at all.
+A watermark needs no timing fields at all.
```json
{ "clips": [
@@ -60,9 +53,9 @@ A watermark. It has no duration of its own, so it needs nothing at all.
] }
```
-Both stop on the last frame, whatever the clips elsewhere add up to. Add another clip later and they still do.
+Both stop on the last frame, however long the other clips run. If you add another clip later, they still do.
-The music bed above, drawn out. Track 1 is the background and the two clips follow each other because neither names a `start`. The music names `length: "end"`, so it stops on the last frame whatever the video adds up to.
+The music bed as a chart. On track 1 the two clips play in turn because neither sets a `start`. The music has `length: "end"`, so it stops on the last frame.
```mermaid
gantt
@@ -78,19 +71,19 @@ gantt
## The rules that fall out of it
+A clip that runs to the end of the video is measured against every clip that does not, so two of them on different tracks both stop on the real last frame. Nothing may follow it on the same track. A timeline where no clip has a length of its own has no end to measure, and the API rejects it.
-A clip running to the end of the **video** is measured against every clip that is not, so two of them on different tracks both land on the real end rather than chasing each other. It runs to the last frame, so nothing may follow it on the same track, and a timeline where nothing has a length of its own has no end to measure and is rejected.
+If two clips on one track overlap with no transition between them, they dissolve into each other across the overlap, as if a `crossfade` sat there. For a cut, start the second clip where the first ends.
-A clip never runs past its container, so a 6 second video in a 3 second scene plays 3 seconds, and a 2 second video in a 5 second scene plays 2. That applies to a length you wrote as well as one the engine worked out.
+A clip never runs past its container. A 6 second video in a 3 second scene plays 3 seconds, and a 2 second video in a 5 second scene plays 2. This holds whether you wrote the length or left it out.
-`"end"` is the opposite request and does not cap: it asks for the container's full extent whatever the source holds. On a source shorter than the container that is impossible, and the job is rejected for outrunning its source. Leave the length off instead, or trim less.
+`"end"` does not cap. It asks for the full length of the container, however long the source is. If the source is shorter, the job is rejected for outrunning its source. Leave `length` off, or trim less.
-Inside a `composition`, `start` and `length` both stay required. A nested timeline's length is set by the clip hosting it, so there is nothing to append to and no container to measure against.
+Inside a `composition`, `start` and `length` are both required. The clip that holds the composition sets its length, so there is nothing to append to and no container to measure against.
## Add a layer
-
-A second track renders over the first. This lays an animated title over the video.
+A second track renders over the first. This example puts an animated title over the video.
```json
"tracks": [
@@ -104,6 +97,17 @@ A second track renders over the first. This lays an animated title over the vide
]
```
+## Background
+
+`background` is the colour that shows wherever no clip draws, such as the bars around a boxed shot. The default is black. `"transparent"` makes those areas see-through on png and webm output. See [transparent video](/docs/jobs/compose/output#transparent-video).
+
+```json
+"timeline": {
+ "background": "#059669",
+ "tracks": [{ "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/sea.mp4" }, "transform": { "size": { "width": "46%", "height": "100%" } } }] }]
+}
+```
+
@@ -115,8 +119,7 @@ A second track renders over the first. This lays an animated title over the vide
## Scene-first authoring
-
-Tracks give you precise multi-track control. For a sequential edit, you can instead describe a list of scenes. Each scene is a self-contained segment with its own clips. A scene's `transition` blends INTO it from the previous scene, so put it on the second of two scenes. `overlays` is a flat list of clips that sit over the whole composition, like a persistent logo or watermark.
+Tracks give you precise multi-track control. For an edit that plays one segment after another, you can write a list of scenes. Each scene is a segment with its own clips. A scene's `transition` blends into it from the previous scene, so put it on the second of two scenes. `overlays` is a flat list of clips that sit over the whole video, such as a logo or watermark.
```json
{
@@ -133,7 +136,7 @@ Tracks give you precise multi-track control. For a sequential edit, you can inst
}
```
-A scene's clips can omit `start` and `length` to span the whole scene, and a scene can omit its own `duration` to take the length of the video inside it. Every clip field works the same as in tracks mode. `scenes` and `tracks` are mutually exclusive: a timeline uses one or the other.
+A scene's clips can omit `start` and `length` to fill the whole scene. A scene can omit `duration` to take the length of the video inside it. Clip fields work the same as with tracks, except that animated text and 3D turns work only on top-level tracks and in `overlays`, not inside a scene or a composition. A timeline uses `scenes` or `tracks`, never both.
@@ -150,7 +153,24 @@ A scene's clips can omit `start` and `length` to span the whole scene, and a sce
## A timeline as one clip
-A `composition` asset holds a whole timeline of its own, so a group of layers can be moved, scaled and faded together rather than one at a time.
+A `composition` asset holds a timeline of its own, so you can move, scale and fade a group of layers together. Its `timeline` takes `tracks` only, and every clip inside needs an explicit `start` and `length`. Compositions nest up to three levels deep.
+
+```json
+{
+ "asset": {
+ "type": "composition",
+ "timeline": {
+ "tracks": [
+ { "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/sea.mp4" }, "start": 0, "length": 6 }] },
+ { "clips": [{ "asset": { "type": "text", "text": "OPEN WATER", "style": { "font": "Bebas Neue", "size": 84, "color": "#FFFFFF" } }, "start": 0, "length": 6 }] }
+ ]
+ }
+ },
+ "transform": { "scale": 0.52, "rotate": -5 },
+ "start": 0,
+ "length": 6
+}
+```
@@ -163,4 +183,4 @@ A `composition` asset holds a whole timeline of its own, so a group of layers ca
## Where to go next
-Clips blend into each other on [Transitions](/docs/jobs/compose/transitions). Anything that moves is on [Keyframes and easing](/docs/jobs/compose/motion). Every field a clip takes is listed in the [clip reference](/docs/jobs/compose/reference/timeline).
+See [Transitions](/docs/jobs/compose/transitions) to blend clips together and [Keyframes and easing](/docs/jobs/compose/motion) to make things move. [Media clips](/docs/jobs/compose/media) covers what a clip can hold, and the [clip reference](/docs/jobs/compose/reference/timeline) lists every field.
diff --git a/jobs/compose/transitions.mdx b/jobs/compose/transitions.mdx
index a80ffd9..a82ae57 100644
--- a/jobs/compose/transitions.mdx
+++ b/jobs/compose/transitions.mdx
@@ -1,13 +1,13 @@
---
title: "Transitions between clips"
sidebarTitle: "Transitions"
-description: "Add a crossfade, fade, slide, push, wipe or zoom between two clips, and see why a hard cut is the right default."
+description: "Use a crossfade, fade, slide, push, wipe or zoom between two clips, pick a wipe shape or your own wipe pattern, and see when a hard cut is better."
icon: "shuffle"
keywords: ["video transitions api", "crossfade video api", "wipe transition json"]
canonical: "https://rendobar.com/docs/jobs/compose/transitions"
---
-Two clips next to each other on a track cut straight from one to the other. To blend them, put a transition BETWEEN them. It is a clip like any other, with its own entry in the track's list, and there is no transition field on an ordinary clip.
+Two clips side by side on a track cut straight from one to the other. To blend them, put a transition clip between them in the track's list. Ordinary clips have no transition field.
```json
"clips": [
@@ -17,21 +17,21 @@ Two clips next to each other on a track cut straight from one to the other. To b
]
```
-A scene takes its own `transition` instead, which describes how that scene arrives from the one before it.
+A scene instead takes a `transition` field that sets how it enters from the scene before.
-The blend is taken out of the two clips, not added to the timeline, so a transition never makes the render longer.
+The blend overlaps the two clips, so a transition never makes the render longer.
## The seven kinds
-`crossfade` dissolves one picture into the other. `fade` goes through the background colour between them, which reads as a chapter break. `slide` moves the new clip in over the old one, `push` shoves the old one out of frame, and both take a `direction`. `wipe` reveals the new clip along a moving edge whose `shape` you choose. `zoom` scales into the new clip. `none` is the hard cut, and it is the default because it is what professional editors use for almost every join.
+`crossfade` dissolves one picture into the other. `fade` dips to black, like a chapter break. `slide` moves the new clip in over the old one. `push` moves the old one out of frame. `wipe` reveals the new clip in place, along a moving edge or through a shape. `zoom` scales into the new clip. `none` is a hard cut, which professional editors use for almost every join.
-Every one below joins the same two shots, so they can be compared.
+All the videos below join the same two shots.
-
+
@@ -54,18 +54,37 @@ Every one below joins the same two shots, so they can be compared.
+## Direction
+
+`slide`, `push` and an `edge` wipe take a `direction` for the way the picture moves: `left`, `right`, `up` or `down`.
+
+```json
+{ "type": "transition", "transition": "push", "direction": "up", "duration": 0.5 }
+```
+
## Wipe shapes
-`edge` is a straight line travelling in `direction`. `circle` opens from the centre, `box` opens as a rectangle, and `barnDoorHorizontal` and `barnDoorVertical` split from the middle outwards.
+A wipe's `shape` defaults to `edge`, a straight line moving in `direction`. `circle` opens from the centre, `box` opens as a rectangle, and `barnDoorHorizontal` and `barnDoorVertical` split from the middle outwards.
-## How long it can be
+```json
+{ "type": "transition", "transition": "wipe", "shape": "circle", "duration": 0.8 }
+```
+
+## Your own wipe
-A transition cannot be longer than either clip it joins. Ask for more and the render clamps it to what fits and the job carries a warning, so a template built for long clips still works when a short one arrives. [Validate](/docs/jobs/compose/output#check-it-before-you-render) reports the same warning before you spend anything.
+`map` takes the URL of a greyscale image and wipes in its pattern. Black areas change first, white last, and greys in between. A radial gradient gives a circular iris and a brush stroke gives a painted reveal. A `map` replaces both `shape` and `direction`.
+
+```json
+{ "type": "transition", "transition": "wipe", "map": "https://example.com/brush-wipe.png", "duration": 1 }
+```
+
+## How long it can be
+A transition cannot be longer than either clip it joins. Ask for more and the render shortens it to fit and adds a warning to the job, so a template made for long clips still works with a short one. [Validation](/docs/jobs/compose/pricing#check-a-timeline-before-you-render) reports the same warning before you spend anything.
## How to use them
-Professionals cut. Keep crossfades under 10% of your joins and pick ONE decorative style (slide or zoom) per video, at section boundaries only.
+Cut hard for most joins and keep crossfades under 10% of them. Use one decorative style (slide or zoom) per video, and only between sections.
| Use | Transition | Duration |
| --- | --- | --- |
diff --git a/jobs/ffmpeg.mdx b/jobs/ffmpeg.mdx
index 0cba160..7c4eb6f 100644
--- a/jobs/ffmpeg.mdx
+++ b/jobs/ffmpeg.mdx
@@ -173,7 +173,7 @@ This burns a subtitle track that lives only in the request, with no file to host
- Filters that read a file by name (`subtitles=`, `lut3d=`, `drawtext fontfile=`, `concat` lists) must get that file through `inputs`. They are not `-i` arguments. See [Input sources](#input-sources).
+ Filters that read a file by name (`subtitles=`, `lut3d=`, `drawtext fontfile=`, `concat` lists) must get that file through `inputs`. They are not `-i` arguments. See [Input sources](#what-can-i-use-as-an-input).
@@ -185,7 +185,7 @@ This burns a subtitle track that lives only in the request, with no file to host
- Max execution time in seconds. Defaults to your plan's full job budget (Free 1 hour, Pro 9 hours), so you only set this when you want a job to fail sooner. Asking for more than your plan allows is rejected at submit with `PLAN_LIMIT`.
+ Max execution time in seconds. Defaults to your plan's full job budget (Free 1 hour, Pro and Studio 9 hours), so you only set this when you want a job to fail sooner. Asking for more than your plan allows is rejected at submit with `PLAN_LIMIT`.
@@ -195,7 +195,7 @@ This burns a subtitle track that lives only in the request, with no file to host
- `gpu`: force a GPU machine.
- `cpu`: force a CPU machine. A command that calls an NVENC encoder is rejected with `VALIDATION_ERROR`.
- GPU jobs run the hardware encoders `h264_nvenc`, `hevc_nvenc`, and `av1_nvenc` on NVIDIA L4 GPUs, billed per second. `gpu` and `auto`-routed GPU jobs **require the Pro plan**. A GPU job on the Free plan returns `403 PLAN_LIMIT`. `cpu` and `auto` work on every plan. See [GPU acceleration](#gpu-acceleration).
+ GPU jobs run the hardware encoders `h264_nvenc`, `hevc_nvenc`, and `av1_nvenc` on NVIDIA L4 GPUs, billed per second. `gpu` and `auto`-routed GPU jobs **require a Pro or Studio plan**. A GPU job on the Free plan returns `403 PLAN_LIMIT`. `cpu` and `auto` work on every plan. See [GPU acceleration](#can-i-use-gpu-acceleration).
## Can I validate a command without running it?
@@ -263,7 +263,7 @@ Set `compute` only to force a choice. `gpu` pins the job to a GPU. `cpu` pins it
```
-GPU jobs require the Pro plan and bill per second at GPU rates. A GPU job on the Free plan returns `403 PLAN_LIMIT`. CPU jobs run on every plan.
+GPU jobs require a Pro or Studio plan and bill per second at GPU rates. A GPU job on the Free plan returns `403 PLAN_LIMIT`. CPU jobs run on every plan.
## How is a command sandboxed?
@@ -279,7 +279,7 @@ Every command is validated before it runs, then executed in an isolated, network
Priced per compute second from your [credit balance](/concepts/credits), the wall-clock time FFmpeg ran, excluding file transfer.
-Plan job budgets: 1 hour (Free), 9 hours (Pro). That is a bound on processing time, not on how long your output can be. See [plan limits](/support/limits) for the full table. Exceeding the timeout returns `RUNNER_TIMEOUT`, and no credits are charged.
+Plan job budgets: 1 hour (Free), 9 hours (Pro and Studio). That is a bound on processing time, not on how long your output can be. See [plan limits](/docs/support/limits) for the full table. A job that runs past its budget fails with `JOB_TIMEOUT` and is billed for the compute it used. A job a runner accepted but never started fails with `RUNNER_TIMEOUT` and is not charged.
## Examples
diff --git a/jobs/image-edit.mdx b/jobs/image-edit.mdx
index 2a087d0..ab2aa74 100644
--- a/jobs/image-edit.mdx
+++ b/jobs/image-edit.mdx
@@ -238,7 +238,7 @@ Previews are decoration and never hold up a job. The `standard` tier finishes in
- Rewrite the instruction for the model before editing. Off by default on every model in the catalog today. The default is per model (`enhanceDefault` in [`GET /models`](#discover-models)), so omit it to keep whatever the model ships with.
+ Rewrite the instruction for the model before editing. Off by default on every model in the catalog today. The default is per model (`enhanceDefault` in [`GET /models`](#how-do-i-discover-models)), so omit it to keep whatever the model ships with.
diff --git a/jobs/image-upscale.mdx b/jobs/image-upscale.mdx
index b3c7074..504cfc5 100644
--- a/jobs/image-upscale.mdx
+++ b/jobs/image-upscale.mdx
@@ -106,7 +106,7 @@ print(job["output"]["file"]["url"]) # the png
-Either way the output lands on exactly the size you asked for, as long as it fits on the GPU. The default is `factor: 2`. See [Size limits](#size-limits) for what happens when it does not.
+Either way the output lands on exactly the size you asked for, as long as it fits on the GPU. The default is `factor: 2`. See [Size limits](#what-are-the-size-limits) for what happens when it does not.
## Parameters
diff --git a/mcp-server.mdx b/mcp-server.mdx
index 3f106ba..4509318 100644
--- a/mcp-server.mdx
+++ b/mcp-server.mdx
@@ -71,7 +71,7 @@ Call `list_job_types` first. It returns every active job type fresh, so it never
| Tool | What it does | Returns |
|---|---|---|
| `list_job_types` | Every active job type: use cases, what it chains with, a params digest, and an example payload. With `type`, the full annotated parameter schema for that one type. | `{ jobTypes[], guidance }` or `{ type, paramsJsonSchema, example, guidance }` |
-| `validate_compose` | Check compose params without rendering. Free, nothing is stored, and it runs the same checks a submit does. Call it before `submit_job` on any timeline you wrote. | `{ valid, problems[], params?, warnings[] }` |
+| `validate_compose` | Check compose params without rendering and get the price of the render. Free, nothing is stored, and it runs the same checks a submit does. Call it before `submit_job` on any timeline you wrote. | `{ valid: true, params, upgraded[], warnings[], price }` or `{ valid: false, problems[], params, upgraded[], warnings[] }`. `params` is null when the document did not parse. |
| `submit_job` | Submit a job. `inputs` accepts a URL string, `{ url }`, `{ content }` (inline text), or `{ job: "job_..." }` for chaining. A URL can be a `storage://` URI, and `destinations` writes the output to a connected bucket. | `{ jobId, status }` |
| `get_job` | Check status and results. `wait: true` blocks up to 45 seconds instead of polling. A completed video job includes an inline poster image. | `{ id, status, output?, cost?, error? }` |
| `list_jobs` | Recent jobs, filterable by `status` and `type`. | `{ jobs[], total }` |
diff --git a/quickstart.mdx b/quickstart.mdx
index d2f4127..12ea190 100644
--- a/quickstart.mdx
+++ b/quickstart.mdx
@@ -165,5 +165,5 @@ Need more credits? See [pricing](https://rendobar.com/pricing/) for plan and cre
- [FFmpeg guide](/jobs/ffmpeg): security model, allowed flags, worked examples
- [Job lifecycle](/concepts/job): the six statuses and what triggers each
-- [Pricing and plans](https://rendobar.com/pricing/): Free vs Pro and credit-pack pricing
+- [Pricing and plans](https://rendobar.com/pricing/): Free, Pro and Studio plans and credit-pack pricing
- [All supported FFmpeg operations](https://rendobar.com/features/): full feature index
diff --git a/sdk.mdx b/sdk.mdx
index 259c7cd..082c97b 100644
--- a/sdk.mdx
+++ b/sdk.mdx
@@ -178,7 +178,9 @@ A WebSocket can't carry an `Authorization` header, so the SDK appends your API k
## Billing
-`billing.state()` returns balance and plan. `billing.usage({ start, end })` totals spend for a window, `billing.usageByClient({ days })` groups that spend by what submitted each job (`dashboard`, `sdk`, `cli`, `mcp`, `n8n`, `api`), and `billing.transactions({ page, limit })` reads the ledger.
+`billing.state()` returns balance and plan. `billing.usage({ start, end })` totals spend for a window, `billing.usageByClient({ days })` groups that spend by what submitted each job (`dashboard`, `sdk`, `cli`, `mcp`, `n8n`, `api`), and `billing.transactions({ page, limit })` reads the ledger. `billing.checkoutPlan("pro" | "studio")` returns a checkout URL, or moves an existing paid subscription to that plan. It replaces `billing.checkoutPro()`, which is deprecated.
+
+`compose.validate(params)` checks a [compose](/docs/jobs/compose) timeline for free and, when it is valid, returns its price. `compose.effects()` and `compose.engineEffects()` list the effects a timeline can name.
`apiKeys`, `assets`, and `orgs` follow the same shape. Organization membership lives on `orgs.members` (`invite`, `changeRole`, `remove`) and `orgs.invitations` (`revoke`). The older `team` resource still works and is marked deprecated.
@@ -201,7 +203,7 @@ const browser = createClient({ credentials: "include" }); // first-party browse
## Errors
-A failed request throws an `ApiError` with `code`, `statusCode`, `message`, `details`, and `retryAfter` on `RATE_LIMITED`. Narrow it with `isApiError`.
+A failed request throws an `ApiError` with `code`, `statusCode`, `message`, `details`, and `retryAfter` on `RATE_LIMITED`. When the API refuses a request with a list of problems, `problems` holds them typed (`path`, `message`, and `upgradeTo` on a plan refusal). Narrow it with `isApiError`.
```ts
import { isApiError } from "@rendobar/sdk";
diff --git a/storage/index.mdx b/storage/index.mdx
index ec1258b..597f2e2 100644
--- a/storage/index.mdx
+++ b/storage/index.mdx
@@ -58,7 +58,7 @@ A connection has three parts:
### Connect a bucket
-In [**Storage**](https://app.rendobar.com/storage), select **Connect Storage** and choose your provider. Each [provider page](#supported-providers) covers the credential it needs.
+In [**Storage**](https://app.rendobar.com/storage), select **Connect Storage** and choose your provider. Each [provider page](#which-providers-are-supported) covers the credential it needs.
diff --git a/support/errors.mdx b/support/errors.mdx
index c529e00..c5699d1 100644
--- a/support/errors.mdx
+++ b/support/errors.mdx
@@ -1,7 +1,7 @@
---
title: "API error codes reference"
sidebarTitle: "Error codes"
-description: "View all 26 API error codes, their HTTP status, and recommended action. Errors follow the {error: {code, message}} envelope on every endpoint."
+description: "View all 27 API error codes, their HTTP status, and recommended action. Errors follow the {error: {code, message}} envelope on every endpoint."
icon: "triangle-exclamation"
keywords: ["rendobar error codes", "api error reference", "ffmpeg error troubleshooting", "insufficient credits", "rate limited"]
canonical: "https://rendobar.com/docs/support/errors"
@@ -39,13 +39,14 @@ Check the HTTP status and `error.code` before reading `data`.
| `FORBIDDEN` | 403 | Authenticated but not allowed for this resource |
| `VALIDATION_ERROR` | 400 | Body failed schema validation. `details.path` points at the field |
| `INVALID_JOB_TYPE` | 400 | Job type not registered or not on your plan |
+| `FEATURE_NOT_ENABLED` | 400 | A compose job uses animated text or 3D turns, which are still rolling out and not enabled for your account yet. Render without them, or contact support to get access |
| `INSUFFICIENT_CREDITS` | 402 | Top up at [app.rendobar.com/billing](https://app.rendobar.com/billing) |
-| `PLAN_LIMIT` | 403 | Plan limit hit (concurrency, file size). See [Limits](/support/limits) |
+| `PLAN_LIMIT` | 403 | Plan limit hit (job timeout, GPU, a compose frame or frame rate above the plan). See [Limits](/docs/support/limits) and [which plan lifts it](#plan-limit-upgrade) |
| `RATE_LIMITED` | 429 | Back off per `Retry-After` header |
| `NOT_FOUND` | 404 | Resource doesn't exist or isn't visible to your org |
| `CONFLICT` | 409 | Request conflicts with current state (e.g. cancelling a complete job) |
| `RUNNER_ERROR` | 502 | Processing failed on the runner. Check `error.message` for details |
-| `RUNNER_TIMEOUT` | 504 | The job did not finish in time. No credits charged |
+| `RUNNER_TIMEOUT` | 504 | A runner accepted the job but never started it, or lost the run. No credits charged |
| `ORG_SUSPENDED` | 403 | Contact `hello@rendobar.com` |
| `INTERNAL_ERROR` | 500 | Server bug. Persist? Contact support with the request ID |
| `STORAGE_QUOTA_EXCEEDED` | 402 | Stored assets exceed the plan quota. Delete assets or upgrade |
@@ -62,6 +63,22 @@ Check the HTTP status and `error.code` before reading `data`.
| `NOT_CONFIGURED` | 503 | The capability is not configured on this deployment. Contact support |
| `NOT_IMPLEMENTED` | 501 | The endpoint exists but does nothing yet |
+## Which plan lifts a PLAN_LIMIT? {#plan-limit-upgrade}
+
+A compose video above your plan and a GPU job on Free return `PLAN_LIMIT` with `error.details`, a list of problems shaped `{ path, message, upgradeTo }`. `upgradeTo` is `"pro"` or `"studio"`, the cheapest plan that runs the job. It is `null` when only an enterprise plan does. Read `upgradeTo` instead of parsing the message.
+
+```json
+{
+ "error": {
+ "code": "PLAN_LIMIT",
+ "message": "A 3840x2160 video at 30 fps is above your plan, which renders compose up to 1080p at 30 fps. Studio ($29/month) renders it.",
+ "details": [
+ { "path": ["output"], "message": "A 3840x2160 video at 30 fps is above your plan, which renders compose up to 1080p at 30 fps. Studio ($29/month) renders it.", "upgradeTo": "studio" }
+ ]
+ }
+}
+```
+
## Storage connections
Creating, testing, or updating a [storage connection](/storage) returns these codes:
diff --git a/support/faq.mdx b/support/faq.mdx
index 494eba6..bd72a36 100644
--- a/support/faq.mdx
+++ b/support/faq.mdx
@@ -21,7 +21,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "How much does Rendobar cost?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Free plan ships with a one-time $5 credit grant, no card required. Pro is $9/mo, includes $5/mo in credits, plus 20% bonus on every credit purchase. Credit packs $10–$100 on both plans. Credits never expire."
+ "text": "Free plan ships with a one-time $5 credit grant, no card required. Pro is $9/mo and includes $5/mo in credits. Studio is $29/mo and includes $20/mo in credits. Both add a 25% bonus on every credit purchase. Credit packs $10 to $100 on every plan. Credits never expire. Compose bills per minute of output video: $0.04 on every plan, billed per second with a 15-second minimum per render. Pro adds compose at 1440p and GPU FFmpeg. Studio adds compose at 4K and 60 fps and 50 concurrent jobs. Every other job type bills per second of compute."
}
},
{
@@ -29,7 +29,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "What can I do with Rendobar today?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "The live API is the ffmpeg job type, any whitelisted FFmpeg command against URLs you provide. That covers transcoding, trimming, filtering, overlays, audio extraction, frame extraction, and anything else FFmpeg does from the command line. More dedicated job types are on the roadmap."
+ "text": "Nine job types are live: ffmpeg and ffprobe for any FFmpeg command and file inspection, compose to render a video from a JSON timeline, compress.target to hit a file size, caption.burn and captions.animate for subtitles, and image.generate, image.edit and image.upscale for images."
}
},
{
@@ -53,7 +53,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "How long do jobs take?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Seconds to minutes, depending on input size and command. Hard caps on processing time: 1 hour (Free), 9 hours (Pro)."
+ "text": "Seconds to minutes, depending on input size and command. Hard caps on processing time: 1 hour (Free), 9 hours (Pro and Studio)."
}
},
{
@@ -61,7 +61,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "Is there a rate limit?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Yes. 120 req/min (Free), 600 req/min (Pro). Applies to REST and MCP. Limit hit returns 429 RATE_LIMITED with a Retry-After header."
+ "text": "Yes. 120 req/min (Free), 600 req/min (Pro), 1200 req/min (Studio). Applies to REST and MCP. Limit hit returns 429 RATE_LIMITED with a Retry-After header."
}
},
{
@@ -69,7 +69,7 @@ canonical: "https://rendobar.com/docs/support/faq"
"name": "What happens if a job fails?",
"acceptedAnswer": {
"@type": "Answer",
- "text": "Failed jobs are charged for the compute they used. Rendobar has no auto-refund. If you believe a failure was caused by a Rendobar bug or platform issue, contact support and we will review for a discretionary refund. Successful and failed jobs are billed the same way: for the compute the job consumed. Inspect error.code and error.message on the job, and retry via POST /jobs/{id}/retry if appropriate."
+ "text": "A failed compose render is free when the failure is on Rendobar's side. When it may come from your input, such as the render tool exiting on a bad file, it pays for the compute it used at 2x. Other failed jobs are charged for the compute they used, the same way successful jobs are. A job that never started is free. Rendobar has no auto-refund. If you believe a failure was caused by a Rendobar bug or platform issue, contact support and we will review for a discretionary refund. Inspect error.code and error.message on the job, and retry via POST /jobs/{id}/retry if appropriate."
}
},
{
@@ -96,11 +96,11 @@ canonical: "https://rendobar.com/docs/support/faq"
- Free plan ships with a one-time $5 credit grant, no card required. Pro is $9/mo, includes $5/mo in credits, plus 20% bonus on every credit purchase. Credit packs $10–$100 on both plans. Credits never expire. Full breakdown: [Credits and billing](/concepts/credits) or the [pricing page](https://rendobar.com/pricing/).
+ Free plan ships with a one-time \$5 credit grant, no card required. Pro is \$9/mo and includes \$5/mo in credits. Studio is \$29/mo and includes \$20/mo in credits. Both add a 25% bonus on every credit purchase. Credit packs \$10 to \$100 on every plan. Credits never expire. Compose bills per minute of output video: \$0.04 on every plan, billed per second with a 15-second minimum per render. Pro adds compose at 1440p and GPU FFmpeg. Studio adds compose at 4K and 60 fps and 50 concurrent jobs. Every other job type bills per second of compute. Full breakdown: [Credits and billing](/docs/concepts/credits), [compose pricing](/docs/jobs/compose/pricing), or the [pricing page](https://rendobar.com/pricing/).
- The live API is the `ffmpeg` job type, any whitelisted FFmpeg command against URLs you provide. That covers transcoding, trimming, filtering, overlays, audio extraction, frame extraction, and anything else FFmpeg does from the command line. More dedicated job types are on the roadmap.
+ Nine job types are live: `ffmpeg` and `ffprobe` for any FFmpeg command and file inspection, `compose` to render a video from a JSON timeline, `compress.target` to hit a file size, `caption.burn` and `captions.animate` for subtitles, and `image.generate`, `image.edit` and `image.upscale` for images.
@@ -112,15 +112,15 @@ canonical: "https://rendobar.com/docs/support/faq"
- Seconds to minutes, depending on input size and command. Hard caps on processing time: 1 hour (Free), 9 hours (Pro). [Limits](/support/limits).
+ Seconds to minutes, depending on input size and command. Hard caps on processing time: 1 hour (Free), 9 hours (Pro and Studio). [Limits](/docs/support/limits).
- Yes. 120 req/min (Free), 600 req/min (Pro). Applies to REST and MCP. Limit hit returns `429 RATE_LIMITED` with a `Retry-After` header.
+ Yes. 120 req/min (Free), 600 req/min (Pro), 1200 req/min (Studio). Applies to REST and MCP. Limit hit returns `429 RATE_LIMITED` with a `Retry-After` header.
- Failed jobs are charged for the compute they used. Rendobar has no auto-refund. If you believe a failure was caused by a Rendobar bug or platform issue, contact support and we will review for a discretionary refund. Successful and failed jobs are billed the same way: for the compute the job consumed. Inspect `error.code` and `error.message` on the job, and retry via `POST /jobs/{id}/retry` if appropriate. [Error codes](/support/errors).
+ A failed compose render is free when the failure is on Rendobar's side. When it may come from your input, such as the render tool exiting on a bad file, it pays for the compute it used at 2x. Other failed jobs are charged for the compute they used, the same way successful jobs are. A job that never started is free. Rendobar has no auto-refund. If you believe a failure was caused by a Rendobar bug or platform issue, contact support and we will review for a discretionary refund. Inspect `error.code` and `error.message` on the job, and retry via `POST /jobs/{id}/retry` if appropriate. [Error codes](/docs/support/errors).
@@ -140,5 +140,5 @@ For more about what Rendobar can do, see [rendobar.com](https://rendobar.com) or
- [Credits and billing](/concepts/credits): full pricing and balance model
- [Plan limits](/support/limits): every rate, file, and storage cap by plan
- [Error codes](/support/errors): every `error.code` the API can return
-- [Pricing](https://rendobar.com/pricing/): Free vs Pro and credit packs
+- [Pricing](https://rendobar.com/pricing/): Free, Pro and Studio plans and credit packs
- [Rendobar vs alternatives](https://rendobar.com/vs/): comparisons with other video APIs
diff --git a/support/limits.mdx b/support/limits.mdx
index 54f2e34..c662efa 100644
--- a/support/limits.mdx
+++ b/support/limits.mdx
@@ -1,7 +1,7 @@
---
title: "Plan limits and quotas"
sidebarTitle: "Limits"
-description: "Compare Free and Pro plan limits across rate limits (120 vs 600/min), file size caps (500 MB vs 10 GB), retention, concurrency, storage."
+description: "Compare Free, Pro and Studio limits: rate limits (120 vs 600/min), file size caps (500 MB vs 10 GB), retention, concurrency, storage, compose frames."
icon: "gauge"
keywords: ["rendobar limits", "api rate limits", "plan limits", "file size limit", "concurrent jobs"]
canonical: "https://rendobar.com/docs/support/limits"
@@ -15,7 +15,7 @@ canonical: "https://rendobar.com/docs/support/limits"
"@type": "TechArticle",
"@id": "https://rendobar.com/docs/support/limits#article",
"headline": "Plan limits and quotas",
- "description": "Free and Pro plan limits: rate limits, file size caps, output retention, concurrent jobs, and storage quotas.",
+ "description": "Free, Pro and Studio plan limits: rate limits, file size caps, output retention, concurrent jobs, storage quotas and compose frames.",
"datePublished": "2026-03-20",
"author": { "@type": "Organization", "@id": "https://rendobar.com/#organization" },
"publisher": { "@type": "Organization", "@id": "https://rendobar.com/#organization" }
@@ -27,32 +27,39 @@ Every limit Rendobar enforces, by plan. Pulled from `PLAN_LIMITS` in the API. Fo
## Plan limits
-| | Free | Pro ($9/mo) |
-|---|---|---|
-| Signup credits | $5 (one-time) | - |
-| Monthly credits | - | $5/mo included |
-| Credit-purchase bonus | - | +20% |
-| Concurrent jobs | 1 | 20 |
-| Queued backlog | 50 | 1000 |
-| API requests / min | 120 | 600 |
-| Max input file | 500 MB | 10 GB |
-| Max job timeout | 1 hour | 9 hours |
-| Output retention | 7 days | 30 days |
-| Total object storage | 5 GB | 250 GB |
-| Priority queue | No | Yes |
-
-`ffmpeg`, MCP, and webhooks are on both plans.
+| | Free | Pro (\$9/mo) | Studio (\$29/mo) |
+|---|---|---|---|
+| Signup credits | $5 (one-time) | - | - |
+| Monthly credits | - | \$5/mo included | \$20/mo included |
+| Credit-purchase bonus | - | +25% | +25% |
+| Concurrent jobs | 1 | 20 | 50 |
+| Queued backlog | 50 | 1000 | 2500 |
+| API requests / min | 120 | 600 | 1200 |
+| Max input file | 500 MB | 10 GB | 10 GB |
+| Total input per job | 2 GB | 8 GB | 8 GB |
+| Max job timeout | 1 hour | 9 hours | 9 hours |
+| Output retention | 7 days | 30 days | 90 days |
+| Total object storage | 5 GB | 250 GB | 250 GB |
+| Compose frame | Up to 1080p at 30 fps | Up to 1440p at 30 fps | Up to 4K at 60 fps |
+| Compose with animated graphics | Up to 1080p at 30 fps | Up to 1080p at 30 fps | Up to 1080p at 30 fps |
+| GPU FFmpeg (NVENC/CUDA) | No | Yes | Yes |
+| Team members | 2 | 3 | 10 |
+| Priority queue (jobs start first when runners are full) | No | No | Yes |
+| Audit log | No | No | Yes |
+| Priority support | No | No | Yes |
+
+`ffmpeg`, MCP, and webhooks are on every plan. A compose video above your plan's frame or frame rate returns `403 PLAN_LIMIT` before it renders, and `upgradeTo` in `error.details` names the plan that renders it. Animated graphics above 1080p or 30 fps need an enterprise plan. `GET /account/capabilities` returns these as `composeMaxPixels`, `composeMaxFps`, `composeAnimatedMaxPixels` and `composeAnimatedMaxFps`. See [compose pricing](/docs/jobs/compose/pricing).
## How long do output URLs last?
- **Signed download URL** (`output.file.url`) is valid for **1 hour**. Re-fetch via `GET /jobs/{id}` for a fresh URL.
-- **Output retention**: 7 days (Free) or 30 days (Pro). After that, the file is deleted; the job record stays but the file URLs return 404.
+- **Output retention**: 7 days (Free), 30 days (Pro) or 90 days (Studio). After that, the file is deleted. The job record stays, but the file URLs return 404.
- **Storage quota** caps total bytes across all your outputs. Delete old outputs or upgrade if you hit it.
- **No separate per-output size limit**. Bounded by input cap and storage quota.
## Credit packs
-$10 / $25 / $50 / $100 (or custom, $10 minimum). Pro adds +20% to every purchase. Credits never expire. Failed jobs are always refunded.
+\$10 / \$25 / \$50 / \$100 (or custom, \$10 minimum). Pro and Studio add +25% to every purchase. Credits never expire. Failed jobs are not refunded automatically. See [credits and billing](/docs/concepts/credits) for what a failed job pays.
## API response limits