Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"group": "Jobs",
"pages": [
"jobs/ffmpeg",
"jobs/compose",
{
"group": "Captions",
"icon": "closed-captioning",
Expand Down Expand Up @@ -133,6 +134,14 @@
"source": "/jobs/caption-burn",
"destination": "/jobs/captions/burn"
},
{
"source": "/jobs/compose/:slug*",
"destination": "/jobs/compose"
},
{
"source": "/job-types/compose",
"destination": "/jobs/compose"
},
{
"source": "/cli/:slug*",
"destination": "/cli"
Expand Down
12 changes: 12 additions & 0 deletions guides/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
}}
/>

Rendobar POSTs JSON to your endpoint when a [job](/concepts/job) changes status. Available on all plans, including Free.

Check warning on line 28 in guides/webhooks.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

guides/webhooks.mdx#L28

Did you really mean 'Rendobar'?

## Set up a webhook

Expand Down Expand Up @@ -87,7 +87,7 @@
"meta": { "format": "mp4", "width": 1920, "height": 1080, "durationMs": 127500 }
}
],
"expiresAt": 1707440415000

Check warning on line 90 in guides/webhooks.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

guides/webhooks.mdx#L90

Did you really mean 'expiresAt'?
},
"cost": { "amount": 50000000, "currency": "USD", "formatted": "$0.05" },
"timing": {
Expand Down Expand Up @@ -122,6 +122,18 @@

<CodeGroup>

```ts SDK
import { verifyWebhookSignature } from "@rendobar/sdk/webhooks";

// The signature covers `${timestamp}.${rawBody}` (raw string, not parsed JSON).
const ok = await verifyWebhookSignature(
`${timestamp}.${rawBody}`,
signature, // X-Rendobar-Signature header
process.env.WEBHOOK_SECRET,
);
if (!ok) throw new Error("Invalid signature");
```

```javascript Node.js
import { createHmac, timingSafeEqual } from "crypto";

Expand Down Expand Up @@ -189,7 +201,7 @@
## Best practices

- **Return 200 fast.** Process asynchronously. Long handlers trigger retries → duplicate deliveries.
- **Deduplicate** on `X-Rendobar-Delivery` or `jobId`. Same event can arrive more than once.

Check warning on line 204 in guides/webhooks.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

guides/webhooks.mdx#L204

Did you really mean 'Deduplicate'?
- **Verify every signature** before reading the body.

## See also
Expand Down
295 changes: 295 additions & 0 deletions jobs/compose.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
---
title: "Compose: render from a timeline"
sidebarTitle: "Compose"
description: "Render a video from a declarative JSON timeline. Tracks, clips, transitions, text, and per-clip effects, in one API call."
icon: "film"
keywords: ["video compose api", "json timeline to video", "video transitions api", "video editing api", "compose"]
canonical: "https://rendobar.com/docs/jobs/compose"
---

<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "TechArticle",
"@id": "https://rendobar.com/docs/jobs/compose/#article",
"headline": "Compose: render from a timeline",
"description": "Render a video from a declarative JSON timeline. Tracks, clips, transitions, text, and per-clip effects, in one API call.",
"datePublished": "2026-06-21",
"dateModified": "2026-06-22",
"author": { "@type": "Organization", "@id": "https://rendobar.com/#organization" },
"publisher": { "@type": "Organization", "@id": "https://rendobar.com/#organization" },
"isPartOf": { "@id": "https://rendobar.com/#website" }
})
}}
/>

`compose` renders a video from a JSON timeline. Start with one clip, then layer up: more clips, transitions, overlays, and effects. This page builds from the smallest render to the full model. Each step adds one idea.

## Your simplest render

The smallest timeline is one track with one clip. This trims three seconds of a video and renders it to MP4. The whole API call:

<CodeGroup>

```ts SDK
import { createClient, outputUrl } from "@rendobar/sdk";

const client = createClient({ apiKey: "rb_YOUR_KEY" });

const job = await client.jobs.create({
type: "compose",
params: {
schemaVersion: 1,
output: { format: "mp4", resolution: { width: 1280, height: 720 }, fps: 30 },
timeline: {
tracks: [
{ clips: [
{ asset: { type: "video", src: "https://cdn.rendobar.com/assets/examples/a.mp4", trim: { from: 0, to: 3 } }, start: 0, length: 3 },
] },
],
},
},
});

const result = await client.jobs.wait(job.id);
console.log(outputUrl(result));
```

```python Python
import requests

res = requests.post(
"https://api.rendobar.com/jobs",
headers={"Authorization": "Bearer rb_YOUR_KEY"},
json={
"type": "compose",
"params": {
"schemaVersion": 1,
"output": {"format": "mp4", "resolution": {"width": 1280, "height": 720}, "fps": 30},
"timeline": {
"tracks": [
{"clips": [
{"asset": {"type": "video", "src": "https://cdn.rendobar.com/assets/examples/a.mp4", "trim": {"from": 0, "to": 3}}, "start": 0, "length": 3}
]}
]
},
},
},
)
```

```bash cURL
curl -X POST https://api.rendobar.com/jobs \
-H "Authorization: Bearer rb_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "compose",
"params": {
"schemaVersion": 1,
"output": { "format": "mp4", "resolution": { "width": 1280, "height": 720 }, "fps": 30 },
"timeline": {
"tracks": [
{ "clips": [
{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/a.mp4", "trim": { "from": 0, "to": 3 } }, "start": 0, "length": 3 }
] }
]
}
}
}'
```

</CodeGroup>

<Info>
**Assets:** video, image, text, audio · **Output:** mp4 / mov / webm / gif / jpg / png

Check warning on line 106 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L106

Did you really mean 'mov'?

Check warning on line 106 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L106

Did you really mean 'webm'?

Check warning on line 106 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L106

Did you really mean 'gif'?
</Info>

That is the shape of every render. Everything below is one more clip, one more track, or one more field.

## Join two clips

Put a second clip on the same track and a transition between them. The clips overlap by the transition's `duration`, so the second clip starts a little before the first ends.

```json
"clips": [
{ "asset": { "type": "video", "src": "a.mp4" }, "start": 0, "length": 3 },
{ "type": "transition", "transition": "crossfade", "duration": 1 },
{ "asset": { "type": "video", "src": "b.mp4" }, "start": 2, "length": 3 }
]
```

`slide` also accepts `direction: "left" | "right" | "up" | "down"`.

| Transition | Effect |
| --- | --- |
| `crossfade` | A dissolves directly into B |
| `fade` | A dips through black, then B fades up |
| `slide` | B slides in over A from an edge (directional) |
| `zoom` | B grows in from the center |

<Columns cols={2}>
<Frame caption="crossfade">
<video src="https://cdn.rendobar.com/assets/showcase/transitions/crossfade.mp4" autoPlay loop muted playsInline />
</Frame>
<Frame caption="fade">
<video src="https://cdn.rendobar.com/assets/showcase/transitions/fade.mp4" autoPlay loop muted playsInline />
</Frame>
<Frame caption="slide (left)">
<video src="https://cdn.rendobar.com/assets/showcase/transitions/slide-left.mp4" autoPlay loop muted playsInline />
</Frame>
<Frame caption="zoom">
<video src="https://cdn.rendobar.com/assets/showcase/transitions/zoom.mp4" autoPlay loop muted playsInline />
</Frame>
</Columns>

## How the timeline works

The model is two ideas:

- **Tracks stack.** The first track is the background. Each later track composites on top of the ones below it: overlays, titles, picture-in-picture.
- **Clips sit in time.** Each clip has a `start` and a `length` in seconds. Two clips on the same track are joined by a transition. A gap between them plays nothing.

So a multi-track timeline is layers (tracks) of sequences (clips). That is the whole structure.

## Add a layer

A second track renders over the first. This lays an animated title over the video.

```json
"tracks": [
{ "clips": [
{ "asset": { "type": "video", "src": "a.mp4" }, "start": 0, "length": 5 }
] },
{ "clips": [
{ "asset": { "type": "text", "text": "Coastal Escape", "style": { "size": 84 } },
"start": 0, "length": 5 }
] }
]
```

## Effects on a clip

Any clip takes effects as fields. Add one field at a time, on the clip it applies to.

### Color grade

`color` grades a clip. Set `contrast`, `saturation`, `temperature`, `brightness`, and `gamma` to set a cinematic mood, or push them to lift flat, hazy footage.

```json
"color": { "contrast": 1.12, "saturation": 1.2, "temperature": 6800, "gamma": 0.95 }
```

<Frame caption="Cinematic color grade to set a mood">
<video src="https://cdn.rendobar.com/assets/showcase/fx/color-grade.mp4" autoPlay loop muted playsInline />
</Frame>

### Filters

`filter` applies a one-shot look: `greyscale`, `negative`, `boost`, `muted`, `lighten`, `darken`, or `contrast`.

```json
"filter": "greyscale"
```

### Transform and Ken Burns

`transform` positions and scales a clip. Add `animateTo` and it animates from the start values to the end values over the clip, the Ken Burns move that brings a still photo to life.

```json
"transform": {
"scale": 1.1,
"position": { "x": "38%", "y": "48%" },
"animateTo": { "scale": 1.32, "position": { "x": "62%", "y": "52%" } }
}
```

<Frame caption="Bring a still photo to life for slideshows and docs">
<video src="https://cdn.rendobar.com/assets/showcase/fx/kenburns.mp4" autoPlay loop muted playsInline />
</Frame>

### Picture-in-picture

Scale a clip down and position it on an overlay track to put a webcam over your content, for reactions and screen-shares.

```json
"transform": { "scale": 0.3, "position": { "x": "80%", "y": "75%" } }
```

### Speed

`speed` is a multiplier. Below `1` slows a clip down for a dramatic action beat, above `1` speeds a slow process into a timelapse.

Check warning on line 222 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L222

Did you really mean 'timelapse'?

```json
"speed": 0.4
```

### Chroma key

`chromaKey` removes a solid background color so you can drop a presenter onto any scene. Put the keyed clip on an overlay track above the new background.

```json
"chromaKey": { "color": "#66DD09", "similarity": 0.28 }
```

<Frame caption="Put a presenter on any background (chroma key)">
<video src="https://cdn.rendobar.com/assets/showcase/fx/chroma.mp4" autoPlay loop muted playsInline />
</Frame>

### Blend modes

`blendMode` controls how an overlay composites with the track below: `screen`, `multiply`, `overlay`, `add`, and more. A light-leak clip over your footage with `screen` adds a cinematic glow.

```json
"blendMode": "screen",
"opacity": 0.7
```

### Opacity and blur

`opacity` takes a number or keyframes. Keyframe it from `0` to `1` and back for a clean fade in and out. `blur` softens a clip, useful as a backdrop that keeps overlaid text readable.

Check warning on line 251 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L251

Did you really mean 'keyframes'?

Check warning on line 251 in jobs/compose.mdx

View check run for this annotation

Mintlify / Mintlify Validation (rendobar) - vale-spellcheck

jobs/compose.mdx#L251

Did you really mean 'Keyframe'?

```json
"opacity": [
{ "time": 0, "value": 0 },
{ "time": 0.6, "value": 1 },
{ "time": 2.4, "value": 1 },
{ "time": 3, "value": 0 }
]
```

## Text and titles

A `text` asset renders a styled title. Set the `font`, `size`, `weight`, `color`, `position`, and an `animate` entrance (`fade`, `slideUp`, `slideDown`, `slideLeft`, `slideRight`). Put one text clip on its own track for a title card, another low and left for a lower-third name tag.

```json
"asset": {
"type": "text",
"text": "Coastal Escape",
"style": {
"font": "Inter",
"size": 84,
"weight": 800,
"color": "#FFFFFF",
"position": { "x": "50%", "y": "38%" },
"animate": { "type": "slideUp", "duration": 0.7 }
}
}
```

<Frame caption="Add a title card and a lower-third name tag (text)">
<video src="https://cdn.rendobar.com/assets/showcase/fx/title-lowerthird.mp4" autoPlay loop muted playsInline />
</Frame>

## Set the canvas

`output` controls the resolution, frame rate, and format. A `9:16` canvas with a `crop` on the clip turns landscape footage into a vertical Reel that fills the frame, no black bars.

```json
"output": { "format": "mp4", "resolution": { "width": 1080, "height": 1920 }, "fps": 30 }
```

<Frame caption="Landscape footage reframed to a vertical 9:16 Reel (crop)">
<video src="https://cdn.rendobar.com/assets/showcase/fx/reframe.mp4" autoPlay loop muted playsInline style={{ maxWidth: "300px" }} />
</Frame>
10 changes: 10 additions & 0 deletions snippets/poll-status.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ Poll `GET /jobs/{id}` until `status` is `complete`. The response then contains `

<CodeGroup>

```ts SDK
import { createClient, outputUrl } from "@rendobar/sdk";

const client = createClient({ apiKey: "rb_your_key" });

// wait() polls until the job reaches a terminal status
const job = await client.jobs.wait("job_abc123");
console.log(job.status, outputUrl(job), job.cost?.formatted);
```

```bash cURL
curl https://api.rendobar.com/jobs/job_abc123 \
-H "Authorization: Bearer rb_your_key"
Expand Down
28 changes: 28 additions & 0 deletions support/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,34 @@ The three you'll hit most: `INSUFFICIENT_CREDITS`, `RATE_LIMITED`, `VALIDATION_E

<CodeGroup>

```ts SDK
import { createClient, isApiError } from "@rendobar/sdk";

const client = createClient({ apiKey: "rb_YOUR_KEY" });

try {
const job = await client.jobs.create({
type: "ffmpeg",
params: { command: "ffmpeg -i https://example.com/video.mp4 -vf scale=1280:720 output.mp4" },
});
} catch (err) {
if (!isApiError(err)) throw err;
switch (err.code) {
case "INSUFFICIENT_CREDITS":
// redirect to billing
break;
case "RATE_LIMITED":
// back off for err.retryAfter seconds
break;
case "VALIDATION_ERROR":
// fix the request
break;
default:
console.error(`API error: ${err.code} - ${err.message}`);
}
}
```

```javascript JavaScript
const res = await fetch("https://api.rendobar.com/jobs", {
method: "POST",
Expand Down
Loading