# Rendobar > ## Agent Instructions > Keep the API key in an env var (RENDOBAR_API_KEY), never in chat or committed code. > Job types change, so fetch the live catalog from GET https://api.rendobar.com/jobs/types rather than relying on memory. > The full integration prompt for coding agents is at https://rendobar.com/prompts/integrate.md. # Run media jobs from Activepieces Source: https://rendobar.com/docs/automation/activepieces Install the Rendobar piece on Activepieces, add an API key, and run media jobs that pause the flow until they finish, with a trigger for finished jobs. The Rendobar piece runs FFmpeg commands, compression, captions and image generation from an Activepieces flow, and a trigger starts a flow when a job finishes. **Run a Media Job** pauses the flow while the job runs, so an hour-long render holds no worker and does not count against the flow's timeout. ```text theme={null} @rendobar/piece-rendobar ``` ![The Rendobar mark joined by a connector line to the Activepieces logo. The caption reads: Flows pause on a waitpoint until the job finishes.](https://mintcdn.com/rendobar/GMgq9TyL67eCvVbG/images/automation/hero-activepieces.png?fit=max\&auto=format\&n=GMgq9TyL67eCvVbG\&q=85\&s=c031d59baa9cd0f66bbb28cd570f4dd0) The Rendobar piece runs video and image jobs inside Activepieces flows. ## Before you start * A Rendobar API key. Create one on the [API Keys](https://app.rendobar.com/api-keys) page of the dashboard. * Platform admin access to Activepieces 0.36.1 or later. The piece installs from npm rather than from the Activepieces catalog, and installing a custom piece is a paid feature on Activepieces Cloud. * An Activepieces that Rendobar can reach over HTTPS, for flows that pause and for the instant trigger. Without one, **Run a Media Job** polls instead and **Finished Job (Polling)** replaces the trigger. ## Install the piece **1. Open Pieces** In the platform admin, select **Pieces** under **Setup**. **2. Install from npm** Select **Install Piece**. Keep **Package Type** on **NPM Registry**, enter `@rendobar/piece-rendobar` as the package name, and confirm. **3. Check the list** Rendobar appears in the list with its package name and the installed version. ![The Activepieces platform admin Pieces page. An Install Piece button sits at the top right. The list shows Rendobar with the package name @rendobar/piece-rendobar at version 0.1.1, and the Webhook piece below it.](https://mintcdn.com/rendobar/j9pv13kq0w3URKob/images/activepieces/installed.png?fit=max\&auto=format\&n=j9pv13kq0w3URKob\&q=85\&s=3186f10b92b558eddeee8454f4b85991) The Pieces page in the platform admin after an install. Rendobar is listed by its npm package name and version, and Install Piece at the top right adds the next one. ## Add your API key Add a Rendobar step to a flow and create a connection with your API key, which starts with `rb_`. The piece checks the key against your account when you save the connection. Use an API key rather than an OAuth token. **Finished Job** registers a webhook on your account, and Rendobar refuses webhook management over OAuth. If you restrict the key's [scopes](/docs/concepts/scopes), keep `webhooks:write` for that trigger. ## Run a job Add **Run a Media Job** and pick a **Job Type**. The form below it is built from that job type's live schema, so a new job type shows up without a piece update. * **Input Media** has one field per file the job reads. `ffmpeg` and `ffprobe` name their files inside the command, so they get a map from file name to URL instead. * **Parameters** are the job's own settings. A job type with several models shows **Variant** first, and the fields change to match. * **Deliver To** writes the output to buckets connected on the [Storage page](https://app.rendobar.com/storage), optionally under a **Delivery Folder or Path**. ![The Run a Media Job panel in the Activepieces builder. Connection is Rendobar and connected. Job Type is compress.target. Variant is disabled with the text Not used by this job type. Below are a required Source field, a For dropdown and a Target JSON editor.](https://mintcdn.com/rendobar/j9pv13kq0w3URKob/images/activepieces/job-form.png?fit=max\&auto=format\&n=j9pv13kq0w3URKob\&q=85\&s=83d7fcf416f962f04cd23a675210a3ae) Run a Media Job with compress.target selected. Variant is disabled because this job type has one mode, and Source, For and Target come from the job type's schema. For a file produced earlier in the flow, run **Upload File** first and put its `url` in a media field. The upload goes straight to storage through a presigned URL, so the bytes never pass through the API. ### What comes out Every job-shaped action and trigger returns the same flat row, so a table built on one step works with the others. | Column | Holds | | - | - | | `id`, `type`, `status` | The job, its type and its status | | `succeeded` | Whether the job completed | | `file_url`, `file_size_bytes` | The headline output file | | `data` | A job-specific result, such as a probe report or a transcript | | `files` | Every output file | | `cost_formatted`, `duration_ms` | What the job cost and how long it ran | | `deliveries` | One entry per bucket under **Deliver To**, with `status`, `path`, `url` and `reason` | `cost_formatted` can be `null` on a row read the moment a job completes, because billing settles a moment later. A later read carries it. ## How do I wait for a long job? ![Two timelines compared. Polling holds a worker open and repeats status calls from submit to done. A waitpoint submits once, shows a paused span with no execution time, and resumes on Rendobar's callback.](https://mintcdn.com/rendobar/j9pv13kq0w3URKob/images/activepieces/waitpoint.svg?fit=max\&auto=format\&n=j9pv13kq0w3URKob\&q=85\&s=669382cde6ae9cbe5a8ec2e7b0cc2bdb) Polling holds a worker and repeats status calls for the whole job. A waitpoint submits once, parks the flow, and resumes it when Rendobar calls back. **Wait for the Result** is on by default. The step creates a waitpoint, sends its resume URL as the job's callback, and pauses the flow. A paused flow holds no worker and does not count against the run timeout, which is 10 minutes on Activepieces Cloud. A pause can last 30 days, longer than any job runs. When the job ends, Rendobar's callback resumes the flow. The step then re-reads the job over your connection rather than trusting the callback body. 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](#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. ## How do I start a flow when a job finishes? **Finished Job** registers a webhook on your Rendobar account when the flow is turned on, and deletes it when the flow is turned off. **Outcome** picks which jobs start the flow: | Outcome | Starts on | | - | - | | Succeeded only | `job.completed`. This is the default | | Failed only | `job.failed` | | Any finished job, including cancelled | `job.completed`, `job.failed` or `job.cancelled` | | Storage deliveries settled | `job.deliveries_settled`, once every bucket under **Deliver To** has resolved | **Job Type** narrows it to one type, such as `compress.target`. The flow receives the same row **Get Job** returns, read fresh from the API. Each flow with this trigger uses one of the 10 [webhook endpoints](/docs/guides/webhooks) an organization can have. **Finished Job (Polling)** does the same on a timer, for an Activepieces that Rendobar cannot reach. ## Actions | Action | What it does | | - | - | | Run a Media Job | Submit a job and, by default, pause until it finishes | | Upload File | Put a file from the flow into Rendobar and get a `url` a job can read | | Get Job | Read a job's status, output and cost | | Find Jobs | List recent jobs, filtered by status and type | | Get Job Logs | Read what the runner reported, to diagnose a failure | | Get Account | Read the plan, credit balance and limits | | Cancel Job | Stop a job that has not started running | | Share Output | Create a shareable link to a completed job's output file | | Find Storage Connections | List the buckets connected on the Storage page | | List Storage Files | List folders and files in a connected bucket, each with a `storage://` URI | | Custom API Call | Call any other Rendobar endpoint with the connection's key | ## Troubleshooting > **Install Piece is not available** > > Installing a custom piece needs platform admin access, and on Activepieces Cloud it is a paid feature. On a self-hosted platform, check that Activepieces is 0.36.1 or later. > **The step polls instead of pausing** > > Activepieces builds the resume URL from `AP_FRONTEND_URL`. When that points at `localhost` or a private address, Rendobar cannot reach it, so the step polls. Set it to the platform's public HTTPS address and restart Activepieces. > **Finished Job cannot be turned on** > > Rendobar could not register the webhook, and the error names the reason. A key without the `webhooks:write` scope is refused, and so is an OAuth token. If this Activepieces is not reachable from the internet, use **Finished Job (Polling)**. > **Get Job reports that no job was given** > > The job id reached the step empty. A reference such as `{{step_1.id}}` resolves to nothing when the step name is wrong, so pick the value again from the data selector. > **A large output file does not attach** > > Activepieces caps a step file at 10 MB on Cloud. Turn **Download the Output File** off and pass `file_url` to the next step, which most steps accept. ## See also * [Callbacks](/docs/guides/callbacks): the per-job URL the waitpoint listens on * [Storage connections](/docs/storage): the buckets **Deliver To** writes to * [Package on npm](https://www.npmjs.com/package/@rendobar/piece-rendobar): versions and release notes * [Source on GitHub](https://github.com/rendobar/activepieces-piece): MIT licensed Next step: add **Run a Media Job** to a flow, pick `ffmpeg`, and run a command against a public video URL. # Automate media jobs without code Source: https://rendobar.com/docs/automation/index Run Rendobar video and image jobs from n8n or Activepieces, and start from ready-made workflows that import and run on n8n Cloud. Run Rendobar jobs from an automation platform instead of code. ![The Rendobar mark in the centre, joined by curved connector lines to the n8n and Activepieces logos on the left and the Make and Zapier logos on the right. The caption reads: Video and image jobs inside your workflows.](https://mintcdn.com/rendobar/GMgq9TyL67eCvVbG/images/automation/hero-overview.png?fit=max\&auto=format\&n=GMgq9TyL67eCvVbG\&q=85\&s=4f285e584a0f55a8e547d6ea61a3e26c) Rendobar runs video and image jobs from the automation platforms you build workflows in. ## Platforms > **[n8n](/docs/automation/n8n)** > > Verified node for n8n Cloud and self-hosted n8n. > **[Activepieces](/docs/automation/activepieces)** > > Piece for self-hosted and paid Activepieces. > **Make** > > Coming soon. > **Zapier** > > Coming soon. ## Templates Finished n8n workflows that run on n8n Cloud. Open a card, copy its address, and paste it into **Import from URL** in the n8n workflow menu. Each workflow lists its setup steps in a sticky note. > **[Create YouTube quote videos in any language from Google Sheets](https://raw.githubusercontent.com/rendobar/n8n-nodes-rendobar/main/templates/quote-videos-from-google-sheets.json)** > > A new sheet row becomes a 9:16 quote video with music, in a font made for its language. > **[Run FFmpeg commands on n8n Cloud with Google Drive and Slack](https://raw.githubusercontent.com/rendobar/n8n-nodes-rendobar/main/templates/run-ffmpeg-on-n8n-cloud.json)** > > A form takes a video URL and a command. Failed jobs post the FFmpeg error to Slack. # Run media jobs from n8n Source: https://rendobar.com/docs/automation/n8n Install the verified Rendobar node on n8n Cloud or self-hosted n8n, add an API key, then run media jobs, wait without polling and hand the node to an AI Agent. The Rendobar node runs FFmpeg commands, compression, captions, composition and image generation from an n8n workflow, and a trigger node starts a workflow when a job finishes. It's verified by n8n, so on n8n Cloud you install it from the nodes panel with no self-hosting. ```text theme={null} @rendobar/n8n-nodes-rendobar ``` ![The Rendobar mark joined by a connector line to the n8n logo. The caption reads: A verified node for n8n Cloud and self-hosted n8n.](https://mintcdn.com/rendobar/GMgq9TyL67eCvVbG/images/automation/hero-n8n.png?fit=max\&auto=format\&n=GMgq9TyL67eCvVbG\&q=85\&s=4808646b39744a9181aa34c099c307a8) The verified Rendobar node runs video and image jobs inside n8n workflows. ## Before you start * 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](#how-do-i-start-a-workflow-when-a-job-ends). ## Install on n8n Cloud **1. Open the nodes panel** Open a workflow and press +, or Tab on the canvas. **2. Search for Rendobar** Type `Rendobar`. It's listed under **More from the community** with a verified badge. **3. Install** Select it and choose **Install**. The node is now available to everyone on the instance. ![The n8n node details panel for Rendobar before install. The Rendobar icon and a verified badge sit next to the name, an orange Install node button sits to the right, and below them is the description.](https://mintcdn.com/rendobar/GMgq9TyL67eCvVbG/images/n8n/install-detail.png?fit=max\&auto=format\&n=GMgq9TyL67eCvVbG\&q=85\&s=cd4dddd550f11bf7a3214a367f1ab9bc) The node details panel before install. The verified badge means n8n reviewed the code and publishes the version itself, and Install node makes the node available to everyone on the instance. If nothing appears under **More from the community**, an instance owner turns on **Verified Community Nodes** in the Cloud Admin Panel. A restart of the instance may be needed the first time. ## Install on self-hosted n8n The same search works on n8n 1.94.0 and later. Two environment variables gate it, and both default to `true`: ```bash theme={null} N8N_COMMUNITY_PACKAGES_ENABLED=true N8N_VERIFIED_PACKAGES_ENABLED=true ``` The older route also works. Open **Settings > Community Nodes**, choose **Install**, and enter `@rendobar/n8n-nodes-rendobar`. n8n's [verified community nodes](https://docs.n8n.io/integrations/community-nodes/installation-and-management/install-verified-community-nodes) page covers both paths. ## Add your API key The node authenticates with a Rendobar API key. Create one on the [API Keys](https://app.rendobar.com/api-keys) page of the dashboard, then add a **Rendobar API** credential in n8n and paste it in. The key starts with `rb_`. n8n checks it against your account when you save, so a wrong key fails at save time and not mid-workflow. Leave **Base URL** at `https://api.rendobar.com` unless you're pointing at another environment. ## Run a job The package ships two nodes. **Rendobar** runs operations against the API and **Rendobar Trigger** starts a workflow on an event. The common chain is three Rendobar nodes: 1. **File > Upload** streams a binary from the previous node and returns a `url`. 2. **Job > Create** picks a job type and references that `url` in its inputs. 3. **Job > Get** with **Download Output File** on fetches the finished file into a binary property. ![The Rendobar node panel in n8n. Resource is Job, Operation is Create, Job Type is caption.burn. Inputs are given as JSON from the previous node's url. Under Values to Send, Language is auto, Font and Font family are empty, and Font size is 56.](https://mintcdn.com/rendobar/cp33P_FO3mIe3KIF/images/n8n/create-job-panel.png?fit=max\&auto=format\&n=cp33P_FO3mIe3KIF\&q=85\&s=d92a33d53585cdeab99e0553521123dc) The Create Job panel with caption.burn selected. The fields under Values to Send come from the job type's own schema, so a new job type appears here without a node update. The **Job Type** dropdown loads live from your account. For `ffmpeg` and `ffprobe`, which name their files inside the command, switch **Specify Inputs** to **Using JSON** and give the inputs by name: ```json theme={null} { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4" } ``` The command then refers to `source` and names its output: ```text theme={null} -i source -vf scale=-2:720 -c:v libx264 -crf 28 -preset fast output.mp4 ``` ### What comes out Every job type returns the same shape on completion, lifted to the top of the item. With **Output** set to **Simplified**, a finished job looks like this: ```json theme={null} { "id": "job_6f2a9c1d4b8e4f30", "type": "compress.target", "status": "complete", "cost": 0.0034, "data": null, "file": { "url": "https://cdn.rendobar.com/outputs/...", "path": "output.mp4", "type": "video", "size": 8123904 }, "files": [{ "url": "...", "path": "output.mp4", "type": "video", "size": 8123904 }], "expiresAt": 1789300000000, "createdAt": 1789213600000, "completedAt": 1789213640000 } ``` `file` is the headline result, or `null` for a job that computes data. `data` carries a probe report or a transcript, or `null` for a file-only job. `files` lists every output. **Raw** returns the full job and **Selected Fields** lets you pick. ## How do I wait for a long job? A transcode can take minutes and an hour-long render can take an hour. The node offers three ways to get the result, and only one of them is right for a long job. ![Three timelines. Wait for Completion repeats status calls and holds a worker for the whole job. Callback URL plus a Wait node submits once, shows a dashed paused span with no execution time, and resumes when Rendobar's callback arrives. Rendobar Trigger shows one workflow submitting and a second workflow starting on the job.completed event.](https://mintcdn.com/rendobar/cp33P_FO3mIe3KIF/images/n8n/three-ways-to-wait.svg?fit=max\&auto=format\&n=cp33P_FO3mIe3KIF\&q=85\&s=454b7c3cd3ce32a4bdbb12b06bc69a78) Wait for Completion holds a worker. Callback URL plus a Wait node parks the execution for free. The trigger node waits for nothing because a second workflow starts when the job ends. **Wait for Completion**, under **Options** on Create, polls until the job ends and returns the result on the same item. It's the right choice for a clip of a few seconds and the wrong one for anything long, because the n8n worker is held for the whole job. **Callback URL plus a Wait node** is the pattern for long jobs. Set **Callback URL** to the resume URL of a Wait node placed right after Create: ```text theme={null} {{ $execution.resumeUrl }} ``` Then configure the Wait node: * **Resume** on **Webhook Call**, with **HTTP Method** set to **POST**. Rendobar POSTs the callback and the Wait node defaults to GET. * **Limit Wait Time** on, at ten hours. An execution whose callback never lands releases itself instead of parking forever. The execution parks with no worker held. When the job ends, Rendobar's callback resumes it with the job in `$json.body.data`. Run **Job > Get** on that `jobId` to re-fetch the job over your own credential and download the file, so the payload that woke the Wait node is never trusted. > [!WARNING] > > Leave **Wait for Completion** off when you set a Callback URL. The node refuses the combination rather than picking one, because a hidden field in n8n keeps its value. ## How do I start a workflow when a job ends? **Rendobar Trigger** registers a webhook with Rendobar when the workflow activates and removes it on deactivation. Pick the events: | Event | Fires when | | - | - | | `job.created` | A job is accepted | | `job.started` | A runner picks it up | | `job.completed` | It finishes with output | | `job.failed` | It stops with an error | | `job.cancelled` | Someone cancels it | | `job.delivery_succeeded` | A destination write succeeds | | `job.delivery_failed` | A destination write fails | | `job.deliveries_settled` | Every destination has resolved | | `balance.low` | The account balance crosses the low mark | | `balance.depleted` | The balance reaches zero | Pair `job.failed` with **Job > Get Logs** and the item carries the runner's log, including FFmpeg's stderr, so a Slack or email node can post the actual error. Pair `balance.low` with **Account > Get** to read how low. Rendobar must reach the webhook URL over HTTPS. That's true on n8n Cloud and on any publicly hosted instance. A `localhost` n8n is not reachable, so test locally through a tunnel such as `cloudflared tunnel --url http://localhost:5678` and set `WEBHOOK_URL` to its hostname. ## Can I use it as an AI Agent tool? The node is marked usable as a tool, so it attaches to the AI Agent node's tool port and n8n builds the tool schema from the node's own parameters. Nothing else to define. ![A Telegram Trigger feeds an AI Agent node, which sends its result back through Telegram. Three Rendobar tools hang off the agent's tool port: Account Get, Job Create and Job Get Logs. To the right, a Rendobar Trigger on job.failed feeds a second AI Agent that reads the logs.](https://mintcdn.com/rendobar/cp33P_FO3mIe3KIF/images/n8n/agent-tools.svg?fit=max\&auto=format\&n=cp33P_FO3mIe3KIF\&q=85\&s=688c6226030b8ee7b61b73e439bcd509) An agent with three Rendobar tools. Account Get reads the balance before the agent spends, Job Create runs the work, and Job Get Logs reads FFmpeg's stderr when a job fails. Three tools cover most agents: * **Account > Get** before anything that spends. `balance.amount` tells the agent how much it has and `plan.limits` what a submission is measured against. * **Job > Create** for the work itself. The agent picks the job type and fills the parameters. * **Job > Get Logs** after a `job.failed` event, so the agent reads the real FFmpeg error, fixes the command and resubmits. Give an agent **Callback URL** and a Wait node rather than **Wait for Completion**, for the same reason as above. On self-hosted n8n, community tools need `N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE=true`. ## Operations | Resource | Operation | What it does | | - | - | - | | Job | Create | Submit a job. Sends an idempotency key per item, so a retried step never double-creates. | | Job | Get | Fetch a job by ID, optionally downloading the headline file. | | Job | Get Many | List jobs with filters on status, type, client and date. | | Job | Get Logs | Read what the runner reported for a job. | | Job | Download Output | Fetch a completed job's file into a binary property. | | Job | Cancel | Stop a running job. | | File | Upload | Stream a binary to Rendobar and get a `url` to use as a job input. Uploads expire after 24 hours. | | Account | Get | Read the balance and plan limits. | | Storage Connection | Get Many | List the buckets connected on the Storage page. | | Storage File | Get Many | List the folders and files in a connected bucket, each with a `storage://` URI a job input accepts. | ### Storage Create Job's **Destinations** option writes the output to buckets connected on the [Storage page](https://app.rendobar.com/storage): one row per bucket, each with an optional folder or path. The job's `deliveries` reports every write. **Storage Connection** lists the connections, and **Storage File** lists the folders and files in one, each with a `storage://` URI that a job input accepts. See [Storage connections](/docs/storage). > [!NOTE] > > Destinations, the Storage resources and the three delivery events need version 0.6.0 or later. An install from the nodes panel gets the version n8n has verified, which can trail the latest npm release. ## Templates Ready-made workflows that import and run on n8n Cloud are listed on the [Automation overview](/docs/automation#templates). ## Troubleshooting > **Rendobar does not appear in the nodes panel** > > On n8n Cloud, an instance owner enables **Verified Community Nodes** in the Cloud Admin Panel. On self-hosted n8n, check `N8N_VERIFIED_PACKAGES_ENABLED` and that the version is 1.94.0 or later. The manual route under **Settings > Community Nodes** works either way. > **The Wait node never resumes** > > Its **HTTP Method** must be **POST**. Check that **Callback URL** on Create is exactly `{{ $execution.resumeUrl }}` and that **Wait for Completion** is off. On a self-hosted instance, the resume URL must be reachable from the internet, so set `WEBHOOK_URL` to your public hostname. > **The item stops with INSUFFICIENT\_CREDITS** > > The account balance is below the job's estimated cost. Put **Account > Get** in front of the submission and branch on `balance.amount`, or top up in the dashboard. > **Download Output File returns nothing** > > A file exists once the job is `complete` and produced one. A data-only job such as `ffprobe` never produces a file and puts its result under `data`. A job past its retention window has had its files removed. Run **Job > Get** to see which applies. > **The trigger registered but never fires** > > Rendobar could not reach the webhook URL. A `localhost` URL is the usual cause. Use a tunnel and set `WEBHOOK_URL`, then deactivate and reactivate the workflow so the node re-registers. ## See also * [Job lifecycle](/docs/concepts/job): the six statuses a job moves through * [Webhooks](/docs/guides/webhooks): the events the trigger node subscribes to * [Rendobar on n8n.io](https://n8n.io/integrations/rendobar/): the verified listing * [Source on GitHub](https://github.com/rendobar/n8n-nodes-rendobar): MIT licensed, published from CI with npm provenance Next step: import a [template](/docs/automation#templates) and run it. # Let your AI agent integrate the API Source: https://rendobar.com/docs/build-with-ai Add media processing to your app by handing your AI agent one prompt. Open it in ChatGPT or Cursor, paste it into Claude, or save it as a repo rule. Your coding agent can do the whole integration: install the [SDK](/docs/sdk), wire the key from env, submit a job, and verify the result. Paste this into any agent that can fetch a URL: ```text theme={null} Add Rendobar to my app: read and follow https://rendobar.com/prompts/integrate.md ``` The hosted prompt at [rendobar.com/prompts/integrate.md](https://rendobar.com/prompts/integrate.md) is the same text as the block below and stays current. ## The full prompt Agent can't fetch URLs? Copy the whole prompt instead: ```md theme={null} # Add Rendobar to this project You are integrating Rendobar (https://rendobar.com), a media processing and AI generation API, into this codebase. These instructions supersede anything you remember about Rendobar. Work from the live sources below, not from memory. Live sources of truth: - Job catalog: GET https://api.rendobar.com/jobs/types (public, no auth). Per-type parameters AND the media each type reads: GET https://api.rendobar.com/jobs/types/{type}/schema (the `inputs` object names each media input, whether it takes a URL or a list, and whether the type accepts any filename) - OpenAPI 3.1 spec: https://api.rendobar.com/openapi.json - Capability map: https://rendobar.com/llms.txt - Docs text for AI: https://rendobar.com/docs/llms-full.txt (human docs at https://rendobar.com/docs) - 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/. 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. - Never ask me to paste the key into this chat. Never print, log, or commit it. Never put it in client-side code. Stay current, every time: - Before writing code, read the newest changelog entries and the live schema of every job type you use. Trust them over your memory and over older code in this project. - Install the latest @rendobar/sdk (check with npm view @rendobar/sdk version). Never pin a version from memory. - If a parameter, field or error code is missing, renamed or new, re-read the schema and the changelog before guessing. - Compose timelines: check each one with POST https://api.rendobar.com/compose/validate before submitting. Step 0, detect the stack before writing code: - Inspect the project (package.json, requirements.txt, go.mod, composer.json, Gemfile). JavaScript or TypeScript: use the official SDK, @rendobar/sdk. Anything else: call the REST API directly using the OpenAPI spec above. Same endpoints, same shapes. - If the project is empty, ask me which stack I want. Steps (SDK path; mirror with plain HTTP on the REST path): 1. Install @rendobar/sdk. Create the client with createClient({ apiKey: process.env.RENDOBAR_API_KEY }). 2. Read the live job catalog and pick the job types this project needs from what it actually lists. Do not invent job types or parameters. 3. Submit with client.jobs.create({ type, inputs, params }). Media inputs are URLs. Pass an idempotencyKey anywhere a retry could double-submit. 4. Results: client.jobs.wait(job.id) is fine for scripts. For a production server, use webhooks: ask me to add an endpoint at https://app.rendobar.com/webhooks, then verify signatures with verifyWebhook from "@rendobar/sdk/webhooks". 5. Handle errors by machine code (error.code), never message text. INSUFFICIENT_CREDITS: tell me to top up at https://app.rendobar.com/billing. RATE_LIMITED: retry with backoff. Full list in the error reference. 6. Local files: upload through the assets flow (POST https://api.rendobar.com/assets, documented in the docs), then pass the returned asset url as the job input. 7. Definition of done: a runnable check that submits an ffprobe job on https://cdn.rendobar.com/assets/examples/sample.mp4 and prints the result. Read that job type's schema first, its parameters are not the same as every other type. Show me the one command that runs it. On a 401, check both causes before reporting: the key may not be reaching the process env, or the key itself may be invalid for this API (revoked, mistyped, or issued for a different environment such as staging). Tell me which one it is. Hard rules: - The API base is https://api.rendobar.com with no version prefix. There is no batch endpoint. One job produces one output. - Additive changes only. Do not refactor or remove unrelated code. - When something is ambiguous, choose the smallest default that keeps the app compiling and tell me what you chose. Ask me only when it is a product decision. If I am talking to you inside an MCP-capable client (Claude, Cursor, and others), also offer to connect Rendobar's MCP server for running jobs in conversation: https://api.rendobar.com/mcp over OAuth, or npx -y @rendobar/mcp with the key read from env. That is separate from the codebase integration above. For later work in this project, offer to install Rendobar's agent skills so future sessions start from current Rendobar knowledge: npx skills add rendobar/agent-skills (in Claude Code: /plugin marketplace add rendobar/agent-skills). ``` ## Open it in one click * [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](/docs/mcp-server) URL filled in. ## How do I keep it in my repo? Save the prompt as an agent rule so the context survives past the first session: * Cursor: `.cursor/rules/rendobar.md` * Claude Code: add it to `CLAUDE.md` or `AGENTS.md` * GitHub Copilot: `.github/copilot-instructions.md` ## Install the agent skills Skill-aware agents such as Claude Code can install Rendobar's skills instead of carrying a prompt: ```bash theme={null} npx skills add rendobar/agent-skills ``` Skills cover the same integration playbook and update independently of your repo. ## See also * [MCP server](/docs/mcp-server): run jobs from the chat over OAuth, no API key to copy * [API quickstart](/docs/quickstart): do the integration by hand in under 5 minutes * [SDK](/docs/sdk): the TypeScript client the prompt installs # Run FFmpeg from your terminal Source: https://rendobar.com/docs/cli Install the rb binary, sign in, and run any ffmpeg command in the cloud from your shell. Local files upload and download for you. `rb` runs FFmpeg in the cloud from your shell. Write the same `ffmpeg` command you'd run locally, prefix it with `rb`, and it uploads your inputs, runs the job, and downloads the result. ![Rendobar CLI rendering a video in the cloud](https://mintcdn.com/rendobar/74ie7vieHC_V5iZZ/images/cli-demo.gif?s=5230a01515806873c62b6e458729f08d) ```bash theme={null} curl -fsSL https://rendobar.com/install.sh | sh rb login rb ffmpeg -i clip.mp4 -vf scale=1280:720 -c:v libx264 -crf 23 out.mp4 ``` That's the whole loop. `rb login` opens your browser for OAuth. The render command uploads `clip.mp4`, runs an [`ffmpeg`](/docs/jobs/ffmpeg) job, and writes the output to `out.mp4`. Progress goes to stderr, so stdout stays clean for piping. For a Node or TypeScript program, or for any job type other than `ffmpeg`, use the [SDK](https://www.npmjs.com/package/@rendobar/sdk) instead. The CLI runs `ffmpeg` jobs and the auth around them, nothing else. ## Install **macOS / Linux** ```bash theme={null} curl -fsSL https://rendobar.com/install.sh | sh ``` **Windows** ```powershell theme={null} irm https://rendobar.com/install.ps1 | iex ``` The installer pulls the matching release from [github.com/rendobar/cli/releases](https://github.com/rendobar/cli/releases), checks a SHA-256, drops `rb` at `~/.rendobar/bin/rb` (or `%USERPROFILE%\.rendobar\bin\rb.exe`), and adds it to your PATH. Open a new shell, then verify: ```bash theme={null} rb --version rb doctor ``` To pin a version for a Dockerfile or provisioning script, set `RENDOBAR_VERSION` before piping: ```bash theme={null} RENDOBAR_VERSION=v1.0.0 curl -fsSL https://rendobar.com/install.sh | sh ``` `rb update` self-replaces the binary in place (SHA-256 verified, rolls back on failure). To remove `rb`, run `curl -fsSL https://rendobar.com/uninstall.sh | sh` (or the `.ps1` on Windows). ## Authenticate The CLI reads credentials in priority order: the `RENDOBAR_API_KEY` env var, then a saved API key, then a saved OAuth token. ```bash theme={null} rb login # browser OAuth, saves a refreshing token rb login --key rb_xxx # paste a key for headless or SSH sessions rb whoami # print org, plan, balance rb logout # revoke the token and delete the credential file ``` `rb login` runs an OAuth 2.1 PKCE flow on `http://127.0.0.1:14832/callback`. Access tokens expire after 1 hour and refresh automatically, so you won't see a re-login prompt unless the token is revoked. Credentials live at `~/.config/rendobar/credentials.json` (`%APPDATA%\rendobar\credentials.json` on Windows). Treat that file as a secret. Get an API key from [app.rendobar.com](https://app.rendobar.com) under Settings, API keys. Keys start with `rb_`. ## Commands | Command | What it does | | - | - | | `rb ffmpeg ` | Run FFmpeg in the cloud. Pass real `ffmpeg` flags. Local `-i` inputs upload automatically | | `rb storage list` | List the buckets connected on the [Storage page](https://app.rendobar.com/storage) | | `rb storage ls [/folder]` | List folders and files in a connected bucket, each with its `storage://` URI. `--all` reads every page | | `rb login [--key ]` | Sign in via browser OAuth, or save an API key with `--key` | | `rb logout` | Revoke the token and delete the credential file | | `rb whoami` | Print the current org, plan, and balance | | `rb doctor [--fix]` | Run install and connectivity checks. `--fix` clears the macOS quarantine bit | | `rb update` | Update `rb` to the latest release | ### rb ffmpeg flags These flags belong to `rb ffmpeg`. Everything else after the subcommand passes through to FFmpeg verbatim. | Flag | Effect | | - | - | | `--json` | Print the full job result as JSON to stdout | | `--url-only` | Print only the signed result URL to stdout | | `--quiet` | Print nothing. Read the exit code | | `--no-wait` | Submit and exit, printing the job ID | | `--timeout N` | Server-side execution timeout in seconds. Range 1 to 900, default 120 | | `--deliver ` | Also write the output to connected storage, for example `storage://prod-media/exports`. Repeat for more buckets. Also on `rb generate` and `rb edit` | Local inputs (`-i ./clip.mp4`) upload before submission and are rewritten to short-lived URLs. Remote inputs (`-i https://...`) pass through untouched. The trailing output filename is where the result downloads to. A delivery starts after the job completes. rb waits up to five minutes for it and prints where each one landed. `rb storage` needs storage access. If you signed in before rb asked for it, run `rb login` again, or use an API key made on or after September 13, 2026. ### Exit codes | Code | Meaning | | - | - | | `0` | Success | | `1` | Job failed, a delivery did not land, or a generic runtime error | | `2` | Not authenticated, bad arguments, port in use, or insufficient credits | | `130` | Cancelled with `Ctrl+C`. The CLI tries to cancel the in-flight job first | ## How do I run it in CI? `RENDOBAR_API_KEY` is the only auth path for CI. It beats any saved credential and writes nothing to disk. Don't run `rb login` in a pipeline, the browser flow will hang. ```yaml title=".github/workflows/render.yml" theme={null} jobs: render: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rendobar CLI run: | curl -fsSL https://rendobar.com/install.sh | sh echo "$HOME/.rendobar/bin" >> "$GITHUB_PATH" env: RENDOBAR_VERSION: v1.0.0 - name: Render run: rb ffmpeg -i ./input.mp4 -c:v libx264 -crf 23 out.mp4 env: RENDOBAR_API_KEY: ${{ secrets.RENDOBAR_API_KEY }} - uses: actions/upload-artifact@v4 with: name: rendered path: out.mp4 ``` `install.sh` updates PATH via shell rc files that later steps don't read, so the `echo … >> "$GITHUB_PATH"` line carries the install dir forward. The same pattern works in GitLab CI or Docker: install with `RENDOBAR_NO_MODIFY_PATH=1`, pass the key as a masked secret, and parse output with `--json` or `--url-only`. ```bash theme={null} URL=$(rb ffmpeg --url-only -i in.mp4 -c:v libx264 out.mp4) rb ffmpeg --json -i in.mp4 -c:v libx264 out.mp4 | jq -r '.id' ``` The CLI has no `--idempotency-key`. If a retried step must not double-charge, submit via the [SDK](https://www.npmjs.com/package/@rendobar/sdk) or [`POST /jobs`](/docs/jobs/ffmpeg) with an `idempotencyKey` field. ## What do I do when a command fails? `rb doctor` runs the install and connectivity checks and prints a `fix:` line under any failure. Run it first. The three you'll actually hit: > **rb: command not found after install** > > The shell hasn't reloaded. Open a new terminal, or `source ~/.zshrc` / `~/.bashrc`. If you set `RENDOBAR_NO_MODIFY_PATH=1` or a custom `RENDOBAR_INSTALL_DIR`, add it yourself: `export PATH="$HOME/.rendobar/bin:$PATH"`. > **curl 403 or 429 during install** > > GitHub Releases rate-limits at 60/hr per IP when unauthenticated. Pass a token: `RENDOBAR_GITHUB_TOKEN=ghp_xxx curl -fsSL https://rendobar.com/install.sh | sh`. In GitHub Actions the default `GITHUB_TOKEN` is picked up automatically. > **Login fails or port 14832 is in use** > > Another process holds the OAuth callback port, or your network blocks loopback HTTP. Skip the browser entirely: `rb login --key rb_xxx`. A `Refresh failed: invalid_grant` means the token was revoked, so run `rb logout && rb login`. For anything else, run `rb doctor --json` and file a bug at [github.com/rendobar/cli/issues](https://github.com/rendobar/cli/issues). ## See also * [FFmpeg job reference](/docs/jobs/ffmpeg): the API the CLI calls per run * [API authentication](/docs/quickstart): keys and OAuth across REST and MCP * [SDK](https://www.npmjs.com/package/@rendobar/sdk): every job type, typed, for Node and TypeScript * [Changelog](https://rendobar.com/changelog/): version history and release notes # Credits and billing Source: https://rendobar.com/docs/concepts/credits 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. 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 | 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 | 20 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 | | Faster output delivery (an output of 512 MB or more streams into your bucket while it uploads) | No | No | 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 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 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 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 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`) 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? Because the submit gate checks `balance > 0` rather than an estimate, a single long-running job can drive your balance briefly negative when the terminal debit lands. When that happens, further submits are blocked with `402 INSUFFICIENT_CREDITS` until you top up. Once your balance is positive again, jobs are accepted immediately. Nothing else on the account changes. ## 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. 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: ```json theme={null} { "error": { "code": "INSUFFICIENT_CREDITS", "message": "Balance is depleted. Add credits at https://app.rendobar.com/billing to keep submitting jobs." } } ``` HTTP `402`. Top up at [app.rendobar.com/billing](https://app.rendobar.com/billing). ## How do I check my balance? ```bash theme={null} curl https://api.rendobar.com/billing/state \ -H "Authorization: Bearer rb_YOUR_KEY" ``` ```json theme={null} { "data": { "balance": { "amount": 5.0 }, "plan": { "slug": "free", "name": "Free" }, "subscription": { "status": "active", "cancelAtPeriodEnd": false }, "isPro": false } } ``` `balance.amount` is in dollars. Internally balances are stored in nanodollars (1 USD = 1,000,000,000). ## How do I track usage? ```bash theme={null} curl "https://api.rendobar.com/billing/usage?start=2026-02-01&end=2026-02-09" \ -H "Authorization: Bearer rb_YOUR_KEY" ``` Returns daily usage by job type: count, amount charged, and compute seconds. ## Practical advice * **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](/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](/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](/docs/support/faq): common pricing and billing questions # The Generation API Source: https://rendobar.com/docs/concepts/generation Generate images from a text prompt or edit them from an instruction on hosted diffusion models. One async API, one job shape, many models. Rendobar's Generation API creates new media from a prompt on hosted diffusion models. It is one of the platform's products, and it submits, runs, and returns like every other job. You describe what you want, the job runs on a GPU, and the result comes back as a signed file URL. ## Modalities Images are the first modality. You can generate an image from a text prompt, or edit existing images from a written instruction. > **[Image generate](/docs/jobs/image-generate)** > > Turn a text prompt into a webp image. Pick a tier or pin an exact model. > **[Image edit](/docs/jobs/image-edit)** > > Edit one to four reference images from a plain-language instruction. More modalities will join the same surface over time. Only image generation and editing are available today. ## Should I pick a tier or an exact model? Every generation job takes a `model`. You can name a tier and let the platform choose, or pin an exact model id. * **Tiers** (`economy`, `standard`, `premium`) express a price and quality posture without naming a model. Omit `model` and you get `economy`. * **Exact ids** (for example `qwen-image-2512`) pin the model and unlock its own controls, such as denoise steps, guidance, and a negative prompt. Tier aliases can be re-pointed to newer models as the catalog grows. Pin an exact id when you need a result to stay stable across that change. ## How do I discover models? `GET /models` returns the live catalog: every model with its supported jobs, tier, relative price, capabilities, and step range. Filter to one job type with `?job=`. Build a model picker against this endpoint instead of hardcoding the list. ```bash theme={null} curl "https://api.rendobar.com/models" \ -H "Authorization: Bearer rb_YOUR_KEY" ``` ```json theme={null} { "data": [ { "id": "qwen-image-2512", "jobs": ["image.generate"], "tier": "premium", "underReview": false, "priceTier": "$$$$", "maxRefImages": 0, "steps": { "default": 50, "min": 20, "max": 50 }, "supports": { "negativePrompt": true, "guidance": true }, "enhanceDefault": false, "status": "active" } ] } ``` `priceTier` compares models against each other, from `$` to `$$$$`. It is not a per-image charge, because jobs bill on the compute they actually use, cost-plus. See [credits and billing](/docs/concepts/credits). A model with `underReview: true` is pulled from tier resolution while we re-evaluate its cost and quality. It stays listed so a caller pinning it can see why the submit was rejected, but no tier resolves to it and pinning it returns `VALIDATION_ERROR`. ## Does every model use the same job shape? A generation job is a [job](/docs/concepts/job) like any other. You submit it to `POST /jobs`, it runs async, and you poll, [wait](/docs/sdk#wait), or receive a [webhook](/docs/guides/webhooks). The image jobs return a single webp file in `output.file`, with `output.data` set to `null`. One thing is specific to generation. While the model denoises, the job emits `job.preview` events carrying a small frame of the image as it resolves, so a client can show the picture arriving instead of a spinner. Previews are decoration: they are never replayed, and a model can emit none. See [watch it render](/docs/jobs/image-generate#watch-it-render). ```ts theme={null} import { createClient, outputUrl } from "@rendobar/sdk"; const client = createClient({ apiKey: "rb_YOUR_KEY" }); const job = await client.jobs.run({ type: "image.generate", params: { prompt: "A paper boat on a still pond at dawn", model: "standard" }, }); console.log(outputUrl(job)); // signed URL to the webp ``` ## See also * [Image generate](/docs/jobs/image-generate): text to image, with the full model catalog * [Image edit](/docs/jobs/image-edit): instruction editing with one to four reference images * [Image upscale](/docs/jobs/image-upscale): enlarge an image without a prompt * [How a job works](/docs/concepts/job): statuses and the output shape * [SDK](/docs/sdk): `jobs.run`, `jobs.wait`, and reading the output * [Credits and billing](/docs/concepts/credits): how compute-based billing works # How a job works Source: https://rendobar.com/docs/concepts/job Run a job from a typed request, through async execution on our runners, to a signed output. The lifecycle, the statuses, and the output shape, in one place. A job is one unit of media work. You describe it, submit it, and it runs on its own while you poll or wait for a webhook. Everything on Rendobar is a job, whether that is an FFmpeg command or animated captions. They all submit the same way and they all return the same shape. Here is the whole round trip: ```mermaid theme={null} sequenceDiagram participant You participant API as Rendobar API participant Runner You->>API: POST /jobs API-->>You: id, status waiting API->>Runner: dispatch with signed URLs Runner->>Runner: download, run, upload Runner-->>API: result callback API-->>You: webhook job.completed You->>API: poll GET /jobs API-->>You: status complete, output ``` ## What does a job look like? Every job is a `type` plus the `inputs` and `params` that type expects: ```json theme={null} { "type": "ffmpeg", "inputs": { "source": "https://..." }, "params": { "command": "ffmpeg -i source out.mp4" } } ``` `type` picks the operation, `inputs` maps names to source URLs, and `params` carries the options for that type. `POST` it to `/jobs` and you get back an id and a `waiting` status. Each [job type](/docs/jobs/ffmpeg) documents its own `inputs` and `params`. To also write the output to a bucket you own, add `destinations`. An input can come from that bucket too, as a `storage://` URI: ```json theme={null} { "type": "ffmpeg", "inputs": { "source": "storage://demo-bucket/raw/clip.mp4" }, "params": { "command": "ffmpeg -i source out.mp4" }, "destinations": ["storage://demo-bucket/exports"] } ``` See [storage connections](/docs/storage) for connecting a bucket and how outputs are named. ## What are the job statuses? A job moves through six statuses. Three are terminal. ```mermaid theme={null} flowchart LR waiting --> dispatched --> running --> complete running --> failed waiting --> cancelled dispatched --> cancelled running --> cancelled ``` | Status | What it means | | - | - | | `waiting` | Saved and queued. The submit gate passed: auth, plan and rate limits, a balance check, and schema validation. | | `dispatched` | Sent to a runner with presigned URLs for download and upload. | | `running` | The runner is downloading inputs, running the work, and uploading the result. | | `complete` | Finished. The `output` is ready. | | `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 ([compose](/docs/jobs/compose/pricing) bills per second of output it delivers, and its failures follow their own rule), and a [webhook](/docs/guides/webhooks) fires if you configured one. A job that never started settles free. Failures carry an `error.code` from the [error catalogue](/docs/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? A `complete` job carries one `output`. The shape is the same for every type, so you never branch on the job type to find the result. ```json theme={null} { "data": | null, "file": | null, "files": [], "expiresAt": | null } ``` **data** `object | null` The computed answer for jobs that return one, such as a probe result or a transcript. `null` for jobs that only write files. **file** `File | null` The headline result you play or download: a single file, or a stream manifest (`.m3u8` / `.mpd`). `null` for data-only jobs and pure file sets. Always one of `files`. **files** `File[]` Every file the job produced. One entry for a single output, the manifest plus segments for a stream, every member for a set. `[]` for data-only jobs. **expiresAt** `integer | null` Unix ms when the URLs expire. Re-fetch the job with `GET /jobs/{id}` to mint fresh ones. > [!TIP] > > Expecting one output? Read `output.file.url` and you are done. Handling a job that can write many files (frames, HLS segments, a resolution ladder)? Iterate `output.files`. The two always agree: `file`, when set, is one of `files`. ### The File type Each entry in `file` and `files` is the same shape: ```json theme={null} { "url": "https://api.rendobar.com/dl/job_abc123?token=", "path": "output.mp4", "type": "video", "size": 4194304, "sha256": "9f2c41d0b7a3e5f48c1d6e02a97b3c5f1e8d4a60b2c7f9e3d5a1b8c4e6f0a41e", "meta": { "format": "mp4", "width": 1280, "height": 720, "durationMs": 30000 } } ``` `url` is a signed link that expires at `output.expiresAt`. `path` is the filename the job wrote. `type` is an open enum (`video`, `image`, `audio`, `captions`, `playlist`, `data`, `other`), so tolerate values you do not recognize. `size` is bytes, and `meta` carries probed `format`, `width`, `height`, and `durationMs` when known. `sha256` is the SHA-256 of the file's bytes as 64 lowercase hex characters. Hash your download, or the copy delivered to your own bucket, and compare the two strings to confirm you have the file the job produced. The field is always present. It is `null` for a job that completed before 10 October 2026 and for the individual files of a multi-file output. ### Four patterns, one shape Which fields are populated depends on what the job produced. **Single file** The common case for transform jobs. `file` is the output, `files` lists it, `data` is null. ```json theme={null} { "data": null, "file": { "url": "https://api.rendobar.com/dl/job_abc123?token=", "path": "output.mp4", "type": "video", "size": 4194304, "sha256": "9f2c41d0b7a3e5f48c1d6e02a97b3c5f1e8d4a60b2c7f9e3d5a1b8c4e6f0a41e" }, "files": [ { "url": "https://api.rendobar.com/dl/job_abc123?token=", "path": "output.mp4", "type": "video", "size": 4194304, "sha256": "9f2c41d0b7a3e5f48c1d6e02a97b3c5f1e8d4a60b2c7f9e3d5a1b8c4e6f0a41e" } ], "expiresAt": 1735689600000 } ``` **Stream** An HLS or DASH stream. `file` is the manifest, typed `playlist`. Point a player at `file.url`. `files` carries the manifest plus every segment. ```json theme={null} { "data": null, "file": { "url": "https://api.rendobar.com/v/job_abc123//master.m3u8", "path": "master.m3u8", "type": "playlist", "size": 412, "sha256": null }, "files": [ { "url": "https://api.rendobar.com/v/job_abc123//master.m3u8", "path": "master.m3u8", "type": "playlist", "size": 412, "sha256": null }, { "url": "https://api.rendobar.com/v/job_abc123//seg_000.ts", "path": "seg_000.ts", "type": "video", "size": 1048576, "sha256": null } ], "expiresAt": 1735689600000 } ``` **Set** An unordered set with no single headline, such as an image sequence or a resolution ladder. `file` is `null`, read `files`. ```json theme={null} { "data": null, "file": null, "files": [ { "url": "https://api.rendobar.com/v/job_abc123//frame_000.png", "path": "frame_000.png", "type": "image", "size": 204800, "sha256": null }, { "url": "https://api.rendobar.com/v/job_abc123//frame_001.png", "path": "frame_001.png", "type": "image", "size": 205120, "sha256": null } ], "expiresAt": 1735689600000 } ``` **Data only** A job that computes an answer instead of writing files. `data` carries it, the rest are empty. ```json theme={null} { "data": { "width": 1280, "height": 720, "durationMs": 30000, "codec": "h264", "fps": 30 }, "file": null, "files": [], "expiresAt": null } ``` ### Reading it The read path is the same every time. No job-type discriminator. ```ts theme={null} const { data: job } = await res.json(); if (job.output.data) console.log(job.output.data); // computed answer if (job.output.file) console.log(job.output.file.url); // the thing to play or download for (const f of job.output.files) console.log(f.path, f.type, f.size); // the full list ``` Three invariants hold for every type, so you can code against them: `output.file` is always one of `output.files` (or `null`), `expiresAt` is set whenever `files` is non-empty, and a `complete` job always has `output.data` or `output.files`. It never returns nothing. ## Should I poll or use webhooks? Poll `GET /jobs/{id}` every second or two until the status is terminal, or skip polling entirely: register a [webhook](/docs/guides/webhooks) and Rendobar pushes `job.completed` to your server the moment the job lands. ## See also * [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 # Scopes and access Source: https://rendobar.com/docs/concepts/scopes Configure an API key to hold only what your code needs, see what an app asks for before you allow it, and read the 403 that names a missing scope. Every credential carries scopes, and every endpoint states the one it needs. What a caller may do comes from what it was granted, not from whether it authenticated with an API key, an OAuth token or a dashboard session. ## Which scopes exist? Seven resources, each with a read and a write scope. | Resource | Read | Write | Write lets it | | - | - | - | - | | Jobs | `jobs:read` | `jobs:write` | Run jobs, which spends credits | | Assets | `assets:read` | `assets:write` | Upload and delete files | | Webhooks | `webhooks:read` | `webhooks:write` | Change where events are delivered | | Billing | `billing:read` | `billing:write` | Change the plan and what you are charged | | Organization | `orgs:read` | `orgs:write` | Add and remove members | | API keys | `keys:read` | `keys:write` | Create keys that act outside the session | | Storage | `storage:read` | `storage:write` | Connect, change and remove buckets | Two rules cover the rest: * **`write` includes `read`.** A credential holding `jobs:write` can list jobs. You never need both. * **Self-description is never scoped.** Any credential can ask which workspace it acts for and what limits apply, whatever else it holds. ## How do I narrow an API key? Pass `scopes` when you create a key and it can do only that. ```ts SDK theme={null} const key = await rendobar.apiKeys.create({ name: "CI deploy key", scopes: ["jobs:write", "assets:write"], }); key.scopes; // what it actually got ``` ```bash cURL theme={null} curl -X POST https://api.rendobar.com/api-keys \ -H "Authorization: Bearer $RENDOBAR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"CI deploy key","scopes":["jobs:write","assets:write"]}' ``` Omit `scopes` and the key gets everything the credential creating it can pass on, which for a dashboard session is everything a key may hold. The response reports what the key actually received, which is not always what was asked for. Two limits apply to every key, however it was made: * **A key can never hold `keys:write`.** A key that can create keys survives its own revocation, so one leak becomes permanent access. Create and revoke keys from the dashboard, or from an app you granted key management to. * **A key is never broader than what created it.** Asking for more than the creating credential holds is refused rather than quietly narrowed. ## What is an app asking for? When you connect an app over OAuth, the consent screen lists what it requested, area by area, with what each one lets it do. You can grant less than it asked for, and the app receives exactly what you allowed. A grant is bound to **one workspace**, the one named on the screen when you approved it. The same app connected to a second workspace is a second grant. [Account, Security](https://app.rendobar.com/account/security) lists every connected app with what it can reach and where. Revoking deletes the grant and every stored token, so the app cannot get new access or renew what it has. An access token it already holds stays valid until it expires, which is under an hour. ## What happens when a scope is missing? A request outside a credential's scopes is refused with `403` and a header naming what was missing, per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750#section-3.1). ```http theme={null} HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", error_description="This endpoint requires the jobs:write scope.", scope="jobs:write" ``` ```json theme={null} { "error": { "code": "INSUFFICIENT_SCOPE", "message": "This endpoint requires the jobs:write scope." } } ``` It is a `403`, not a `401`: the credential authenticated correctly and presenting it again cannot help. Read the `scope` parameter to see what to ask for, then create a key that carries it or reconnect the app and allow that area. Storage scopes were added on 2026-09-13. Keys created and apps connected before then do not hold them, so the [storage](/docs/storage) endpoints and `client.storage` answer them with this `403`. Create a new key, or reconnect the app and allow Storage. A job that reads from or delivers to a connected bucket needs only `jobs:write`, so existing keys still run those. ## See also * [MCP server](/docs/mcp-server): connecting an app over OAuth * [Error codes](/docs/support/errors): every code the API returns * [Plan limits](/docs/support/limits): what a plan allows, which is a separate question from what a credential may do # Per-job completion callbacks Source: https://rendobar.com/docs/guides/callbacks Add a completion callback to a single job, POST the result to a per-job URL, and resume a Cloudflare Workflow without polling. A callback attaches a completion hook to one [job](/docs/concepts/job). You pass `callback.url` when you create the job, and the moment that job reaches a terminal state Rendobar POSTs the result there. The body is byte-identical to the [webhook](/docs/guides/webhooks) envelope, so the same receiver code handles both. The correlation lives in the URL you chose, so the receiver stays stateless and never polls `GET /jobs/{id}`. ## How do I attach a callback? Add a `callback` object to `client.jobs.create(...)`. The only required field is `url`. ```ts theme={null} import { createClient } from "@rendobar/sdk"; const client = createClient({ apiKey: process.env.RENDOBAR_API_KEY }); await client.jobs.create({ type: "ffmpeg", params: { command: "ffmpeg -i input.mp4 -t 1 output.mp4" }, callback: { url: "https://your-app.example.com/rb/job_a1b2c3d4" }, }); ``` | Field | Type | Purpose | | - | - | - | | `callback.url` | string, HTTPS | Where Rendobar POSTs the job envelope on any terminal state (`complete`, `failed`, `cancelled`). SSRF-validated, HTTPS only. | | `callback.headers` | object, optional | Extra request headers on the POST, for example `{ Authorization: "Bearer ..." }`, so the callback can hit an authenticated target directly. | | `callback.verify` | boolean, optional | Sign the POST with `X-Rendobar-Signature`. Default `false`. | | `callback.events` | string\[], optional | Opt into extra non-terminal events. The only value today is `"job.started"`. | > [!NOTE] > > Terminal events (`complete`, `failed`, `cancelled`) always fire and cannot be filtered out, so a step waiting on a callback can never hang. `callback.events` only adds non-terminal pings on top. The one available today is `"job.started"`, a progress signal sent when the job begins running. A job with `destinations` also sends `job.deliveries_settled` to `callback.url` once every destination has resolved. It is the only [delivery event](/docs/storage#delivery-events) a callback receives. Custom headers let the POST reach an authenticated endpoint with no receiver of your own. Framing headers and any `X-Rendobar-*` header are reserved. If you set one, the platform's value wins. > [!WARNING] > > Rendobar never returns `callback.url` or `callback.headers` in any API response. Both are secrets. `GET /jobs/{id}` reports only `callback.status`. ## Resume a Cloudflare Workflow Callbacks were built for this pattern. A Cloudflare Workflow dispatches a job and suspends until it finishes, with no polling in between. There are two shapes. ### Shape A: receiver Worker with sendEvent The Workflow's own `fetch` handler is the receiver. The callback URL carries `event.instanceId`, the receiver route turns the POST into `instance.sendEvent(...)`, and that resumes the suspended `waitForEvent`. This exact suspend and resume loop was verified end to end. ```ts theme={null} import { WorkflowEntrypoint, WorkflowStep, WorkflowEvent } from "cloudflare:workers"; import { createClient } from "@rendobar/sdk"; export class RenderJob extends WorkflowEntrypoint { async run(event: WorkflowEvent<{ source: string }>, step: WorkflowStep) { await step.do("submit rendobar job", async () => { const client = createClient({ apiKey: this.env.RENDOBAR_API_KEY }); await client.jobs.create({ type: "ffmpeg", params: { command: `ffmpeg -i ${event.payload.source} -t 1 output.mp4` }, // The instance id in the path is how the callback finds this exact run. callback: { url: `https://your-worker.example.com/rb/${event.instanceId}` }, }); }); // Suspend the whole workflow until Rendobar calls back. No polling. const done = await step.waitForEvent("await rendobar callback", { type: "rb-job", timeout: "10 minutes", }); return { url: done.payload?.data?.output?.file?.url }; } } export default { async fetch(req: Request, env: Env): Promise { const url = new URL(req.url); const m = url.pathname.match(/^\/rb\/(.+)$/); if (req.method === "POST" && m) { const instance = await env.RENDER_JOB.get(m[1]); await instance.sendEvent({ type: "rb-job", payload: await req.json() }); return new Response("ok"); } return new Response("not found", { status: 404 }); }, }; ``` ### Shape B: no receiver Worker Point `callback.url` straight at Cloudflare's Workflows "send event" REST endpoint for the instance, and put your Cloudflare API token in `callback.headers`. Rendobar POSTs the job result directly to Cloudflare, so there is no shim to deploy. This is the zero-infrastructure option. The caller authenticates the callback itself through the header, so signing is usually unnecessary here. ```ts theme={null} await client.jobs.create({ type: "ffmpeg", params: { command: `ffmpeg -i ${source} -t 1 output.mp4` }, callback: { url: `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/workflows/render-job/instances/${instanceId}/events/rb-job`, headers: { Authorization: `Bearer ${CF_API_TOKEN}` }, }, }); ``` The waiting Workflow is the same as Shape A. It calls `waitForEvent({ type: "rb-job" })` and resumes when Cloudflare delivers the event. ## How do I secure a callback? Callbacks are unsigned by default because the URL itself is the capability. Set `callback.verify: true` to sign the POST, then check it on your receiver. The signature is the same HMAC-SHA256 scheme as webhooks, over `${timestamp}.${body}`, and the SDK ships the same verifier. ```ts theme={null} import { verifyCallback } from "@rendobar/sdk/webhooks"; // Raw body (a string, not parsed JSON) plus the request headers. const ok = await verifyCallback(rawBody, req.headers, process.env.CALLBACK_SECRET); if (!ok) throw new Error("invalid signature"); ``` `verifyCallback` is the same function as `verifyWebhook`. Fetch the organization callback signing secret once and store it in your secret manager. ```ts theme={null} // GET /orgs/current/callback-secret -> { data: { secret: "cbsec_..." } } const { data } = await client.orgs.getCallbackSecret(); ``` Rotate the secret with `POST /orgs/current/callback-secret/rotate`. Reach for signing when the callback lands on an endpoint you own and you want to prove the POST came from Rendobar. For Shape B, where you already authenticate to Cloudflare through `callback.headers`, signing usually adds nothing. ## How do I watch delivery? Once a job carries a callback, `GET /jobs/{id}` reports its delivery state under `callback.status`. | Status | Meaning | | - | - | | `pending` | The job is not terminal yet, or the callback is queued for delivery | | `delivered` | The receiver returned `2xx` | | `failed` | Delivery exhausted its retries and moved to the dead-letter path | The dashboard job detail view shows the same status. Delivery is at-least-once, with retries and a dead-letter path, so the same event can arrive more than once. Deduplicate on the `X-Rendobar-Delivery` header, a `whd_...` id that is unique per delivery. Every POST carries these headers. ``` X-Rendobar-Event: job.completed X-Rendobar-Delivery: whd_x1y2z3 X-Rendobar-Timestamp: 1707436815 X-Rendobar-Attempt: 1 ``` ## How do callbacks compare to org webhooks? [Webhooks](/docs/guides/webhooks) and callbacks carry the identical envelope and verify with the same code. The difference is scope. A webhook is one static URL registered in the dashboard that receives events for every job in the organization. A callback is a per-job URL, set at create time, that receives events for exactly one job. Webhooks fit a central event pipeline. Callbacks fit an orchestrator that dispatches a job and waits on that one job, such as the Cloudflare Workflow above. ## Related * [Webhooks for job events](/docs/guides/webhooks): the shared envelope, signature scheme, and retry behavior * [Job output](/docs/concepts/job#the-output): the `output` and `error` shape a callback carries * [Job lifecycle](/docs/concepts/job): what each terminal state means * [Changelog](https://rendobar.com/changelog/): callback and webhook payload changes # Webhooks for job events Source: https://rendobar.com/docs/guides/webhooks Receive job and account events as signed HMAC POSTs. Create an endpoint in the dashboard, verify with one SDK call, and handle retries. Webhooks push events to your server the moment a [job](/docs/concepts/job) changes status or your balance runs low. No polling. Rendobar POSTs a signed JSON payload, your server verifies it and reacts. > [!NOTE] > > Want a hook scoped to a single job instead of every job in the organization? See [per-job callbacks](/docs/guides/callbacks). ## How do I create an endpoint? Create and manage endpoints on the [**Webhooks page**](https://app.rendobar.com/webhooks) in the dashboard. 1. Open [**Webhooks**](https://app.rendobar.com/webhooks) and click **New Endpoint**. 2. Name it, paste your **HTTPS** URL, and check the events you want. New endpoints start with `job.completed`, `job.failed`, and `job.cancelled` selected. 3. Click **Create Endpoint**, then copy the signing secret (`whsec_...`). It is shown once, so store it in your secret manager. Each endpoint gets a card where you edit events, rotate the secret, send a test delivery, and re-send anything that failed. An organization can have up to 10. > [!TIP] > > Hit **Send Test** on the card to fire a sample event and confirm your receiver returns `2xx` before real jobs start flowing. ## Events | Event | When | | - | - | | `job.created` | Job accepted and queued | | `job.started` | Job started executing on a runner | | `job.completed` | Job finished successfully. Carries the unified `output` and `cost` | | `job.failed` | Job failed. Carries the unified `error` (`code`, `message`, `detail`, `retryable`) | | `job.cancelled` | Job was cancelled | | `job.delivery_succeeded` | The output was written to one [storage destination](/docs/storage#delivery-events). Carries `storageId`, `path`, and `url` | | `job.delivery_failed` | Writing the output to one storage destination failed. Carries `storageId` and the [`reason`](/docs/storage#delivery-errors) | | `job.deliveries_settled` | Every storage destination of the job has resolved. Sent once, with every outcome | | `balance.low` | Credit balance crossed the low threshold | | `balance.depleted` | Credit balance reached zero | ## How do I verify a delivery? Rendobar signs each payload. Check the signature before you trust the body. The SDK does it in one call. ```ts theme={null} import { verifyWebhook } from "@rendobar/sdk/webhooks"; // Raw body (a string, not parsed JSON) plus the request headers. 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](#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. ```ts theme={null} import express from "express"; import { verifyWebhook } from "@rendobar/sdk/webhooks"; const app = express(); app.post( "/webhooks/rendobar", express.raw({ type: "application/json" }), async (req, res) => { const raw = req.body.toString("utf8"); if (!(await verifyWebhook(raw, req.headers, process.env.WEBHOOK_SECRET))) { return res.status(401).send("invalid signature"); } res.status(200).send("ok"); // ack fast, then work off the request path const { event, data } = JSON.parse(raw); if (event === "job.completed") console.log(data.jobId, data.output.file?.url); }, ); app.listen(3000); ``` Not on Node? The signature is HMAC-SHA256 over `{timestamp}.{body}`, hex-encoded, in `X-Rendobar-Signature` as `sha256=`. ```javascript Node.js theme={null} import { createHmac, timingSafeEqual } from "crypto"; function verify(body, signature, timestamp, secret) { const message = `${timestamp}.${body}`; const expected = createHmac("sha256", secret).update(message).digest("hex"); const received = signature.replace("sha256=", ""); return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex")); } ``` ```python Python theme={null} import hmac, hashlib def verify_webhook(body: bytes, signature: str, timestamp: str, secret: str) -> bool: message = f"{timestamp}.{body.decode()}" expected = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature.replace("sha256=", "")) ``` > [!WARNING] > > Compare with a timing-safe function (`timingSafeEqual`, `hmac.compare_digest`). Plain string equality leaks the signature one byte at a time. ## What is in the payload? Every delivery carries these headers. ``` X-Rendobar-Signature: sha256=abc123... X-Rendobar-Timestamp: 1707436815 X-Rendobar-Event: job.completed X-Rendobar-Delivery: whd_x1y2z3 X-Rendobar-Attempt: 1 ``` The body is a versioned envelope. The envelope fields identify the event and delivery. The event payload lives under `data`, which for job events matches [GET /jobs/](/docs/concepts/job#the-output). | Field | Meaning | | - | - | | `version` | Envelope schema version (currently `"1"`) | | `event` | The event name | | `deliveryId` | Unique per delivery. Use it to deduplicate | | `timestamp` | When the envelope was built (unix ms) | | `orgId` | Your organization ID | | `data` | Event-specific payload | A `job.completed` delivery. `output.file.url` is a signed, time-limited URL, and `output.files` lists every produced file. ```json theme={null} { "version": "1", "event": "job.completed", "deliveryId": "whd_x1y2z3", "timestamp": 1707436815000, "orgId": "org_abc123", "data": { "jobId": "job_a1b2c3d4", "jobType": "ffmpeg", "status": "complete", "output": { "data": null, "file": { "url": "https://r2.rendobar.com/...", "path": "output.mp4", "type": "video", "size": 15234567, "sha256": "9f2c41d0b7a3e5f48c1d6e02a97b3c5f1e8d4a60b2c7f9e3d5a1b8c4e6f0a41e", "meta": { "format": "mp4", "width": 1920, "height": 1080, "durationMs": 127500 } }, "files": [{ "url": "https://r2.rendobar.com/...", "path": "output.mp4", "type": "video", "size": 15234567, "sha256": "9f2c41d0b7a3e5f48c1d6e02a97b3c5f1e8d4a60b2c7f9e3d5a1b8c4e6f0a41e" }], "expiresAt": 1707440415000 }, "cost": { "amount": 50000000, "currency": "USD", "formatted": "$0.05" }, "timing": { "createdAt": 1707436800000, "startedAt": 1707436805000, "completedAt": 1707436815000 } } } ``` The `output` shape is identical for every job type. A data-only job (such as `ffprobe`) puts its answer in `output.data` with `file` null. See [Job output](/docs/concepts/job#the-output) for each pattern. For `job.failed`, `data` carries `error` instead of `output`. ```json theme={null} "data": { "jobId": "job_a1b2c3d4", "jobType": "ffmpeg", "status": "failed", "error": { "code": "RUNNER_ERROR", "message": "FFmpeg process exited with code 1", "detail": "Invalid data found when processing input", "retryable": false } } ``` ## What happens when a delivery fails? If your endpoint does not return `2xx` within 10 seconds, Rendobar retries up to 5 times, doubling the wait each attempt. | Retry | Wait | | - | - | | 1st | 10 s | | 2nd | 20 s | | 3rd | 40 s | | 4th | 80 s | | 5th | 160 s | After that the delivery is marked `failed`. Re-send a `failed` or `cancelled` delivery from the card, or from code: ```ts theme={null} await client.webhooks.retryDelivery(deliveryId); ``` An endpoint that fails 10 deliveries in a row is disabled automatically and flagged in the dashboard. Update or re-enable it to reset the counter. ## How do I rotate the signing secret? Rotate from the card, or with `client.webhooks.rotateSecret(endpointId)`. For 24 hours Rendobar signs with both secrets, sending `X-Rendobar-Signature` (new) and `X-Rendobar-Signature-Previous` (old), so `verifyWebhook` keeps passing while you roll the new one out. After the window the old secret stops signing. An endpoint can rotate once per 24 hours. > [!NOTE] > > Endpoint URLs must be HTTPS. Delivery to private and reserved ranges (`10.x`, `172.16-31.x`, `192.168.x`, `127.x`, `::1`) is blocked to prevent SSRF, so point webhooks at a publicly reachable host. ## Best practices * **Return 200 fast.** Acknowledge, then process asynchronously. A slow handler trips the 10-second timeout and earns a duplicate delivery. * **Deduplicate** on `X-Rendobar-Delivery` (or `data.jobId`). The same event can arrive twice after a retry. * **Verify every signature** before you read the body. ## Related * [Job output](/docs/concepts/job#the-output): the `output` and `error` shape this payload carries * [Job lifecycle](/docs/concepts/job): what each status means * [Error codes](/docs/support/errors): the codes inside `job.failed` payloads * [Changelog](https://rendobar.com/changelog/): webhook payload changes and new events # Docs for the media processing API Source: https://rendobar.com/docs/index Start with the quickstart, then browse job types, storage connections, automation platforms, and the SDK, CLI and MCP server. Rendobar runs media jobs on our infrastructure and hands back a finished file. You POST a job with your inputs, we execute it on isolated cloud workers, and you collect the output by webhook or by polling. One endpoint covers raw FFmpeg, media inspection, timeline composition, compression, captions, and image generation. > **[Quickstart](/docs/quickstart)** > > Submit your first job, poll it to completion, and download the result. > **[Build with AI](/docs/build-with-ai)** > > Hand your coding agent one prompt and let it write the integration. ## How does a job work? Every job shares the same shape, whatever its type. Read [how a job works](/docs/concepts/job) for the lifecycle and the output contract, [scopes and access](/docs/concepts/scopes) for what an API key is allowed to do, and [credits](/docs/concepts/credits) for how work is billed. Results arrive two ways: [webhooks](/docs/guides/webhooks) for signed delivery to your endpoint, or [callbacks](/docs/guides/callbacks) when you want the job to call your own service. ## Job types > **[FFmpeg](/docs/jobs/ffmpeg)** > > Run any FFmpeg command in a sandbox, with guardrails. > **[ffprobe](/docs/jobs/ffprobe)** > > Probe media metadata from a URL, normalized plus raw. > **[Compose](/docs/jobs/compose)** > > Render video from a declarative JSON timeline. > **[Compress](/docs/jobs/compress)** > > Shrink a file to a target size or quality budget. > **[Captions](/docs/jobs/captions/animate)** > > Burn static subtitles or animate word-level captions. > **[Generate images](/docs/jobs/image-generate)** > > Generate, edit and upscale images on hosted models. ## Bring your own storage Connect a bucket you own and jobs read inputs from it and write outputs back, so your files never need to be public. Start at [storage connections](/docs/storage), then pick your provider: [Cloudflare R2](/docs/storage/cloudflare-r2), [Amazon S3](/docs/storage/amazon-s3), [Supabase](/docs/storage/supabase), or any [S3-compatible](/docs/storage/s3-compatible) bucket. ## Automation and tooling > **[Automation](/docs/automation)** > > Run jobs from n8n or Activepieces, no code required. > **[SDK and CLI](/docs/sdk)** > > TypeScript SDK, plus the rb command line tool. > **[MCP server](/docs/mcp-server)** > > Let Claude, Cursor and other agents run jobs directly. ## What do I do when something goes wrong? Check the [FAQ](/docs/support/faq) first, then [error codes](/docs/support/errors) to handle failures by machine code rather than message text. [Limits](/docs/support/limits) covers plan caps, and [versioning](/docs/support/versioning) covers how the API changes. # Animated word-level captions Source: https://rendobar.com/docs/jobs/captions/animate Generate animated word-level captions from a video, with 11 style presets, custom fonts, auto-emphasis, and translation. The `captions.animate` job returns a video with word-level animated captions burned in. Speech is automatically transcribed, segmented into timed cues, and rendered as styled subtitles burned onto the source. > [!NOTE] > > **Accepts:** video · **Type:** `captions.animate` ## Request ```ts SDK theme={null} import { createClient, outputUrl } from "@rendobar/sdk"; const client = createClient({ apiKey: "rb_YOUR_KEY" }); const job = await client.jobs.create({ type: "captions.animate", inputs: { source: "https://cdn.rendobar.com/assets/examples/sample.mp4", }, params: { preset: "hormozi", mode: "balanced" }, }); const result = await client.jobs.wait(job.id); console.log(outputUrl(result)); ``` ```python Python theme={null} import requests res = requests.post( "https://api.rendobar.com/jobs", headers={"Authorization": "Bearer rb_YOUR_KEY"}, json={ "type": "captions.animate", "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4", }, "params": { "preset": "hormozi", "mode": "balanced", }, }, ) ``` ```bash cURL theme={null} curl -X POST https://api.rendobar.com/jobs \ -H "Authorization: Bearer rb_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "captions.animate", "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4" }, "params": { "preset": "hormozi", "mode": "balanced" } }' ``` ## Parameters **preset** `enum` (default: hormozi) Built-in caption style. One of: `hormozi`, `mrbeast`, `tiktok`, `pill`, `karaoke`, `word-pop`, `minimal`, `subtitle-block`, `neon`, `gold`, `reveal`. **language** `string` ISO-639-1 language hint (e.g. `en`, `es`). Absent = English. Non-English forces the `large-v3-turbo` model regardless of `mode`. **mode** `enum` (default: balanced) Speed-vs-accuracy tradeoff for transcription. * `fast`: `base.en`, English only, \~3× faster, \~7-9% WER * `balanced`: `small.en`, default, \~5-7% WER * `accurate`: `large-v3-turbo`, \~3× slower, \~3-4% WER **style** `object` 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](#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. * `highlight`: active-word treatment. `{ "mode": "color" | "box", "color": "#RRGGBB", "textColor": "#RRGGBB", "scale": 100-200 }`. * `fill`, `stroke`, `shadow`, `bold`, `case`, `marginV`: colour, outline, and weight controls. **autoEmphasis** `boolean` (default: false) Let an in-stack model mark the important words so they render in the accent colour (the "gold words" look). **translateTo** `enum` Translate captions before animating. One of `es`, `fr`, `de`, `pt`, `it`, `nl`. Omit to keep the original language. **captionData** `boolean` (default: false) Also return the word-level transcript plus SRT and VTT on `output.data`. ## What inputs does it take? **source** `string` *required* URL of the source video. An uploaded asset's content URL works too. **subtitles** `string` URL of an SRT or VTT file. When provided, its text is animated and transcription is skipped. **font** `string` 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? Eleven presets ship with sensible default fonts. To pick a different one, set `style.font` to a bundled family. No upload needed. Any [Google Fonts](https://fonts.google.com) family also works by name and is fetched at render time, and `style.font` accepts a CSS fallback stack like `"Lato, Inter, sans-serif"` that falls through in order. | Bundled family | Pass as `style.font` | | - | - | | Inter | `"Inter"` | | Bebas Neue | `"Bebas Neue"` | | Anton | `"Anton"` | | Montserrat | `"Montserrat"` | | Poppins | `"Poppins"` | | Roboto | `"Roboto"` | | Oswald | `"Oswald"` | To use your own font, pass its URL as the `font` input. It wins over `style.font`, so nothing else is required: ```json theme={null} { "type": "captions.animate", "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4", "font": "https://example.com/MyBrand.ttf" }, "params": { "preset": "gold" } } ``` Naming the family embedded in the file as `style.font` still works, so an existing integration that does both keeps rendering the same way. `style.font` must match the family name baked into the file, not the filename or URL. If it does not match, the renderer falls back to the preset font. Like every job, this returns the standard response. See [Job output](/docs/concepts/job#the-output) for the shape and how to read the result. To burn existing subtitles instead, see [caption.burn](/docs/jobs/captions/burn). ## See also * [Burn subtitles](/docs/jobs/captions/burn): static SRT or VTT burned into the frame * [Compose](/docs/jobs/compose): build the video these captions sit on * [Compress](/docs/jobs/compress): shrink the captioned output before you deliver it * [ffprobe](/docs/jobs/ffprobe): check the audio stream before transcribing it * [Plan limits](/docs/support/limits): duration and file-size caps per plan # Burn captions into a video Source: https://rendobar.com/docs/jobs/captions/burn Burn subtitles into a video from an SRT, VTT, or ASS file, or auto-transcribe in any language, with full styling control. The `caption.burn` job burns subtitles permanently into a video. Provide an SRT, VTT, or ASS file as the `subtitles` input, or omit it to auto-transcribe the audio. Style the text with web-hex colors, an outline, a background box, and a position. > [!NOTE] > > **Accepts:** video · **Type:** `caption.burn` ## Request ```ts SDK theme={null} import { createClient, outputUrl } from "@rendobar/sdk"; const client = createClient({ apiKey: "rb_YOUR_KEY" }); const job = await client.jobs.create({ type: "caption.burn", inputs: { source: "https://cdn.rendobar.com/assets/examples/sample.mp4", subtitles: "https://cdn.rendobar.com/assets/examples/sample.srt", }, params: { fontFamily: "Inter", fontSize: 48, boxEnabled: true }, }); const result = await client.jobs.wait(job.id); console.log(outputUrl(result)); ``` ```python Python theme={null} import requests res = requests.post( "https://api.rendobar.com/jobs", headers={"Authorization": "Bearer rb_live_YOUR_KEY"}, json={ "type": "caption.burn", "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4", "subtitles": "https://cdn.rendobar.com/assets/examples/sample.srt", }, "params": {"fontFamily": "Inter", "fontSize": 48, "boxEnabled": True}, }, ) ``` ```bash cURL theme={null} curl -X POST https://api.rendobar.com/jobs \ -H "Authorization: Bearer rb_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "caption.burn", "inputs": { "source": "https://cdn.rendobar.com/assets/examples/sample.mp4", "subtitles": "https://cdn.rendobar.com/assets/examples/sample.srt" }, "params": { "fontFamily": "Inter", "fontSize": 48, "boxEnabled": true } }' ``` Omit `subtitles` to auto-transcribe. The spoken language is detected by default (`language: "auto"`). Pass an ISO-639-1 code to skip detection. ## What inputs does it take? **source** `string` *required* URL of the source video to caption. **subtitles** `string` URL of an SRT, VTT, or ASS subtitle file. A provided ASS file is burned as-is, preserving its own styling. Omit to auto-transcribe the audio. **font** `string` URL of your own font file (TTF, OTF, TTC, WOFF, WOFF2). Captions render in it, and it takes priority over `fontFamily`, so you do not need to know the family name inside your own file. An uploaded asset's content URL works here. ## Parameters **language** `enum` (default: auto) Spoken language for the auto-transcribe path, as an ISO-639-1 code (e.g. `en`, `es`, `ja`) or `auto` to detect. English uses a faster model. Everything else routes to multilingual transcription. Ignored when a subtitle file is provided. **font** `string` (default: Inter) Font family, or a CSS fallback stack. Any [Google Fonts](https://fonts.google.com) family works by name and is fetched at render time. Seven are bundled and resolve instantly: `Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`, `Bebas Neue`. A stack falls through in order, so `"Lato, Inter, sans-serif"` tries each in turn. To render in your own file, pass its URL as the `font` **input**, which wins over this parameter. **fontFamily** `string` *deprecated* Deprecated alias for `font`. Still accepted so existing clients keep working. `font` wins when both are present. **fontSize** `integer` (default: 48) Font size in output pixels. Range 8–400. **fontColor** `string` (default: #FFFFFF) Text fill color as web hex (`#RRGGBB` or `#AARRGGBB`, alpha first). **bold** `boolean` (default: true) Render the text bold. **italic** `boolean` (default: false) Render the text italic. **outlineColor** `string` (default: #000000) Outline (stroke) color as web hex. **outlineWidth** `number` (default: 2) Outline thickness in pixels. Range 0–20. **shadow** `number` (default: 1) Drop-shadow depth in pixels. Range 0–20. `0` disables the shadow. **boxEnabled** `boolean` (default: false) Draw a filled background box behind the text (the accessibility "subtitle bar"). **boxColor** `string` (default: #000000) Background box color as web hex. Applies when `boxEnabled` is `true`. **boxOpacity** `number` (default: 0.6) Background box opacity, `0` (transparent) to `1` (opaque). **position** `enum` (default: bottom) Vertical placement. One of `bottom`, `center`, `top`. **alignment** `enum` (default: center) Horizontal alignment. One of `left`, `center`, `right`. **marginV** `integer` (default: 60) Vertical margin in pixels. Range 0–2000. **marginH** `integer` (default: 40) Horizontal margin in pixels. Range 0–2000. Also bounds the line-wrap width. **maxCharsPerLine** `integer` Maximum characters per line on the auto-transcribe path. Range 10–80. Like every job, this returns the standard response. See [Job output](/docs/concepts/job#the-output) for the shape and how to read the result. For animated word-level captions instead, see [captions.animate](/docs/jobs/captions/animate). ## See also * [Animated captions](/docs/jobs/captions/animate): word-level captions generated from the audio * [ffprobe](/docs/jobs/ffprobe): check the subtitle streams already in a file * [Compose](/docs/jobs/compose): assemble the video before you burn subtitles onto it * [Error codes](/docs/support/errors): the full error catalogue * [Plan limits](/docs/support/limits): duration and file-size caps per plan # Audio levels, mixing and effects Source: https://rendobar.com/docs/jobs/compose/audio 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. 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 theme={null} { "asset": { "type": "audio", "src": "https://example.com/music.mp3", "volume": 0.35, "fadeIn": 1.5, "fadeOut": 3 }, "length": "end" } ``` `length: "end"` runs the music to the end of the video, however long it is. ## Levels `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`. `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. ## Ducking music under a voice 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. ```json theme={null} { "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 theme={null} { "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 theme={null} "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 theme={null} "effects": [{ "type": "highPass", "cutoff": 80 }, { "type": "voiceDenoise" }, { "type": "compressor" }, { "type": "loudness" }] ``` ## Sound drawn on the picture `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 theme={null} "effects": [{ "type": "visualizer", "kind": "waveform", "color": "#34D399", "y": "80%", "width": "76%", "height": "20%" }] ```