diff --git a/docs.json b/docs.json
index dbe7218..21406c1 100644
--- a/docs.json
+++ b/docs.json
@@ -58,7 +58,44 @@
"pages": [
"jobs/ffmpeg",
"jobs/ffprobe",
- "jobs/compose",
+ {
+ "group": "Compose",
+ "icon": "film",
+ "expanded": true,
+ "root": "jobs/compose",
+ "pages": [
+ "jobs/compose/timeline",
+ "jobs/compose/framing",
+ "jobs/compose/transitions",
+ "jobs/compose/motion",
+ "jobs/compose/text",
+ "jobs/compose/fonts",
+ "jobs/compose/shapes",
+ "jobs/compose/effects",
+ "jobs/compose/audio",
+ "jobs/compose/output",
+ "jobs/compose/craft",
+ {
+ "group": "Reference",
+ "icon": "table-list",
+ "pages": [
+ "jobs/compose/reference/timeline",
+ "jobs/compose/reference/video",
+ "jobs/compose/reference/image",
+ "jobs/compose/reference/audio",
+ "jobs/compose/reference/text",
+ "jobs/compose/reference/shape",
+ "jobs/compose/reference/timer",
+ "jobs/compose/reference/subtitles",
+ "jobs/compose/reference/animation",
+ "jobs/compose/reference/composition",
+ "jobs/compose/reference/transition",
+ "jobs/compose/reference/output",
+ "jobs/compose/reference/effects"
+ ]
+ }
+ ]
+ },
"jobs/compress",
{
"group": "Captions",
diff --git a/jobs/compose.mdx b/jobs/compose.mdx
index 7a73390..1dced86 100644
--- a/jobs/compose.mdx
+++ b/jobs/compose.mdx
@@ -1,7 +1,7 @@
---
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."
+description: "Render a video from a declarative JSON timeline: tracks, clips, transitions, text, shapes, keyframes and effects."
icon: "film"
keywords: ["video compose api", "json timeline to video", "video transitions api", "video editing api", "compose"]
canonical: "https://rendobar.com/docs/jobs/compose"
@@ -15,7 +15,7 @@ canonical: "https://rendobar.com/docs/jobs/compose"
"@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.",
+ "description": "Render a video from a declarative JSON timeline: tracks, clips, transitions, text, shapes, keyframes and effects.",
"datePublished": "2026-06-21",
"author": { "@type": "Organization", "@id": "https://rendobar.com/#organization" },
"publisher": { "@type": "Organization", "@id": "https://rendobar.com/#organization" }
@@ -27,6 +27,7 @@ canonical: "https://rendobar.com/docs/jobs/compose"
## Your simplest render
+
The smallest timeline is one track with one clip. This renders the first three seconds of a video to MP4. The whole API call:
@@ -140,7 +141,7 @@ curl -X POST https://api.rendobar.com/jobs \
-**Assets:** video, image, text, audio, composition · **Output:** mp4 / webm / gif / mp3 (audio only) / jpg / png (still frame)
+**Assets:** video, image, audio, text, timer, subtitles, shape, animation (Lottie), composition · **Output:** mp4 / webm / gif / mp3 (audio only) / jpg / png (still frame)
Without `trim`, a clip plays its source from the start for `length` seconds, or for the whole source when `length` is left off. To use a specific segment instead, add `trim: { from, to }` (in source seconds) to the asset. `length` is always the timeline duration. `trim` only chooses which part of the source fills it.
@@ -149,6 +150,7 @@ That is the shape of every render. Everything below is one more clip, one more t
## Join two clips
+
Put a second clip on the same track and a transition between them.
```json
@@ -171,682 +173,66 @@ Writing the timing out by hand gives the same render. The second clip starts `du
]
```
-`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 |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## 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.** A clip can carry a `start` and a `length` in seconds, and it can leave both out. Two clips on the same track are joined by a transition. A gap between them plays nothing.
-
-So a multi-track timeline is layers (tracks) of sequences (clips). That is the whole structure.
-
-### Timing is optional
-
-Every clip sits in a **container**: the scene it belongs to, or the whole video. Two sentences cover every case.
-
-- **Omit `length`** and the clip runs as long as its **content** does, never past its container.
-- **Write `length: "end"`** and it runs to the end of its **container**, whatever its content says.
-
-Content means the source's duration, or the trim span, or the source duration divided by `speed` (so `speed: 0.5` on a 4 second clip occupies 8 seconds), or a composition's own end.
-
-Text and images have no duration of their own, so for them the two sentences say the same thing. A title with no `length` stays up for the whole video, which is what a watermark wants.
-
-`start` follows the same shape: omit it and the clip begins after everything already placed on its track, pulled back by any transition immediately before it. Give it a number and that is seconds from the start of its container.
-
-Set either field wherever you want something else. Every timeline written with full timing keeps rendering exactly as it did.
-
-### Worked examples
-
-A music bed. The source is three minutes long, the video is not, and `"end"` is how you say so without totalling the clips above it.
-
-```json
-"tracks": [
- { "clips": [
- { "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/river.mp4" } },
- { "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/forest.mp4" } }
- ] },
- { "clips": [
- { "asset": { "type": "audio", "src": "https://cdn.rendobar.com/assets/showcase/music.mp3", "volume": 0.35, "fadeOut": 2 },
- "length": "end" }
- ] }
-]
-```
-
-A watermark. It has no duration of its own, so it needs nothing at all.
-
-```json
-{ "clips": [
- { "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" },
- "transform": { "scale": 0.15, "position": { "x": "88%", "y": "12%" } }, "opacity": 0.8 }
-] }
-```
-
-Both stop on the last frame, whatever the clips elsewhere add up to. Add another clip later and they still do.
-
-### The rules that fall out of it
-
-A clip running to the end of the **video** is measured against every clip that is not, so two of them on different tracks both land on the real end rather than chasing each other. It runs to the last frame, so nothing may follow it on the same track, and a timeline where nothing has a length of its own has no end to measure and is rejected.
-
-A clip never runs past its container, so a 6 second video in a 3 second scene plays 3 seconds, and a 2 second video in a 5 second scene plays 2. That applies to a length you wrote as well as one the engine worked out.
-
-`"end"` is the opposite request and does not cap: it asks for the container's full extent whatever the source holds. On a source shorter than the container that is impossible, and the job is rejected for outrunning its source. Leave the length off instead, or trim less.
-
-Inside a `composition`, `start` and `length` both stay required. A nested timeline's length is set by the clip hosting it, so there is nothing to append to and no container to measure against.
-
-## 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": "https://cdn.rendobar.com/assets/examples/sea.mp4" }, "start": 0, "length": 3 }
- ] },
- { "clips": [
- { "asset": { "type": "text", "text": "Coastal Escape", "style": { "font": "Inter", "size": 84, "color": "#FFFFFF" } },
- "start": 0, "length": 3 }
- ] }
-]
-```
-
-## Effects on a clip
-
-Effects are fields on a clip. Stack as many as you like on one clip. Each example below shows the source on the left and the rendered result on the right. Exact fields and ranges are in the [Reference](#clip-fields).
-
-```json
-{
- "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/sea.mp4" },
- "start": 0,
- "length": 3,
- "color": { "saturation": 1.2, "temperature": 6800 },
- "speed": 0.8,
- "transform": { "scale": 1.1, "animateTo": { "scale": 1.3 } }
-}
-```
-
-### Color and filters
-
-`color` grades a clip with `contrast`, `saturation`, `temperature`, `brightness`, `gamma`, and `hue`. `filter` applies a one-shot look like `greyscale` or `boost`.
-
-
-
-
-
-
-
-
-
-
-
-
-
-### Motion and speed
-
-`transform` positions, scales, and rotates a clip. Add `animateTo` for a Ken Burns move, or scale a clip down and place it on an overlay track for picture-in-picture. `speed` is a playback multiplier: below `1` for slow motion, above `1` for a timelapse.
-
-
-
-
-
-### Compositing
-
-`chromaKey` keys out a solid background color so you can drop a presenter onto any scene. `blendMode` controls how an overlay blends with the track below. `opacity` (a number or keyframes) and `blur` handle fades and soft backdrops.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## 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. `font` takes any Google Fonts family by name, or a family from your own file. See [Fonts](#fonts).
-
-```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 }
- }
-}
-```
-
-
-
-
-
-## Shapes
-
-A `shape` asset draws a rect, ellipse, circle, triangle, diamond, or any SVG
-path you paste in. No image to make, no file to host, and it stays sharp at any
-output resolution.
-
-```json
-"asset": {
- "type": "shape",
- "color": "#0A0A0ACC",
- "position": { "x": "50%", "y": "86%" },
- "size": { "width": "100%", "height": "22%" }
-}
-```
-
-That is a bar across the lower third, dark enough to read a title on and thin
-enough to see the footage through. Put it on a track below the text.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Only `type` and `size` are required. Everything else on this page is optional,
-and the defaults are chosen so the common cases need almost none of it.
-
-Two things to know up front. `position` is the **centre** of the shape unless
-you set `anchor`, so a full-width bar sits at `x: "50%"` whatever its height.
-And there is no `length`, because a shape has no duration of its own: like text
-and images it runs to the end of its container unless you give it one.
-
-`width` is a percent of the frame's width, `height` a percent of its height.
-
-### Leave the height out to get a circle
-
-Omit `size.height` and the shape is made **square in pixels**. That is the only
-way to get a true circle or square on a frame that is not square, and it is the
-one thing about shapes worth memorising.
-
-A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels. Rounding it fully
-gives a pill, not a circle. Omit the height and the engine derives 17.8% for
-you.
-
-```json
-{ "type": "shape", "shape": "circle", "color": "#059669",
- "position": { "x": "88%", "y": "18%" }, "size": { "width": "12%" } }
-```
-
-`shape: "circle"` is `ellipse` with that rule applied, so it rejects an explicit
-height rather than quietly ignoring one. The same omission works on every kind:
-a `rect` with no height is a square, a `diamond` with no height is a regular one.
-
-### Anchor
-
-`anchor: "topLeft"` points `position` at the corner instead of the centre. It is
-what makes letterbox bars arithmetic-free.
-
-```json
-{ "type": "shape", "color": "#000000", "anchor": "topLeft",
- "position": { "x": "0%", "y": "90%" }, "size": { "width": "100%", "height": "10%" } }
-```
-
-### Fills, flat and gradient
-
-`color` takes `#RRGGBB`, or the 8-digit `#RRGGBBAA` for a constant translucency
-the way `#0A0A0ACC` does above.
-
-Do not reach for the clip's `opacity` instead. `opacity` dims the fill as well
-as fading it, so a half-opacity white box renders grey rather than white you can
-see through. Keep `opacity` for animated fades.
-
-Give `color` **two** colours and it is a gradient. `gradient` names the
-direction: `vertical` (the default), `horizontal`, `diagonal` or `radial`.
-
-```json
-{ "type": "shape", "color": ["#E11D48", "#3B82F6"], "gradient": "diagonal",
- "position": { "x": "50%", "y": "50%" }, "size": { "width": "40%", "height": "30%" } }
-```
-
-The gradient maps to the **shape**, not the frame, so a small badge shows the
-whole ramp instead of a sliver of one. Stops take the 8-digit form too, which is
-how you get a scrim that fades to nothing: `["#0A0A0A00", "#0A0A0AFF"]`.
-
-### Outlines, with a real hole
-
-`stroke` draws a border just inside the shape's edge, its `width` in pixels of
-the output frame. Give the shape a `color` too and you get a filled shape with a
-border; `radius` rounds both together.
-
-Leave `color` out and only the outline draws. What it surrounds is genuinely
-transparent, not filled with black, so the footage shows through the middle.
-That works on every kind except a path.
-
-```json
-{ "type": "shape", "shape": "ellipse",
- "position": { "x": "62%", "y": "40%" },
- "size": { "width": "26%", "height": "40%" },
- "stroke": { "color": "#F59E0B", "width": 5 } }
-```
-
-### Rounded corners
-
-`radius` runs from `0` (square) to `1` (fully round) on a `rect`. At `1` a wide
-box becomes a pill and a square one becomes a circle.
-
-### Drop shadows
-
-`shadow` follows the shape, including its outline, its rotation and its
-animation. Every field has a default, so `"shadow": {}` is a sensible shadow.
-
-```json
-{ "type": "shape", "color": ["#FFFFFF", "#E5E7EB"], "radius": 0.3,
- "stroke": { "color": "#111827", "width": 3 },
- "shadow": { "color": "#00000099", "blur": 14, "x": 8, "y": 10 },
- "position": { "x": "30%", "y": "50%" }, "size": { "width": "34%", "height": "40%" } }
-```
-
-### Rotation turns about the shape, not the frame
-
-`rotate` is degrees clockwise about the shape's **own** centre, so a shape spins
-where it sits rather than orbiting the middle of the frame. A diagonal rule is a
-thin rect and one field:
-
-```json
-{ "type": "shape", "color": "#FFFFFF", "rotate": 30,
- "position": { "x": "50%", "y": "72%" },
- "size": { "width": "40%", "height": "0.8%" } }
-```
-
-There is no `line` type for the same reason there is no `square` type: a line is
-a rect with a small height, and `rotate` is what makes it diagonal.
-
-### Softness
-
-`softness` runs 0 to 1 and feathers the edge. At 0.2 to 0.4 an ellipse reads as
-a spotlight or a vignette rather than a hard-edged disc. A full-frame ellipse
-with `softness: 0.5` and a radial `["#00000000", "#000000CC"]` fill is a
-vignette with no image asset at all.
-
-### Paths
-
-`shape: "path"` takes SVG path data straight out of a design tool. `viewBox`
-gives the coordinate space it was drawn in, defaulting to `"0 0 100 100"`, and
-the path is scaled into `size` so the same data works at any size on any frame.
-
-```json
-{ "type": "shape", "shape": "path", "viewBox": "0 0 24 24",
- "path": "M12 21C12 21 3 14.5 3 8.5C3 5.5 5.5 3 8.5 3C10.4 3 12 4.5 12 4.5C12 4.5 13.6 3 15.5 3C18.5 3 21 5.5 21 8.5C21 14.5 12 21 12 21Z",
- "color": "#E11D48", "position": { "x": "50%", "y": "50%" }, "size": { "width": "20%" } }
-```
-
-`M L H V C S Q T Z` are supported, absolute and relative. Elliptical arcs (`A`)
-are refused rather than approximated, so re-export with arcs converted to
-curves. A path takes a fill, a gradient, a shadow, rotation and softness, but
-not a `stroke`: outlining arbitrary path data needs true path offsetting.
-
-### Animation
-
-`animateTo` moves `position`, `size`, `rotate`, `color` and `radius` over the
-clip. `easing` is `linear` by default, which is what a progress bar or a timer
-wants, or `smooth` for an entrance.
-
-```json
-{ "type": "shape", "color": "#FFFFFF", "anchor": "topLeft",
- "position": { "x": "2%", "y": "86%" },
- "size": { "width": "0%", "height": "1.5%" },
- "animateTo": { "size": { "width": "96%" }, "easing": "smooth" } }
-```
-
-A dimension you leave out of `animateTo` keeps its starting value, so the bar
-above grows sideways without changing height. A shape that is square in pixels
-stays square as it grows, so an animated circle does not turn into an ellipse
-halfway through. A solid colour can animate into a gradient, and a square can
-round itself off with `animateTo.radius`.
-
-An animated `rotate` has no limit: rotation is baked into the shape's own
-outline, so animating from 0 to 720 is two full turns.
-
-### Combinations the API refuses
-
-Two engine primitives draw shapes and they have different abilities, so a few
-combinations are rejected at submit time rather than rendered wrong. Each error
-names the fix.
-
-| Not combinable | Why | What to do instead |
-| --- | --- | --- |
-| `radius` on anything but a `rect` | only rectangles have corners | drop it |
-| `radius` with no `color` | an outline-only shape is drawn by the primitive with no corner rounding | give it a fill |
-| `radius` with `rotate` or `softness` | the primitive that rounds corners cannot turn or feather | drop one of them |
-| `stroke` on a `path` | outlining arbitrary path data needs true path offsetting | draw the outline into the path itself |
-| `transform` on any shape | it moves the whole frame, not the shape in it, so the result lands somewhere neither value asked for | use `position`, `size` and `rotate` |
-
-### What else a shape takes
-
-`blendMode` mixes it with the layer below, `blur` softens the whole thing,
-colour grading tints it, and opacity keyframes fade it in and out. A track
-holding only shapes composites as a base layer, so a solid colour background is
-a timeline that references no files at all.
-
-There is no arbitrary path stroke and no per-corner radius. Anything past that
-is still an `image` asset.
-
-
-## Fonts
-
-Name any [Google Fonts](https://fonts.google.com) family in `style.font` and it
-is fetched at render time. Nothing to upload, no configuration.
-
-```json
-"style": { "font": "Bebas Neue", "size": 96, "weight": 700 }
-```
-
-Seven families are bundled into the render image and resolve instantly, with no
-fetch: `Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`,
-`Bebas Neue`.
-
-### Your own font files
-
-Put the file URLs in the job's `fonts` input, each under a family name **you**
-choose, then reference that name in `style.font`.
-
-```json
-{
- "type": "compose",
- "inputs": {
- "fonts": [
- { "family": "Display", "src": "https://cdn.example.com/BrandDisplay.woff2" },
- { "family": "Body", "src": "https://cdn.example.com/BrandText.otf" }
- ]
- },
- "params": {
- "schemaVersion": 1,
- "timeline": {
- "tracks": [
- { "clips": [{ "asset": { "type": "text", "text": "LAUNCH", "style": { "font": "Display", "size": 140 } }, "start": 0, "length": 3 }] },
- { "clips": [{ "asset": { "type": "text", "text": "in stores now", "style": { "font": "Body", "size": 54 } }, "start": 0, "length": 3 }] }
- ]
- },
- "output": { "format": "mp4", "size": "1080p" }
- }
-}
-```
-
-There is no limit on how many you declare. TTF, OTF, TTC, WOFF and WOFF2 all
-work, and an [uploaded asset's](/sdk#uploads) content URL works in
-`src` like any other input URL.
-
-Naming your own font is the point. The alternative is to reference the family
-name buried inside the file, which you have to discover with a font inspector
-and which is frequently not what the file is called. Omit `family` and that
-embedded name is read for you, so `{ "src": "..." }` on its own is also valid.
-
-A family you declare shadows a bundled one of the same name, exactly the way an
-`@font-face` rule beats a system font in CSS. Declare a font called `Inter` and
-yours is the one that renders.
-
-For a single file, `inputs.font` is the shorthand. It is addressed as `custom`:
-
-```json
-"inputs": { "font": "https://cdn.example.com/BrandDisplay.ttf" },
-"style": { "font": "custom", "size": 96 }
-```
-
-### Fallback stacks
-
-`style.font` takes a CSS-style list, and it falls through in order:
-
-```json
-"style": { "font": "Display, Bebas Neue, sans-serif" }
-```
-
-This is a failure policy, not just a preference. If your CDN is unreachable or
-a Google family cannot be fetched, the render continues with the next entry
-instead of failing the job or silently substituting whatever the system happens
-to have. Every substitution is reported on the job:
-
-```json
-"steps": [
- {
- "id": "execute-engine",
- "meta": {
- "fontFallbacks": [
- "font \"Display\" is not available; trying the next entry in \"Display, Bebas Neue\""
- ]
- }
- }
-]
-```
-
-A value naming nothing resolvable is rejected at submit, which catches the
-common mistake of pasting a font URL into `style.font` instead of into `fonts`.
-
-### Weights and languages
-
-`weight` runs 100 to 1000. Variable fonts honour it exactly; a static family
-snaps to its nearest available cut, rounding up on a tie.
-
-Latin, Cyrillic, Greek, Vietnamese, Turkish, Arabic, Hebrew, Thai, Devanagari,
-Bengali, Tamil, Amharic, Khmer, Chinese, Japanese and Korean all render, with
-right-to-left joining and conjunct shaping handled by the engine. Colour emoji
-does not render in text assets.
-
-
-## 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 }
-```
-
-
-
-
-
-## Scene-first authoring
-
-Tracks give you precise multi-track control. For a sequential edit, you can instead describe a list of scenes. Each scene is a self-contained segment with its own clips. A scene's `transition` blends INTO it from the previous scene, so put it on the second of two scenes. `overlays` is a flat list of clips that sit over the whole composition, like a persistent logo or watermark.
-
-```json
-{
- "scenes": [
- { "duration": 4,
- "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/river.mp4" } }] },
- { "duration": 4, "transition": { "transition": "crossfade", "duration": 0.6 },
- "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/forest.mp4" } }] }
- ],
- "overlays": [
- { "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" },
- "transform": { "scale": 0.2, "position": { "x": "88%", "y": "12%" } } }
- ]
-}
-```
-
-A scene's clips can omit `start` and `length` to span the whole scene, and a scene can omit its own `duration` to take the length of the video inside it. Every clip field works the same as in tracks mode. `scenes` and `tracks` are mutually exclusive: a timeline uses one or the other.
-
-## Edit like a pro
-
-You do not need editing experience to get professional results. These are the conventions working editors follow, translated into compose fields. AI agents get the same rules automatically through the MCP compose reference.
-
-### Pacing
-
-Clip `length` is the pacing lever. Social clips run 0.8 to 3 seconds each with a visual change at least every 3 seconds, vlogs 2 to 6, tutorials 5 to 15 (cut at step boundaries), cinematic 4 to 8 with varied lengths. Shorten clips as the video builds, then hold the final shot about twice as long. The first clip is the hook: no fade-in, no logo, the payoff visible or promised within 3 seconds. A 20 to 35 second total is the highest-completion band on social.
-
-Cutting to music: seconds per beat is 60 divided by BPM. Cut every 2 or 4 beats, every beat only at the climax. For 120 BPM music that means 1 or 2 second clips.
-
-### Transitions
-
-Professionals cut. Keep crossfades under 10% of your joins and pick ONE decorative style (slide or zoom) per video, at section boundaries only.
-
-| Use | Transition | Duration |
-| --- | --- | --- |
-| Almost every join | none (hard cut) | 0 |
-| Time passes, mood softens | `crossfade` | 0.6 to 1.0s |
-| Chapter break | `fade` | 1 to 2s |
-| Energy, social | `slide` or `zoom` | 0.3 to 0.7s |
-
-### Text placement and size
-
-On vertical video keep every text element inside x 8 to 92%, y 13.5 to 83% (platform UI covers the rest). Captions sit at y 60 to 75%. Sizes at 1080x1920: titles 135 to 190px, lower thirds 70 to 80px, captions 90 to 105px. Always add a stroke (black, width 4 to 8), a shadow, or a background box. Titles are 6 words or fewer, 2 lines maximum. Pair one display font (Oswald, Anton, Bebas Neue) with one body font (Inter, Roboto).
-
-### Color looks
-
-Apply one look to every clip in the video:
-
-| Look | `color` value |
-| --- | --- |
-| Warm sunset | `{ "temperature": 5000, "saturation": 1.2, "contrast": 1.1 }` |
-| Cool corporate | `{ "temperature": 7500, "contrast": 1.15, "saturation": 0.95 }` |
-| Moody cinematic | `{ "temperature": 6200, "saturation": 0.75, "contrast": 1.25, "gamma": 0.92 }` |
-| Vintage faded | `{ "temperature": 5600, "contrast": 0.85, "saturation": 0.65, "brightness": 0.06 }` |
-| Bright social | `{ "temperature": 6800, "saturation": 1.3, "gamma": 1.15 }` |
-| Night | `{ "temperature": 11000, "saturation": 0.6, "gamma": 0.8, "brightness": -0.15 }` |
-
-With faces in frame stay inside saturation 0.7 to 1.3, hue within 8 degrees, temperature 4500 to 12000K. To match two clips shot differently: fix brightness first, then temperature, then saturation.
-
-### Audio levels
-
-`volume` is linear gain. With speech at 1.0: music under speech 0.10 to 0.12, music alone 0.35 to 0.5, sound effects 0.25 to 0.32. Fade music in over 1 to 2 seconds and out over 3 to 5, ending on the last frame. To duck music under speech, split the music into three clips: before speech at 0.4 with a 0.4s fadeOut, under speech at 0.11, after speech at 0.4 with a 1.5s fadeIn.
-
-### Photos need motion
-
-A static photo in a video reads as a mistake. Give every image clip a gentle Ken Burns move: `"transform": { "scale": 1, "position": { "x": "50%", "y": "50%" }, "animateTo": { "scale": 1.12 } }`.
-
-## Reference
-
-### Output
-
-The `output` object controls the render target. Provide either an explicit `resolution` or a named `size` preset.
-
-| Field | Values |
-| --- | --- |
-| `format` | `mp4` (default), `webm`, `gif` (no audio), `mp3` (audio only), `jpg`, `png` (still frame) |
-| `resolution` | `{ width, height }` (integer pixels). Wins over `size` when both are set |
-| `size` | Named preset: `360p`, `720p`, `1080p`, `1440p`, `4k`, `tiktok`, `reels`, `shorts`, `instagram-story`, `instagram-feed`, `square`, `youtube-hd`, `youtube-4k`, `x-landscape`, `linkedin` |
-| `fps` | Frame rate (default `30`) |
-| `quality` | Encoder ladder: `draft` (fast preview), `standard` (default, web delivery), `high`, `max`. Sets rate factor, encoder speed, and audio bitrate together |
-| `videoCodec` | `h264`, `vp9` (optional, per-format default otherwise) |
-| `audioCodec` | `aac`, `opus`, `mp3` (optional) |
-| `crf` | Advanced: constant rate factor, lower is higher quality. Overrides `quality`. `0`..`51` for h264, up to `63` for vp9 |
-| `encoderPreset` | Advanced: h264 speed preset from `ultrafast` to `veryslow`. Slower means smaller files at the same quality |
-| `frameTime` | second of the timeline to capture for `jpg` / `png` (default `0`) |
-
-### Assets
-
-A clip's `asset` is one of:
-
-| `type` | Fields |
-| --- | --- |
-| `video` | `src`, `trim` `{ from, to }` (seconds in the source), `volume` (default `1`, `0` mutes) |
-| `image` | `src` |
-| `audio` | `src`, `trim`, `volume`, `fadeIn`, `fadeOut` (seconds) |
-| `text` | `text`, `style` |
-| `shape` | `shape` (`rect`, `ellipse`, `circle`, `triangle`, `diamond`, `path`), `color` (one colour, or two for a gradient; omit for outline-only), `gradient`, `stroke` `{ color, width }`, `shadow` `{ color, blur, x, y }`, `position` (the **centre** by default), `anchor`, `size` `{ width, height? }` (omit the height for a shape square in pixels), `radius`, `rotate`, `softness`, `path` + `viewBox`, `animateTo` `{ position, size, rotate, color, radius, easing }`. See [Shapes](#shapes) |
-| `composition` | `timeline`, a nested `{ tracks }` rendered and composited as a single clip. Every clip field below applies to a composition clip too, except `speed` (set speed on the clips inside the composition instead) |
-
-### Clip fields
-
-Every clip has an `asset`. `start` and `length` are optional, and `length` also accepts `"end"` (see [Timing is optional](#timing-is-optional)). Plus any of:
-
-| Field | What it does |
-| --- | --- |
-| `opacity` | `0`..`1`, or keyframes `[{ time, value, easing: step \| linear \| smooth }]`. Keyframe `time` is in seconds from the clip's start |
-| `transform` | `position {x,y}` (percent strings, the clip's center by default), `scale`, `rotate` (degrees), `anchor` (`center` \| `topLeft`), `fit` (`contain` \| `fill`), `animateTo` (Ken Burns end state). Without `position` the clip pins to the top-left corner |
-| `transform.animateTo` | End state for an eased Ken Burns move over the clip's duration: any of `position`, `scale`, `rotate`. Omitted fields keep their start value, so `animateTo: { scale: 1.4 }` is a plain zoom |
-| `crop` | `top` / `right` / `bottom` / `left`, each a percent string like `"10%"`. The kept region scales back up to fill the frame |
-| `speed` | playback rate (`0.5` = slow motion, `2` = fast, and pitch shifts with speed). Avoid a slow-motion clip as the outgoing side of a transition, it can run out of frames mid-overlap |
-| `color` | `brightness` (-1..1), `contrast` (0..4), `saturation` (0..3), `gamma` (0.1..10), `hue` (-180..180), `temperature` (1000..40000 K) |
-| `filter` | `greyscale`, `negative`, `boost`, `muted`, `lighten`, `darken`, `contrast` (mutually exclusive with `color`) |
-| `blur` / `sharpen` | gaussian blur strength / unsharp amount |
-| `chromaKey` | `color` (hex), `similarity` (0..1, default 0.2). Key a clip on an overlay track so the removed color reveals the track below |
-| `blendMode` | `normal` (default), `multiply`, `screen`, `overlay`, `darken`, `lighten`, `add`, `difference`. Applies where the clip meets the layer below it |
-| `pan` | stereo balance, `-1` (left) to `1` (right) |
-| `flip` | `{ horizontal, vertical }` |
-
-### Transitions
-
-Place between two clips on a track: `{ "type": "transition", "transition": , "duration": , "direction"?: }`.
-
| Transition | Effect |
| --- | --- |
| `crossfade` | A dissolves directly into B |
| `fade` | A dips through black, then B fades up |
-| `slide` | B slides in from an edge (`direction`: left, right, up, down) |
-| `zoom` | B grows in from the center |
-| `none` | Hard cut, no blend |
-
-### Scenes
-
-The scene-first shape, when you use `scenes` instead of `tracks`:
-
-| Field | Notes |
-| --- | --- |
-| `scenes[].duration` | Seconds. Defaults to the longest thing in the scene, measured from its clips' own lengths or the sources inside them |
-| `scenes[].transition` | The transition INTO this scene from the previous one: `{ transition, duration, direction? }`, same kinds as the table above |
-| `scenes[].clips` | Clips as in tracks mode. The scene is their container: an omitted `length` runs the clip as long as its source but never past the scene, and `length: "end"` spans the scene |
-| `overlays` | Clips composited over the whole video (a logo, a watermark, a persistent title). The whole video is their container, so an omitted `length` spans it. `start` defaults to 0 |
-
-### Text style
-
-Fields on a text asset's `style`:
-
-| Field | Notes |
-| --- | --- |
-| `font` | Font family, or a CSS fallback stack like `"Display, Inter, sans-serif"`. Any Google Fonts family works by name. Families you declare in the `fonts` input win over both. See [Fonts](#fonts) |
-| `size` | Point size |
-| `weight` | 100..1000 (default 400) |
-| `color` | Hex fill |
-| `align` | `left`, `center`, `right` |
-| `position` | `{ x, y }` as percent strings |
-| `stroke` | `{ color, width }` outline |
-| `background` | Hex fill behind the text |
-| `shadow` | `{ color, opacity, offsetX, offsetY }` |
-| `animate` | `{ type, duration }` (type: fade, slideUp, slideDown, slideLeft, slideRight) |
+| `slide` | B slides in over A from an edge |
+| `push` | B pushes A off the frame |
+| `wipe` | An edge travels across, in a shape or along your own greyscale map |
+| `zoom` | B grows in from the centre |
+| `none` | A hard cut, no blend |
+
+`slide`, `push` and `wipe` take a `direction`: `left`, `right`, `up` or `down`. A wipe takes a `shape` instead (`circle`, `box`, `barnDoorHorizontal`, `barnDoorVertical`), or a `map`: a greyscale image of your own, where black turns over first and white last.
+
+```json
+{ "type": "transition", "transition": "wipe", "shape": "circle", "duration": 0.8 }
+```
+
+## Everything else
+
+This page is the first render and the shape of a request. Each idea past that has its own page.
+
+
+
+ Tracks, clips and timing, and why you can leave most of the timing out.
+
+
+ Position, scale, rotate, crop, pin and fade a clip, each shown before and after.
+
+
+ Crossfade, fade, slide, push, wipe and zoom, each one shown playing.
+
+
+ Keyframes on anything numeric, and all 33 easings.
+
+
+ Titles, lower thirds, a running clock, and subtitles from a file.
+
+
+ Bars, circles, paths and masks, with fills, outlines and shadows.
+
+
+ Colour, looks, stylised filters and layer effects, before and after.
+
+
+ Levels, fades, voice clean-up, and sound drawn on the picture.
+
+
+ Frame size, format, quality, and a free check before you render.
+
+
+ The pacing, transition and text conventions working editors follow.
+
+
+ Every field, generated from the schema the API validates with.
+
+
## See also
+
+- [Effects reference](https://rendobar.com/compose/effects/): every effect and transition, rendered before and after
- [Job output](/concepts/job#the-output): the output shape every job returns
- [Animated captions](/jobs/captions/animate): word-level captions over a rendered video
- [Compress](/jobs/compress): shrink the render to a size or quality budget
diff --git a/jobs/compose/audio.mdx b/jobs/compose/audio.mdx
new file mode 100644
index 0000000..c6926e8
--- /dev/null
+++ b/jobs/compose/audio.mdx
@@ -0,0 +1,45 @@
+---
+title: "Audio levels and effects"
+sidebarTitle: "Audio"
+description: "Set levels and fades, clean up a voice recording, and draw sound onto the picture as a waveform or bars."
+icon: "waveform-lines"
+keywords: ["video audio api", "audio normalize api", "audiogram api"]
+canonical: "https://rendobar.com/docs/jobs/compose/audio"
+---
+
+Sound is a clip like any other, and a `volume` on any clip that has audio sets its level. Fades sit next to it. Everything past that is an effect on the `sound` medium, and the same effect list works on a clip or on a whole track.
+
+```json
+{
+ "asset": { "type": "audio", "src": "https://example.com/music.mp3", "volume": 0.35, "fadeIn": 1.5, "fadeOut": 2 },
+ "length": "end"
+}
+```
+
+## Levels that work
+
+
+`volume` is linear gain. With speech at 1.0: music under speech 0.10 to 0.12, music alone 0.35 to 0.5, sound effects 0.25 to 0.32. Fade music in over 1 to 2 seconds and out over 3 to 5, ending on the last frame. To duck music under speech, keep one music clip and put keyframes on its `volume`: 0.4 before the first word, 0.11 under the speech, back to 0.4 after it. `fadeIn` and `fadeOut` work alongside those keyframes.
+
+## Cleaning up a recording
+
+`loudness` brings a quiet recording to a streaming target. `voiceDenoise` removes steady background noise from under speech. `gate` closes the microphone between phrases, `highPass` cuts rumble below the voice, `eq` shapes it, and `compressor` evens out loud and quiet words.
+
+## Shaping and colour
+
+`reverb`, `echo`, `pitch`, `lowPass` and `limiter`. A limiter makes music louder without letting peaks clip.
+
+## Sound drawn on the picture
+
+`visualizer` draws the sound onto the frame for an audiogram or a podcast clip, as a waveform, bars or a level meter.
+
+
+
+
+
+
+
+
+
+
+Every sound effect and its parameters are in the [effect reference](/docs/jobs/compose/reference/effects).
diff --git a/jobs/compose/craft.mdx b/jobs/compose/craft.mdx
new file mode 100644
index 0000000..cfd9f08
--- /dev/null
+++ b/jobs/compose/craft.mdx
@@ -0,0 +1,56 @@
+---
+title: "Edit like a professional"
+sidebarTitle: "Craft"
+description: "Use the pacing, transition, text and colour conventions working editors follow, written here as compose fields."
+icon: "clapperboard"
+keywords: ["video editing best practices", "social video pacing", "video title safe area"]
+canonical: "https://rendobar.com/docs/jobs/compose/craft"
+---
+
+
+
+You do not need editing experience to get professional results. These are the conventions working editors follow, translated into compose fields. AI agents get the same rules automatically through the MCP compose reference.
+
+## Pacing
+
+
+Clip `length` is the pacing lever. Social clips run 0.8 to 3 seconds each with a visual change at least every 3 seconds, vlogs 2 to 6, tutorials 5 to 15 (cut at step boundaries), cinematic 4 to 8 with varied lengths. Shorten clips as the video builds, then hold the final shot about twice as long. The first clip is the hook: no fade-in, no logo, the payoff visible or promised within 3 seconds. A 20 to 35 second total is the highest-completion band on social.
+
+Cutting to music: seconds per beat is 60 divided by BPM. Cut every 2 or 4 beats, every beat only at the climax. For 120 BPM music that means 1 or 2 second clips.
+
+## Text placement and size
+
+
+On vertical video keep every text element inside x 8 to 92%, y 13.5 to 83% (platform UI covers the rest). Captions sit at y 60 to 75%. Sizes at 1080x1920: titles 135 to 190px, lower thirds 70 to 80px, captions 90 to 105px. Always add a stroke (black, width 4 to 8), a shadow, or a background box. Titles are 6 words or fewer, 2 lines maximum. Pair one display font (Oswald, Anton, Bebas Neue) with one body font (Inter, Roboto).
+
+
+
+
+
+
+
+
+
+
+## Color looks
+
+
+Apply one look to every clip in the video:
+
+| Look | `color` value |
+| --- | --- |
+| Warm sunset | `{ "temperature": 5000, "saturation": 1.2, "contrast": 1.1 }` |
+| Cool corporate | `{ "temperature": 7500, "contrast": 1.15, "saturation": 0.95 }` |
+| Moody cinematic | `{ "temperature": 6200, "saturation": 0.75, "contrast": 1.25, "gamma": 0.92 }` |
+| Vintage faded | `{ "temperature": 5600, "contrast": 0.85, "saturation": 0.65, "brightness": 0.06 }` |
+| Bright social | `{ "temperature": 6800, "saturation": 1.3, "gamma": 1.15 }` |
+| Night | `{ "temperature": 11000, "saturation": 0.6, "gamma": 0.8, "brightness": -0.15 }` |
+
+With faces in frame stay inside saturation 0.7 to 1.3, hue within 8 degrees, temperature 4500 to 12000K. To match two clips shot differently: fix brightness first, then temperature, then saturation.
+
+## Photos need motion
+
+
+A static photo in a video reads as a mistake. Give every image clip a gentle Ken Burns move: `"transform": { "position": { "x": "50%", "y": "50%" }, "scale": [{ "time": "0%", "value": 1, "easing": "easeInOutSine" }, { "time": "100%", "value": 1.12 }] }`.
+
+Pacing and transitions have their own longer treatment on [Transitions](/docs/jobs/compose/transitions), and levels on [Audio](/docs/jobs/compose/audio).
diff --git a/jobs/compose/effects.mdx b/jobs/compose/effects.mdx
new file mode 100644
index 0000000..27c8272
--- /dev/null
+++ b/jobs/compose/effects.mdx
@@ -0,0 +1,174 @@
+---
+title: "Picture effects"
+sidebarTitle: "Effects"
+description: "Add colour grades, looks, stylised filters and layer effects to a clip or a whole track, with keyframes on any number."
+icon: "wand-magic-sparkles"
+keywords: ["video effects api", "video color grading api", "chroma key api"]
+canonical: "https://rendobar.com/docs/jobs/compose/effects"
+---
+
+
+
+Effects are one ordered list. The same list sits on a clip, on a track, or on the whole timeline, and it runs in the order you write it.
+
+```json
+{
+ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/sea.mp4" },
+ "start": 0,
+ "length": 3,
+ "effects": [
+ { "type": "color", "saturation": 1.2, "temperature": 6800 },
+ { "type": "vignette" }
+ ]
+}
+```
+
+On a **track**, the list covers every clip on it. On the **timeline**, it covers the finished video, which is where one grade for the whole edit belongs.
+
+```json
+"timeline": {
+ "effects": [{ "type": "look", "name": "muted" }],
+ "tracks": [
+ { "effects": [{ "type": "grain" }], "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/sea.mp4" } }] }
+ ]
+}
+```
+
+Every entry takes an optional `start` and `length`, in seconds or as a percent of what it sits on, so an effect can run for part of a clip. Numeric parameters take keyframes, so they can move while it runs.
+
+```json
+{ "type": "blur", "radius": [{ "time": "0%", "value": 14, "easing": "easeOutCubic" }, { "time": 1, "value": 0 }] }
+```
+
+There are 34 effects, for picture and for sound, each with its own parameters in real units. The full list, with a rendered before and after for each one, is the [effects reference](https://rendobar.com/compose/effects/). The groups:
+
+| Group | Examples |
+| --- | --- |
+| Colour and looks | `color`, `look`, `lut`, `denoise`, `glow`, `grain`, `vignette`, `sharpen`, `blur` |
+| Stylise | `shake`, `glitch`, `mosaic`, `posterize`, `rgbShift`, `scanLines`, `sketch`, `trails` |
+| Layers and masking | `chromaKey`, `mask`, `blurRegion`, `outline`, `dropShadow` |
+| Sound | `eq`, `compressor`, `limiter`, `gate`, `highPass`, `lowPass`, `loudness`, `pitch`, `echo`, `reverb`, `voiceDenoise` |
+| Sound made visible | `visualizer` |
+
+Two of them read a file, which is fetched and checked like any clip source: `lut` takes a `.cube` grade, and `mask` takes a picture that decides what of the clip shows.
+
+```json
+{ "type": "mask", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png", "channel": "alpha" }
+```
+
+An effect that changes the picture is refused on a clip that has none, and one that changes the sound is refused on a silent clip, so a mistake is a 400 rather than a render that ignores it. Text and shapes draw their own outline and shadow, so `outline` and `dropShadow` are refused on them.
+
+## Colour
+
+One `color` effect carries brightness, contrast, saturation, temperature and the rest, so there is one place to correct a picture rather than nine overlapping filters. `look` applies a promoted recipe in one word, and `lut` takes a `.cube` file you supply.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Stylise
+
+`glitch`, `pixelate`, `posterize`, `rgbShift`, `scanLines`, `sketch`, `trails` and `shake`. Used for a beat or a scene change rather than a whole video.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Layers
+
+`chromaKey` removes a solid background, `mask` cuts a clip to a shape, `blurRegion` hides a face or a plate, and `outline` and `dropShadow` edge a layer that has transparency.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Anything the engine can do
+
+
+`{ "type": "engine" }` reaches the rest of the render engine by its own names, for the case a promoted effect does not cover. Every service and property is checked before the job starts, so a typo is a 400 and not a failed render.
+
+```json
+{ "type": "engine", "service": "avfilter.colorize", "params": { "av.hue": 200, "av.saturation": 0.6 } }
+```
+
+An engine entry that duplicates a promoted effect is refused, so there is one way to ask for each thing. The names come from the engine and can change when it is upgraded, which the promoted names never do.
+
+Every effect and every parameter is in the [effect reference](/docs/jobs/compose/reference/effects). Sound effects are on [Audio levels and effects](/docs/jobs/compose/audio).
diff --git a/jobs/compose/fonts.mdx b/jobs/compose/fonts.mdx
new file mode 100644
index 0000000..6bfff83
--- /dev/null
+++ b/jobs/compose/fonts.mdx
@@ -0,0 +1,114 @@
+---
+title: "Fonts and languages"
+sidebarTitle: "Fonts"
+description: "Use your own font files, set fallback stacks, and render scripts that need shaping such as Arabic and Devanagari."
+icon: "language"
+keywords: ["custom fonts video api", "arabic text video api", "video font fallback"]
+canonical: "https://rendobar.com/docs/jobs/compose/fonts"
+---
+
+
+
+Name any [Google Fonts](https://fonts.google.com) family in `style.font` and it
+is fetched at render time. Nothing to upload, no configuration.
+
+```json
+"style": { "font": "Bebas Neue", "size": 96, "weight": 700 }
+```
+
+Seven families are bundled into the render image and resolve instantly, with no
+fetch: `Inter`, `Roboto`, `Montserrat`, `Poppins`, `Oswald`, `Anton`,
+`Bebas Neue`.
+
+
+
+
+
+## Your own font files
+
+
+Put the file URLs in the job's `fonts` input, each under a family name **you**
+choose, then reference that name in `style.font`.
+
+```json
+{
+ "type": "compose",
+ "inputs": {
+ "fonts": [
+ { "family": "Display", "src": "https://cdn.example.com/BrandDisplay.woff2" },
+ { "family": "Body", "src": "https://cdn.example.com/BrandText.otf" }
+ ]
+ },
+ "params": {
+ "schemaVersion": 1,
+ "timeline": {
+ "tracks": [
+ { "clips": [{ "asset": { "type": "text", "text": "LAUNCH", "style": { "font": "Display", "size": 140 } }, "start": 0, "length": 3 }] },
+ { "clips": [{ "asset": { "type": "text", "text": "in stores now", "style": { "font": "Body", "size": 54 } }, "start": 0, "length": 3 }] }
+ ]
+ },
+ "output": { "format": "mp4", "size": "1080p" }
+ }
+}
+```
+
+There is no limit on how many you declare. TTF, OTF, TTC, WOFF and WOFF2 all
+work, and an [uploaded asset's](/sdk#uploads) content URL works in
+`src` like any other input URL.
+
+Naming your own font is the point. The alternative is to reference the family
+name buried inside the file, which you have to discover with a font inspector
+and which is frequently not what the file is called. Omit `family` and that
+embedded name is read for you, so `{ "src": "..." }` on its own is also valid.
+
+A family you declare shadows a bundled one of the same name, exactly the way an
+`@font-face` rule beats a system font in CSS. Declare a font called `Inter` and
+yours is the one that renders.
+
+For a single file, `inputs.font` is the shorthand. It is addressed as `custom`:
+
+```json
+"inputs": { "font": "https://cdn.example.com/BrandDisplay.ttf" },
+"style": { "font": "custom", "size": 96 }
+```
+
+## Fallback stacks
+
+
+`style.font` takes a CSS-style list, and it falls through in order:
+
+```json
+"style": { "font": "Display, Bebas Neue, sans-serif" }
+```
+
+This is a failure policy, not a preference. If your CDN is unreachable or
+a Google family cannot be fetched, the render continues with the next entry
+instead of failing the job or silently substituting whatever the system happens
+to have. Every substitution is reported on the job:
+
+```json
+"steps": [
+ {
+ "id": "execute-engine",
+ "meta": {
+ "fontFallbacks": [
+ "font \"Display\" is not available; trying the next entry in \"Display, Bebas Neue\""
+ ]
+ }
+ }
+]
+```
+
+A value naming nothing resolvable is rejected at submit, which catches the
+common mistake of pasting a font URL into `style.font` instead of into `fonts`.
+
+## Weights and languages
+
+
+`weight` runs 100 to 1000. Variable fonts honour it exactly; a static family
+snaps to its nearest available cut, rounding up on a tie.
+
+Latin, Cyrillic, Greek, Vietnamese, Turkish, Arabic, Hebrew, Thai, Devanagari,
+Bengali, Tamil, Amharic, Khmer, Chinese, Japanese and Korean all render, with
+right-to-left joining and conjunct shaping handled by the engine. Colour emoji
+does not render in text assets.
diff --git a/jobs/compose/framing.mdx b/jobs/compose/framing.mdx
new file mode 100644
index 0000000..3c9dc74
--- /dev/null
+++ b/jobs/compose/framing.mdx
@@ -0,0 +1,131 @@
+---
+title: "Placing and framing a clip"
+sidebarTitle: "Framing"
+description: "Set a clip's position, scale, rotation, box, crop and opacity, and see every one of them before and after on one shot."
+icon: "crop-simple"
+keywords: ["video position api", "picture in picture api", "crop video json"]
+canonical: "https://rendobar.com/docs/jobs/compose/framing"
+---
+
+Every clip takes the same set of fields, whatever it holds. Each one below is the same shot on the same backdrop, changed in exactly one way.
+
+## Where it sits
+
+
+
+
+
+
+
+
+
+
+`position` is the clip's centre by default, as a percent of the frame or in pixels. `anchor` changes what the point refers to.
+
+
+
+
+
+
+
+
+
+
+## How big it is
+
+
+
+
+
+
+
+
+
+
+`scale` multiplies the clip's natural size. `size` gives it a box of its own, which the picture fills the way [fit](/docs/jobs/compose/output#how-a-picture-fills-its-box) says, so a box plus cover is how a wide shot becomes a tall panel.
+
+
+
+
+
+
+
+
+
+
+## Turning and mirroring
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Trimming the edges
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Pinning to a surface
+
+`corners` is the other form of a transform: four points instead of a position. It puts footage on a screen or a sign in the shot, and it does not combine with position, scale, rotate or fit.
+
+
+
+
+
+
+
+
+
+
+## How solid, and how fast
+
+
+
+
+
+
+
+
+
+
+Keyframes on `opacity` are how a clip fades in or out. Anything that moves is on [Keyframes and easing](/docs/jobs/compose/motion).
+
+
+
+
+
+
+
+
+
+
+Every field is in the [clip reference](/docs/jobs/compose/reference/timeline#clip).
diff --git a/jobs/compose/motion.mdx b/jobs/compose/motion.mdx
new file mode 100644
index 0000000..ef670d0
--- /dev/null
+++ b/jobs/compose/motion.mdx
@@ -0,0 +1,49 @@
+---
+title: "Keyframes and easing"
+sidebarTitle: "Motion"
+description: "Add keyframes to position, scale, rotation, opacity, volume and any numeric effect parameter, with 33 easings."
+icon: "chart-line"
+keywords: ["video keyframes api", "ken burns effect api", "video easing curves"]
+canonical: "https://rendobar.com/docs/jobs/compose/motion"
+---
+
+One shape covers a Ken Burns push, a fade, a colour change and ducking music under a voice.
+
+
+Any numeric field that moves takes a list instead of a value: `position`, `scale`, `rotate`, `opacity`, `volume`, a shape's `color` and `radius`, a box's `size`, and any numeric effect parameter.
+
+```json
+"transform": {
+ "scale": [
+ { "time": "0%", "value": 1, "easing": "easeInOutSine" },
+ { "time": "100%", "value": 1.12 }
+ ]
+}
+```
+
+- `time` is seconds from the clip's start, or a percent of its length, so `"100%"` means the last frame without knowing how long the clip is.
+- `easing` shapes the move **from that keyframe to the next one**, so it goes on the first of a pair. The last keyframe's easing is never used.
+- 33 easings: `step`, `linear`, `smooth`, and the `easeIn`, `easeOut` and `easeInOut` families over Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic and Bounce.
+
+A Ken Burns push-in is the example above. A fade is the same shape on `opacity`, and ducking music under a voice is the same shape on `volume`.
+
+
+
+
+
+
+
+
+
+
+A still photograph becomes a shot. Nothing else changed between the two.
+
+## Where keyframes are accepted
+
+`transform.position`, `transform.scale`, `transform.rotate`, `transform.size`, clip `opacity`, clip `volume`, a shape's `color` and `radius`, and every effect parameter marked animatable in the [effect reference](/docs/jobs/compose/reference/effects).
+
+## The 33 easings
+
+`step` holds the value until the next keyframe. `linear` moves at a constant rate. `smooth` eases in and out gently. The remaining 30 are the `easeIn`, `easeOut` and `easeInOut` families over Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic and Bounce.
+
+Back, Elastic and Bounce overshoot the value you gave, which is the point of them, so leave room at the edges of the frame when you use one on `position`.
diff --git a/jobs/compose/output.mdx b/jobs/compose/output.mdx
new file mode 100644
index 0000000..d30e0a9
--- /dev/null
+++ b/jobs/compose/output.mdx
@@ -0,0 +1,75 @@
+---
+title: "Output, size and validation"
+sidebarTitle: "Output"
+description: "Set the frame size, format, frame rate and quality of a render, and check a timeline for free before you pay for it."
+icon: "file-video"
+keywords: ["video output format api", "video resolution api", "validate video json"]
+canonical: "https://rendobar.com/docs/jobs/compose/output"
+---
+
+
+
+`output` controls the size, frame rate, format and encoder. Give it an explicit `resolution`, or a named `size` preset like `"1080p"`, `"tiktok"` or `"reels"`.
+
+```json
+"output": { "format": "mp4", "resolution": { "width": 1080, "height": 1920 }, "fps": 30 }
+```
+
+A 9:16 canvas with `"fit": "cover"` or a `crop` on the clip turns landscape footage into a vertical Reel that fills the frame, with no black bars. `"fit": "blur"` keeps all of the picture instead, over a blurred copy of itself.
+
+`background` fills whatever the clips do not cover. It takes a hex colour, or `"transparent"`, which png and webm keep as real alpha. mp4 has no transparency, so it refuses that rather than rendering black.
+
+```json
+{
+ "schemaVersion": 1,
+ "output": { "format": "webm", "size": "1080p" },
+ "timeline": {
+ "background": "transparent",
+ "tracks": [{ "clips": [{ "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" } }] }]
+ }
+}
+```
+
+`range` renders part of the timeline instead of all of it, and on `jpg` and `png` its `start` is the frame you get.
+
+```json
+"output": { "format": "png", "size": "1080p", "range": { "start": 4.5 } }
+```
+
+## How a picture fills its box
+
+A wide photograph in a phone-shaped box. `contain` shows all of it and leaves bars, `cover` fills the box and cuts what overflows.
+
+
+
+
+
+
+
+
+
+
+A compose job renders exactly one file. Two sizes of the same edit are two jobs, which is also how you get them rendered at the same time. `gif` carries no audio, `mp3` is audio only and needs no frame size, and `jpg` and `png` are a single frame.
+
+## Check it before you render
+
+
+`validate` parses a timeline exactly as a render would and costs nothing. It returns the params as the engine reads them, anything it had to upgrade from an older shape, and warnings for settings the render will ignore.
+
+
+
+```ts SDK
+const check = await client.compose.validate(params);
+if (!check.valid) console.error(check.problems);
+```
+
+```bash cURL
+curl -X POST https://api.rendobar.com/compose/validate \
+ -H "Authorization: Bearer rb_YOUR_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{ "params": { "schemaVersion": 1, "output": { "format": "mp4", "size": "1080p" }, "timeline": { "tracks": [{ "clips": [{ "asset": { "type": "video", "src": "https://example.com/a.mp4" } }] }] } } }'
+```
+
+
+
+Every output field is in the [output reference](/docs/jobs/compose/reference/output).
diff --git a/jobs/compose/reference/animation.mdx b/jobs/compose/reference/animation.mdx
new file mode 100644
index 0000000..95ed764
--- /dev/null
+++ b/jobs/compose/reference/animation.mdx
@@ -0,0 +1,21 @@
+---
+title: "Animation asset reference"
+sidebarTitle: "animation fields"
+description: "Add a Lottie animation exported by a designer with its transparency kept, and every field the asset accepts."
+icon: "bolt"
+keywords: ["lottie video api", "animated overlay api", "json animation video"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/animation"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"animation"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `src` * | string | Lottie animation URL: the .json file LottieFiles, After Effects (Bodymovin) and Figma plugins export. It plays once, for its own length, with a transparent background, and fits the frame like an image: place and size it with the transform. |
+| `trim` | object | Which part of the source file to use, in source seconds. Omit to play from the start. |
+| `trim.from` * | number | Start second in the SOURCE file. min 0. |
+| `trim.to` * | number | End second in the SOURCE file (exclusive). over 0. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/audio.mdx b/jobs/compose/reference/audio.mdx
new file mode 100644
index 0000000..3961f0e
--- /dev/null
+++ b/jobs/compose/reference/audio.mdx
@@ -0,0 +1,24 @@
+---
+title: "Audio asset reference"
+sidebarTitle: "audio fields"
+description: "Add an audio clip and set its volume, fades, trim and speed, with every field it accepts listed in full."
+icon: "music"
+keywords: ["audio clip api", "background music api", "audio fade api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/audio"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"audio"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `src` * | string | Audio URL (mp3/wav/aac). Put music on its own track under the video tracks. |
+| `trim` | object | Which part of the source file to use, in source seconds. Omit to play from the start. |
+| `trim.from` * | number | Start second in the SOURCE file. min 0. |
+| `trim.to` * | number | End second in the SOURCE file (exclusive). over 0. |
+| `volume` | number \| object[] | Gain: 1 = source level. 0.2-0.4 sits music under speech. Keyframes duck it: drop to 0.2 while someone speaks and back to 1 after. default `1`. |
+| `fadeIn` | number | Seconds to ramp in from silence. Works with `volume` keyframes: the fade shapes whatever level they set. default `0`, min 0. |
+| `fadeOut` | number | Seconds to ramp out to silence at the clip's end. Works with `volume` keyframes, like `fadeIn`. default `0`, min 0. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/composition.mdx b/jobs/compose/reference/composition.mdx
new file mode 100644
index 0000000..cb4e521
--- /dev/null
+++ b/jobs/compose/reference/composition.mdx
@@ -0,0 +1,18 @@
+---
+title: "Composition asset reference"
+sidebarTitle: "composition fields"
+description: "Use a nested timeline as one clip so a group of clips moves, scales and fades together, with every field listed."
+icon: "layer-group"
+keywords: ["nested timeline api", "video group clips", "composition video api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/composition"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"composition"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `timeline` * | object | |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/effects.mdx b/jobs/compose/reference/effects.mdx
new file mode 100644
index 0000000..9ed84c5
--- /dev/null
+++ b/jobs/compose/reference/effects.mdx
@@ -0,0 +1,326 @@
+---
+title: "Effect reference"
+sidebarTitle: "effect fields"
+description: "Read every compose effect and each one of its parameters, grouped by colour, stylise, layers and sound."
+icon: "wand-magic-sparkles"
+keywords: ["video effects reference", "video filter parameters", "audio effects api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/effects"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Every entry of an `effects` list. An effect goes on a clip, on a track, or on the timeline itself, and the list runs in the order you write it. Any entry takes `start` and `length` to run for part of its clip, and a parameter marked animatable takes [keyframes](/docs/jobs/compose/motion).
+
+## Color
+
+### `color`
+
+Grade the picture by hand. Every parameter is optional, and one left out leaves that quality unchanged.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `brightness` | number | -1 to 1, 0 = unchanged. Small moves (0.05 to 0.15) read as exposure lifts. min -1, max 1, animatable. |
+| `contrast` | number | 0 to 4, 1 = unchanged. 1.1 to 1.4 adds punch, above 2 crushes. min 0, max 4, animatable. |
+| `saturation` | number | 0 to 3, 1 = unchanged. 0 is black and white, 1.2 to 1.4 is a vivid social look. min 0, max 3, animatable. |
+| `gamma` | number | 0.1 to 10, 1 = unchanged. Above 1 lifts midtones and shadows. min 0.1, max 10, animatable. |
+| `hue` | number | Hue rotation in degrees, 0 = unchanged. min -180, max 180, in deg. |
+| `temperature` | number | Colour temperature in kelvin, about 6500 is neutral. Lower is warm and orange, higher is cool and blue. min 1000, max 40000, in K. |
+
+### `look`
+
+A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette).
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `name` | `"greyscale"` \| `"negative"` \| `"boost"` \| `"muted"` \| `"lighten"` \| `"darken"` \| `"contrast"` \| `"sepia"` \| `"oldFilm"` | Which look. |
+
+### `blur`
+
+Soften the picture. 5 to 15 makes a backdrop that keeps text over it readable.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `radius` | number | How far the blur spreads, in pixels. 0 is no blur. min 0, in px, animatable. |
+
+### `sharpen`
+
+Crisp up soft footage.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1. 0.2 to 0.6 is subtle, 1 is as sharp as it goes. default `0.4`, min 0, max 1, animatable. |
+
+### `denoise`
+
+Clean the grain a small sensor adds in low light. Works across neighbouring frames as well as inside each one.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1. 0.3 to 0.6 cleans a dark phone clip, higher starts to smear fine detail. default `0.55`, min 0, max 1, animatable. |
+
+### `glow`
+
+Make the bright parts of the picture bloom. Best on lights against a dark frame: a city at night, a stage, a candle.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how far the bloom spreads. default `0.45`, min 0, max 1, animatable. |
+
+### `grain`
+
+A layer of film grain, so digital footage feels less clinical.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1. 0.2 to 0.35 is a light film stock, above 0.6 is heavy and deliberate. default `0.28`, min 0, max 1, animatable. |
+
+### `lut`
+
+Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `src` | file | URL of the LUT: a 3D .cube file, the kind DaVinci Resolve, Premiere and most LUT packs export. |
+
+### `vignette`
+
+Darken the corners to pull the eye to the middle of the frame.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how dark the corners get. 1 is black. default `0.6`, min 0, max 1, animatable. |
+| `radius` | number | 0 to 1, how much of the frame stays clear. Smaller closes in on the centre. default `0.5`, min 0, max 1, animatable. |
+
+## Layers
+
+### `chromaKey`
+
+Remove a solid background colour (green screen). Put the keyed clip on an UPPER track, and the removed area shows the track below.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `color` | color | The background colour to remove, hex like "#00FF00" for green screen. |
+| `similarity` | number | How close a pixel must be to the key colour to become transparent. Raise toward 0.3 to 0.4 for uneven lighting. default `0.2`, min 0, max 1. |
+
+### `blurRegion`
+
+Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `x` | length | Centre of the region, measured from the left edge. |
+| `y` | length | Centre of the region, measured from the top edge. |
+| `width` | length | Width of the region. |
+| `height` | length | Height of the region. |
+| `shape` | `"ellipse"` \| `"rect"` | ellipse suits a face, rect a plate or a screen. default `"ellipse"`. |
+| `radius` | number | How far the blur spreads inside the region, in pixels. 40 makes a face unrecognisable at 1080p. default `40`, min 0, in px, animatable. |
+
+### `mask`
+
+Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it. A picture seen through a logo, or any shape you can draw.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `src` | file | URL of the mask image, png or jpg. It is stretched over the clip's picture, so draw it at the same shape. |
+| `channel` | `"luma"` \| `"alpha"` | What of the image decides: luma reads its brightness, for a black and white matte; alpha reads its transparency, for a png cut-out like a logo. default `"luma"`. |
+
+### `outline`
+
+A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo. With dropShadow it is the sticker look. Does nothing on a clip that fills the frame. Text and shapes have their own stroke.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `color` | color | Stroke colour, hex like "#FFFFFF". default `"#FFFFFF"`. |
+| `width` | number | Stroke width in pixels. The engine draws up to 20. default `8`, min 0, max 20, in px, animatable. |
+
+### `dropShadow`
+
+A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it. Does nothing on a clip that fills the frame.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `color` | color | Shadow colour, hex. default `"#000000"`. |
+| `opacity` | number | 0 to 1, how dark the shadow is. default `0.6`, min 0, max 1. |
+| `blur` | number | How soft the shadow's edge is, in pixels. default `16`, min 0, max 100, in px, animatable. |
+| `offsetX` | number | How far right the shadow falls, in pixels. Negative is left. default `0`, min -100, max 100, in px, animatable. |
+| `offsetY` | number | How far down the shadow falls, in pixels. Negative is up. default `8`, min -100, max 100, in px, animatable. |
+
+## Sound
+
+### `visualizer`
+
+Draw the sound over the picture: the audiogram look. In the TIMELINE's effects it draws the final mix, which is the usual place. On a track or a clip it draws that track's or that clip's own sound.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `kind` | `"waveform"` \| `"spectrum"` | waveform is a moving line, spectrum is frequency bars that jump with the sound. default `"waveform"`. |
+| `color` | color | Colour of the drawing, hex. default `"#FFFFFF"`. |
+| `x` | length | Centre of the drawing, measured from the left edge. default `"50%"`. |
+| `y` | length | Centre of the drawing, measured from the top edge. The default sits it in the lower third. default `"77%"`. |
+| `width` | length | Width of the drawing. default `"88%"`. |
+| `height` | length | Height of the drawing. default `"38%"`. |
+
+### `eq`
+
+Three-band tone control. More bass and treble on a music bed, or less boom on a voice.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `bass` | number | Low end, a shelf from about 150 Hz down, in decibels. 0 is unchanged, 3 to 6 warms a music bed, negative thins a boomy voice. min -70, max 6, in dB. |
+| `mid` | number | Middle, two octaves around 1 kHz, in decibels. 0 is unchanged. A few decibels down softens a harsh voice. min -70, max 6, in dB. |
+| `treble` | number | Top end, a shelf from about 6 kHz up, in decibels. 0 is unchanged, 3 to 5 adds sparkle and presence. min -70, max 6, in dB. |
+
+### `compressor`
+
+Even out a voice, so quiet words and loud words sit at one level. The defaults suit speech.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `threshold` | number | The level above which it starts to turn the sound down, in decibels. default `-22`, min -30, max 0, in dB. |
+| `ratio` | number | How hard it turns down what is over the threshold. 2 to 4 is natural on speech, above 10 is a limiter. default `4`, min 1, max 20. |
+| `makeup` | number | Gain added back afterwards, in decibels, so the evened-out sound is as loud as before. default `9`, min 0, max 24, in dB. |
+
+### `limiter`
+
+Stop peaks from clipping. Add gain to make a track louder with no distortion, or leave gain at 0 as a safety net on the final mix.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `gain` | number | Gain going in, in decibels. 6 to 9 makes music clearly louder. default `0`, min -20, max 20, in dB. |
+| `ceiling` | number | The level nothing is allowed over, in decibels. default `-1`, min -20, max 0, in dB. |
+
+### `gate`
+
+Close the microphone between phrases, so room noise drops away in the pauses.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `threshold` | number | Sound quieter than this is turned down, in decibels. Set it just above the noise. default `-30`, min -70, max 20, in dB. |
+
+### `highPass`
+
+Cut everything below a frequency. Wind, traffic and handling noise live under the voice.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `cutoff` | number | Frequencies below this are removed, in hertz. 80 to 120 cleans speech, 200 and up is drastic. default `100`, min 5, max 21600, in Hz. |
+
+### `lowPass`
+
+Cut everything above a frequency. Music from the next room, or a voice on the phone.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `cutoff` | number | Frequencies above this are removed, in hertz. default `700`, min 5, max 21600, in Hz. |
+
+### `loudness`
+
+Bring sound to a standard loudness, riding the level as it goes. -16 suits online video and podcasts, -23 is broadcast.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `target` | number | The loudness to aim for, in LUFS. default `-16`, min -50, max -10, in LUFS. |
+
+### `pitch`
+
+Raise or lower the pitch without changing the speed.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `semitones` | number | How far to shift. 12 is an octave up, -3 deepens a voice. min -36, max 36, in semitones. |
+
+### `echo`
+
+A repeat behind the sound.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `delay` | number | Time before the repeat, in seconds. The engine starts at 0.1. default `0.28`, min 0.1, max 10, in s. |
+| `amount` | number | 0 to 1, how loud the repeat is against the original. default `0.4`, min 0, max 1. |
+
+### `reverb`
+
+Place a dry voice in a hall.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how much room you hear. 0.3 is a touch of air, 0.8 is a cathedral. default `0.5`, min 0, max 1. |
+
+### `voiceDenoise`
+
+Remove steady background noise from under speech: a river, a fan, a street. Trained on voices, so keep it off music.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how much of the noise is removed. default `1`, min 0, max 1. |
+
+## Stylize
+
+### `shake`
+
+Handheld camera energy on a steady shot. The picture is zoomed in slightly so its edges never show.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how far the frame moves. 0.2 to 0.4 is handheld, 1 is an earthquake. default `0.3`, min 0, max 1, animatable. |
+| `speed` | number | 0 to 1, how fast it moves. default `0.3`, min 0, max 1, animatable. |
+
+### `glitch`
+
+Digital break-up: blocks of the picture jump sideways and the colours tear. For a beat or a scene change, so give it a short length.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1, how often the picture breaks. default `0.55`, min 0, max 1, animatable. |
+
+### `pixelate`
+
+Large square pixels over the whole picture.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `size` | number | The side of one block, in pixels. default `24`, min 1, in px, animatable. |
+
+### `posterize`
+
+Flatten the picture into a few steps of each colour, like a screen print.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `levels` | number | How many steps each colour channel keeps. 2 to 6 is a poster, above 16 is barely visible. default `4`, min 2, max 48, animatable. |
+
+### `rgbShift`
+
+Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `distance` | number | How far each channel moves, in pixels. The engine stops at a sixteenth of the frame's width. default `10`, min 0, in px, animatable. |
+
+### `scanLines`
+
+The dark horizontal lines of a CRT monitor.
+
+_No parameters._
+
+### `sketch`
+
+Turn the picture into a pencil drawing.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `amount` | number | 0 to 1. 1 is a pure pencil drawing on white, lower lets the original colours wash back in behind the lines. default `1`, min 0, max 1, animatable. |
+
+### `trails`
+
+Blend the last moments of the clip together, so moving lights and people leave streaks.
+
+| Parameter | Type | What it does |
+| --- | --- | --- |
+| `duration` | number | How long a streak lasts, in seconds. default `0.9`, min 0, in s. |
+
+## Easings
+
+Any keyframe takes one of 33: `step`, `linear`, `smooth`, `easeInSine`, `easeOutSine`, `easeInOutSine`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeInQuart`, `easeOutQuart`, `easeInOutQuart`, `easeInQuint`, `easeOutQuint`, `easeInOutQuint`, `easeInExpo`, `easeOutExpo`, `easeInOutExpo`, `easeInCirc`, `easeOutCirc`, `easeInOutCirc`, `easeInBack`, `easeOutBack`, `easeInOutBack`, `easeInElastic`, `easeOutElastic`, `easeInOutElastic`, `easeInBounce`, `easeOutBounce`, `easeInOutBounce`.
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/image.mdx b/jobs/compose/reference/image.mdx
new file mode 100644
index 0000000..437e181
--- /dev/null
+++ b/jobs/compose/reference/image.mdx
@@ -0,0 +1,18 @@
+---
+title: "Image asset reference"
+sidebarTitle: "image fields"
+description: "Add a still image clip and control how long it lasts and how it fills its box, with every field listed in full."
+icon: "image"
+keywords: ["image to video api", "still image clip", "photo video api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/image"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"image"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `src` * | string | Image URL (png/jpg). Give a photo life with scale keyframes from 1 to about 1.15, a Ken Burns move. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/output.mdx b/jobs/compose/reference/output.mdx
new file mode 100644
index 0000000..e2f2642
--- /dev/null
+++ b/jobs/compose/reference/output.mdx
@@ -0,0 +1,31 @@
+---
+title: "Output reference"
+sidebarTitle: "output fields"
+description: "Set the format, frame size, frame rate, codecs, quality and range of the single file a compose job renders."
+icon: "file-video"
+keywords: ["video output api", "video resolution json", "video codec api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/output"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+What the render produces. One compose job renders exactly one file.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `format` | `"mp4"` \| `"webm"` \| `"gif"` \| `"mp3"` \| `"jpg"` \| `"png"` | Output container/format. mp4=H.264+AAC, webm=VP9+Opus, gif=animated (no audio), mp3=audio only and needs no size, jpg/png=single frame. default `"mp4"`. |
+| `size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. |
+| `resolution` | object | Explicit output dimensions. Wins over `size`. Every format but mp3 needs this or `size`. |
+| `resolution.width` * | integer | Output width in pixels. over 0, max 9007199254740991. |
+| `resolution.height` * | integer | Output height in pixels. over 0, max 9007199254740991. |
+| `fps` | number | Output frame rate. Defaults to 30. default `30`, over 0. |
+| `quality` | `"draft"` \| `"standard"` \| `"high"` \| `"max"` | Encoder quality ladder (crf + speed + audio bitrate). draft=fast preview, standard=web delivery (default), high/max=higher fidelity, slower. Explicit `crf`/`encoderPreset` override the ladder. default `"standard"`. |
+| `videoCodec` | `"h264"` \| `"vp9"` | The video codec. mp4 takes h264 (default) or vp9. webm is always vp9. gif is always GIF, so it takes none. |
+| `audioCodec` | `"aac"` \| `"opus"` \| `"mp3"` | The audio codec. mp4 takes aac (default), opus or mp3. webm is always opus and mp3 always mp3. |
+| `crf` | integer | Advanced: constant rate factor (lower = higher quality/larger file). Overrides `quality`. h264 accepts 0-51, vp9 0-63. Does not apply to gif/mp3/jpg/png. min 0, max 63. |
+| `encoderPreset` | `"ultrafast"` \| `"superfast"` \| `"veryfast"` \| `"faster"` \| `"fast"` \| `"medium"` \| `"slow"` \| `"slower"` \| `"veryslow"` | Advanced: x264 speed/compression preset (slower = smaller file, same quality). Overrides `quality`. h264 only. |
+| `range` | object | Which part of the timeline to render: a preview, or one section checked without paying for the whole render. A jpg or png is the single frame at `start`, the first frame when there is no range. |
+| `range.start` | number | Second of the timeline to start from. For a jpg or png, the moment captured. default `0`, min 0. |
+| `range.length` | number | Seconds to render. Leave it out to run to the end. A range past the end stops at the end. A jpg or png is one frame and ignores it. over 0. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/shape.mdx b/jobs/compose/reference/shape.mdx
new file mode 100644
index 0000000..a118ea7
--- /dev/null
+++ b/jobs/compose/reference/shape.mdx
@@ -0,0 +1,32 @@
+---
+title: "Shape asset reference"
+sidebarTitle: "shape fields"
+description: "Render a rectangle, ellipse, polygon or path as a clip, with every field a shape asset accepts listed in full."
+icon: "shapes"
+keywords: ["video shape api", "draw rectangle video", "svg path video"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/shape"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"shape"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `shape` | `"rect"` \| `"ellipse"` \| `"circle"` \| `"triangle"` \| `"diamond"` \| `"path"` | What to draw, in the box the clip's `transform.size` gives it. Size, place and turn the shape with the transform like any clip, and with no size it fills the frame. rect is the default and the only kind that takes a `radius` or a two-colour fill plus stroke. circle takes one side of the box, its diameter, so it stays round on any frame. triangle points down until you `rotate` it. path draws whatever SVG path data you paste into `path`. A LINE is a rect with a small height, turned with `rotate` when it needs to be diagonal. default `"rect"`. |
+| `color` | string \| any[] \| object[] | Fill colour, "#RRGGBB", or "#RRGGBBAA" to make the fill itself translucent. "#00000080" is a 50% scrim to sit text on. Give TWO colours as an array and it is a gradient, ["#E11D48","#3B82F6"], running in the direction `gradient` names. Keyframes change it over time, and a solid can become a gradient. Leave it out and only the `stroke` draws, which is an outline with a real hole in it. The clip opacity makes the whole shape translucent the same way, outline and shadow included, and can be keyframed. |
+| `gradient` | `"vertical"` \| `"horizontal"` \| `"diagonal"` \| `"radial"` | Which way a two-colour `color` runs: vertical (top to bottom, the default), horizontal, diagonal, or radial from the centre out. The gradient is mapped to the SHAPE, not the frame, so a small badge still shows the whole ramp. |
+| `stroke` | object | An outline drawn just inside the edge of the shape. With no `color` it is an outline only, and what it surrounds is genuinely transparent. |
+| `stroke.color` * | string | Outline colour, hex like "#FFFFFF". pattern `^#[0-9A-Fa-f]{6}$`. |
+| `stroke.width` * | number | Outline width in pixels of the output frame, drawn INSIDE the edge of the shape. 2-8 reads well at 1080p. over 0. |
+| `path` | string | SVG path data, the `d` attribute straight out of a design tool, for `shape: "path"`. Supports M L H V C S Q T Z, absolute and relative. Elliptical arcs (A) are refused rather than approximated, so re-export with arcs converted to curves. The path is scaled into the clip's box, so the same data works at any size on any frame. |
+| `viewBox` | string | The coordinate space `path` is written in, like "0 0 24 24" for a pasted icon. Defaults to "0 0 100 100", so a path written in percentages needs no viewBox at all. |
+| `radius` | number \| object[] | Corner rounding on a rect. 0 is square, 1 is fully round, which gives a pill on a wide box and a circle on a square one. Keyframes round it off or square it up. Rectangles only, and not combinable with a turn or `softness`. default `0`. |
+| `softness` | number | Feathers the edge; 0 is a hard edge. 0.1 to 0.3 makes a soft spotlight or vignette instead of a hard-edged shape. Not available on a rect that has a `radius`. default `0`, min 0, max 1. |
+| `shadow` | object | A drop shadow behind the shape. Every field has a default, so "shadow": {} is a sensible one. It follows the shape, including its outline, its turn and its animation. |
+| `shadow.color` | string | Shadow colour, usually black with alpha like "#00000080". default `"#00000080"`, pattern `^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `shadow.blur` | number | Blur radius in pixels. 8-20 reads as a soft drop shadow; 0 is a hard offset copy. default `12`, min 0. |
+| `shadow.x` | number | Horizontal offset in pixels, positive to the right. default `6`. |
+| `shadow.y` | number | Vertical offset in pixels, positive downward. default `6`. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/subtitles.mdx b/jobs/compose/reference/subtitles.mdx
new file mode 100644
index 0000000..6a05021
--- /dev/null
+++ b/jobs/compose/reference/subtitles.mdx
@@ -0,0 +1,34 @@
+---
+title: "Subtitles asset reference"
+sidebarTitle: "subtitles fields"
+description: "Burn subtitles written inline or read from an SRT or VTT file, with every field the asset accepts listed."
+icon: "closed-captioning"
+keywords: ["burn subtitles api", "srt to video", "video captions json"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/subtitles"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"subtitles"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `cues` | object[] | The lines, in order. Times count from the start of the clip, so moving the clip moves its subtitles. Or give `src` instead. |
+| `cues.start` * | number | When the line appears, in seconds from the start of THIS clip. min 0. |
+| `cues.end` * | number | When the line goes, in seconds from the start of this clip. over 0. |
+| `cues.text` * | string | The line. Keep it to two short rows. |
+| `src` | string | An .srt or .vtt file with the lines, instead of `cues`: what a transcription service, or the caption.extract job, gives you. Its times count from the start of the clip. |
+| `reveal` | `"line"` \| `"typewriter"` | line shows each cue at once, typewriter types it out word by word, the social-video style. default `"line"`. |
+| `style` * | object | |
+| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
+| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
+| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
+| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
+| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/text.mdx b/jobs/compose/reference/text.mdx
new file mode 100644
index 0000000..67a6f82
--- /dev/null
+++ b/jobs/compose/reference/text.mdx
@@ -0,0 +1,29 @@
+---
+title: "Text asset reference"
+sidebarTitle: "text fields"
+description: "Add a text clip and set its font, fill, stroke, shadow and alignment, with every field it accepts listed."
+icon: "font"
+keywords: ["text overlay api", "video title api", "text style json"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/text"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"text"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `text` * | string | The text to render. Keep titles short, 2-6 words reads best. |
+| `style` * | object | |
+| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
+| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
+| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
+| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
+| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/timeline.mdx b/jobs/compose/reference/timeline.mdx
new file mode 100644
index 0000000..bf3d834
--- /dev/null
+++ b/jobs/compose/reference/timeline.mdx
@@ -0,0 +1,100 @@
+---
+title: "Timeline, tracks and clips"
+sidebarTitle: "timeline fields"
+description: "Read the shape of a compose request from the outside in: the params, the timeline, a track, and every clip field."
+icon: "table-list"
+keywords: ["compose timeline reference", "video clip fields", "json video schema"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/timeline"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+The shape of a compose request, from the outside in. Each asset kind adds its own fields on top of the clip fields here, and those are one page each under this group.
+
+## Top level
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `schemaVersion` * | `1` | Timeline schema version. Always 1. |
+| `output` * | object | |
+| `output.format` | `"mp4"` \| `"webm"` \| `"gif"` \| `"mp3"` \| `"jpg"` \| `"png"` | Output container/format. mp4=H.264+AAC, webm=VP9+Opus, gif=animated (no audio), mp3=audio only and needs no size, jpg/png=single frame. default `"mp4"`. |
+| `output.size` | `"360p"` \| `"720p"` \| `"1080p"` \| `"1440p"` \| `"4k"` \| `"tiktok"` \| `"reels"` \| `"shorts"` \| `"instagram-story"` \| `"instagram-feed"` \| `"square"` \| `"youtube-hd"` \| `"youtube-4k"` \| `"x-landscape"` \| `"linkedin"` | Named resolution preset (dimensions only): a generic ladder ("1080p", "4k") or a platform alias ("tiktok", "reels", "youtube-4k"). An explicit `resolution` overrides this. Not used by mp3. |
+| `output.resolution` | object | Explicit output dimensions. Wins over `size`. Every format but mp3 needs this or `size`. |
+| `output.fps` | number | Output frame rate. Defaults to 30. default `30`, over 0. |
+| `output.quality` | `"draft"` \| `"standard"` \| `"high"` \| `"max"` | Encoder quality ladder (crf + speed + audio bitrate). draft=fast preview, standard=web delivery (default), high/max=higher fidelity, slower. Explicit `crf`/`encoderPreset` override the ladder. default `"standard"`. |
+| `output.videoCodec` | `"h264"` \| `"vp9"` | The video codec. mp4 takes h264 (default) or vp9. webm is always vp9. gif is always GIF, so it takes none. |
+| `output.audioCodec` | `"aac"` \| `"opus"` \| `"mp3"` | The audio codec. mp4 takes aac (default), opus or mp3. webm is always opus and mp3 always mp3. |
+| `output.crf` | integer | Advanced: constant rate factor (lower = higher quality/larger file). Overrides `quality`. h264 accepts 0-51, vp9 0-63. Does not apply to gif/mp3/jpg/png. min 0, max 63. |
+| `output.encoderPreset` | `"ultrafast"` \| `"superfast"` \| `"veryfast"` \| `"faster"` \| `"fast"` \| `"medium"` \| `"slow"` \| `"slower"` \| `"veryslow"` | Advanced: x264 speed/compression preset (slower = smaller file, same quality). Overrides `quality`. h264 only. |
+| `output.range` | object | Which part of the timeline to render: a preview, or one section checked without paying for the whole render. A jpg or png is the single frame at `start`, the first frame when there is no range. |
+| `timeline` * | object | The edit itself, in ONE of two shapes: { tracks } for precise multi-track control (layers, exact timing), or { scenes, overlays } for sequential scene-by-scene authoring. Not both. |
+| `prompt` | string | The natural-language request this timeline was generated from, when AI assist was used. |
+| `assets` | string[] | The asset URLs supplied to AI assist, when it was used. |
+
+## Timeline
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `background` | string \| `"transparent"` | The colour behind every clip, wherever nothing else is drawn, like "#FFFFFF". Defaults to black. "transparent" leaves those parts see-through, for png and webm output: a title or a lower third to lay over other video. |
+| `effects` | object[] | Effects on the finished picture and the final mix, after every track is composited: one grade over the whole video, or a limiter on the master. |
+| `tracks` | object[] | Layer stack: the FIRST track is the background, each later track composites on top (overlays, picture-in-picture, titles). Within a track, clips play in time; between two clips a transition item blends them. |
+| `tracks.name` | string | Optional label for your own reference, not rendered. |
+| `tracks.effects` | object[] | Effects on the whole track, after its clips are laid out: grade every clip on it at once, or put a limiter on a music track. |
+| `tracks.clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
+| `scenes` | object[] | Sequential segments played one after another, each with its own clips and an optional transition into it. |
+| `scenes.duration` | number | Scene length in seconds. Omit to take it from the longest thing inside: a clip's own length, or the source in it. over 0. |
+| `scenes.transition` | object | The transition INTO this scene from the previous one. Omit on the first scene and for hard cuts. |
+| `scenes.clips` * | object[] | Clips layered within this scene: first is the background, later clips composite on top. |
+| `overlays` | object[] | Clips composited over the WHOLE video regardless of scenes: a logo, a watermark, a persistent title. The whole video is their container, so omitting `length` spans it. `start` defaults to 0. |
+| `overlays.asset` * | object | |
+| `overlays.start` | number | When the clip begins, in seconds from the start of the video. Omit it and the clip starts after everything already placed on its track, pulled back by any transition sitting immediately before it. The first clip on a track starts at 0. min 0. |
+| `overlays.length` | number \| `"end"` | How long the clip plays. Omit it and the clip runs as long as its CONTENT does: the trim span, the source duration divided by speed, or a composition's own end. Use "end" and it runs to the end of the video instead, whatever the source says, which is what a music bed wants. Text and images have no duration of their own, so omitting is the same as "end" for them and a title with no length stays up. A clip that runs to the end must be the last on its track. |
+| `overlays.opacity` | number \| object[] | Clip opacity: a constant 0..1, or keyframes [{time, value, easing}] for animated fades. Fade a clip in with [{time:0,value:0},{time:1,value:1}]. default `1`. |
+| `overlays.transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns). Omit for full-frame. |
+| `overlays.crop` | object | Trim edges off the frame by percent; the kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `overlays.speed` | number | Playback rate: 0.5 = slow motion, 2 = timelapse. Pitch shifts with speed. Not allowed on composition clips. Avoid slow motion on the OUTGOING side of a transition (it can run out of frames). over 0. |
+| `overlays.effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(service, params): any other engine filter by the engine's own names, strictly typed ones only. |
+| `overlays.blendMode` | `"normal"` \| `"multiply"` \| `"screen"` \| `"overlay"` \| `"darken"` \| `"lighten"` \| `"add"` \| `"difference"` | How this clip mixes with the layer below: screen for light leaks/glows, multiply for darkening textures, overlay for contrast looks, add/lighten/darken/difference for effects. Meaningful on overlay tracks. |
+| `overlays.pan` | number | Stereo balance: -1 full left, 0 centered, 1 full right. min -1, max 1. |
+| `overlays.flip` | object | Mirror the clip horizontally and/or vertically. |
+
+## Track
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `name` | string | Optional label for your own reference, not rendered. |
+| `effects` | object[] | Effects on the whole track, after its clips are laid out: grade every clip on it at once, or put a limiter on a music track. |
+| `clips` * | object[] | Clips in time order, with optional transition items between adjacent clips. Omit start and length and they lay themselves out end to end, each as long as its source, overlapping by any transition between them. Text and image clips have no source, so they run to the end of the video and must come last on their track. |
+
+## Clip
+
+Every clip takes these, whatever its asset.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `start` | number | When the clip begins, in seconds from the start of the video. Omit it and the clip starts after everything already placed on its track, pulled back by any transition sitting immediately before it. The first clip on a track starts at 0. min 0. |
+| `length` | number \| `"end"` | How long the clip plays. Omit it and the clip runs as long as its CONTENT does: the trim span, the source duration divided by speed, or a composition's own end. Use "end" and it runs to the end of the video instead, whatever the source says, which is what a music bed wants. Text and images have no duration of their own, so omitting is the same as "end" for them and a title with no length stays up. A clip that runs to the end must be the last on its track. |
+| `opacity` | number \| object[] | Clip opacity: a constant 0..1, or keyframes [{time, value, easing}] for animated fades. Fade a clip in with [{time:0,value:0},{time:1,value:1}]. default `1`. |
+| `transform` | object | Position/scale/rotate the clip within the frame (picture-in-picture, Ken Burns). Omit for full-frame. |
+| `transform.position` | object \| object[] | Where to place the clip, in pixels like {x:1600,y:900} or percent like {x:"75%",y:"75%"}, or keyframes of them to move it. Omit it and the clip is centred, so a scale of 0.5 alone is a centred picture-in-picture. |
+| `transform.scale` | number \| object[] | Size relative to the frame: 1 = full frame, 0.3 = a corner PiP, above 1 zooms in (crops edges). Keyframes animate it: [{time:"0%",value:1,easing:"easeInOutSine"},{time:"100%",value:1.15}] is a Ken Burns push-in. A keyframe's easing shapes the move into the next one. default `1`. |
+| `transform.rotate` | number \| object[] | Rotation in degrees, clockwise. Keyframes spin it. default `0`. |
+| `transform.anchor` | `"center"` \| `"topLeft"` | What `position` points at: the clip's center (default) or its top-left corner. It is also what the clip turns about. With no position the clip is centred either way. On text, x follows style.align, so anchor only decides whether y is the text's middle or its top edge. default `"center"`. |
+| `transform.fit` | `"contain"` \| `"cover"` \| `"fill"` \| `"blur"` | How a picture fills its box when their shapes differ: contain shows all of it, cover fills the box and cuts what overflows (a vertical phone clip in a 16:9 frame with no bars), fill stretches it, blur shows all of it over an enlarged, blurred copy that fills the bars, for a clip that fills the frame. The box is `size`, or the frame. default `"contain"`. |
+| `transform.size` | object \| object[] | The clip's box, in pixels like {width:480,height:480} or a percent of the frame. Omit it and the box is the whole frame. A picture fills it the way `fit` says: {width:400,height:400} with fit "cover" is a square cut from 16:9 footage. A shape is drawn to it: {width:"100%",height:"12%"} is a lower-third bar. Give one side and the clip keeps its own shape, which for a shape is SQUARE IN PIXELS, the only way to a true circle on a 16:9 frame. `scale` then multiplies the box, for zooms. Keyframes resize it: a width from "0%" to "80%" is a progress bar. |
+| `transform.corners` | object \| object[] | Pins the clip's four corners to four points, in pixels or percent: a picture on a screen, a sign seen in perspective. It is the other form of a transform, so it is not combined with position, scale, rotate or fit. A corner may sit up to a frame beyond each edge. Keyframes move the corners. |
+| `crop` | object | Trim edges off the frame by percent; the kept region scales back up to fill. Use to reframe landscape footage for a vertical canvas, and `radius` to round its corners. |
+| `crop.top` | string | Percent of the frame height to remove from the top. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `crop.right` | string | Percent of the frame width to remove from the right. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `crop.bottom` | string | Percent of the frame height to remove from the bottom. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `crop.left` | string | Percent of the frame width to remove from the left. default `"0%"`, pattern `^-?\d+(\.\d+)?%$`. |
+| `crop.radius` | number | Rounds the corners of what is kept, 0 to 1. 1 rounds the shorter side fully, a pill. The corners ride the transform, so a scaled-down clip becomes a rounded picture-in-picture. The kept part fills the frame, so radius 1 is only a circle on a square frame. min 0, max 1. |
+| `speed` | number | Playback rate: 0.5 = slow motion, 2 = timelapse. Pitch shifts with speed. Not allowed on composition clips. Avoid slow motion on the OUTGOING side of a transition (it can run out of frames). over 0. |
+| `effects` | object[] | What is done to the clip's picture or sound: one ordered list, each entry an object with a "type". Picture effects run in the order listed, and so do sound effects. Any entry takes "start" and "length" (seconds, or a percent of the clip) to run for part of the clip, and a number parameter marked animatable takes keyframes. Effects: chromaKey(color, similarity): Remove a solid background colour (green screen); color(brightness, contrast, saturation, gamma, hue, temperature): Grade the picture by hand; look(name): A one-word look: greyscale, boost (vivid social pop), muted (desaturated, cinematic), lighten and darken (exposure), contrast, negative, sepia (brown archive tone), oldFilm (sepia with grain, dust, scratches, projector flicker and a vignette); blur(radius): Soften the picture; sharpen(amount): Crisp up soft footage; denoise(amount): Clean the grain a small sensor adds in low light; glow(amount): Make the bright parts of the picture bloom; grain(amount): A layer of film grain, so digital footage feels less clinical; lut(src): Grade the picture with a 3D LUT, the .cube file a colourist or a grading app exports; vignette(amount, radius): Darken the corners to pull the eye to the middle of the frame; shake(amount, speed): Handheld camera energy on a steady shot; glitch(amount): Digital break-up: blocks of the picture jump sideways and the colours tear; pixelate(size): Large square pixels over the whole picture; posterize(levels): Flatten the picture into a few steps of each colour, like a screen print; rgbShift(distance): Pull the red and blue channels apart sideways, for a chromatic, analogue-video edge; scanLines(): The dark horizontal lines of a CRT monitor; sketch(amount): Turn the picture into a pencil drawing; trails(duration): Blend the last moments of the clip together, so moving lights and people leave streaks; blurRegion(x, y, width, height, shape, radius): Blur one part of the picture and leave the rest sharp: a face, a number plate, a logo, a screen; mask(src, channel): Show the clip only where an image says: white or opaque keeps it, black or clear cuts it away, grey fades it; outline(color, width): A stroke round the edge of a layer that has transparency: a keyed subject, a picture in picture, a logo; dropShadow(color, opacity, blur, offsetX, offsetY): A soft shadow under a layer that has transparency, so a logo, a cut-out or a picture in picture lifts off what is behind it; visualizer(kind, color, x, y, width, height): Draw the sound over the picture: the audiogram look; eq(bass, mid, treble): Three-band tone control; compressor(threshold, ratio, makeup): Even out a voice, so quiet words and loud words sit at one level; limiter(gain, ceiling): Stop peaks from clipping; gate(threshold): Close the microphone between phrases, so room noise drops away in the pauses; highPass(cutoff): Cut everything below a frequency; lowPass(cutoff): Cut everything above a frequency; loudness(target): Bring sound to a standard loudness, riding the level as it goes; pitch(semitones): Raise or lower the pitch without changing the speed; echo(delay, amount): A repeat behind the sound; reverb(amount): Place a dry voice in a hall; voiceDenoise(amount): Remove steady background noise from under speech: a river, a fan, a street; engine(service, params): any other engine filter by the engine's own names, strictly typed ones only. |
+| `blendMode` | `"normal"` \| `"multiply"` \| `"screen"` \| `"overlay"` \| `"darken"` \| `"lighten"` \| `"add"` \| `"difference"` | How this clip mixes with the layer below: screen for light leaks/glows, multiply for darkening textures, overlay for contrast looks, add/lighten/darken/difference for effects. Meaningful on overlay tracks. |
+| `pan` | number | Stereo balance: -1 full left, 0 centered, 1 full right. min -1, max 1. |
+| `flip` | object | Mirror the clip horizontally and/or vertically. |
+| `flip.horizontal` | boolean | Mirror left-right. default `false`. |
+| `flip.vertical` | boolean | Mirror top-bottom. default `false`. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/timer.mdx b/jobs/compose/reference/timer.mdx
new file mode 100644
index 0000000..8e21bb1
--- /dev/null
+++ b/jobs/compose/reference/timer.mdx
@@ -0,0 +1,30 @@
+---
+title: "Timer asset reference"
+sidebarTitle: "timer fields"
+description: "Add a clock that counts up or down over the length of its own clip, with every field the asset accepts listed."
+icon: "stopwatch"
+keywords: ["video countdown api", "timer overlay video", "clock on video"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/timer"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"timer"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `direction` | `"up"` \| `"down"` | up counts from zero, down counts to zero from the clip's length. default `"up"`. |
+| `format` | `"SS"` \| `"SS.S"` \| `"SS.SS"` \| `"MM:SS"` \| `"MM:SS.SS"` \| `"HH:MM:SS"` | What the clock shows. SS.SS is a race clock, MM:SS a countdown to a launch, HH:MM:SS a stream timer. default `"MM:SS"`. |
+| `style` * | object | |
+| `style.font` | string | Font family, or a CSS fallback stack. Any Google Fonts family works by name. Bundled and instant: Inter, Bebas Neue, Anton, Montserrat, Poppins, Roboto, Oswald. Families declared in the `fonts` job input win over both. A stack falls through in CSS order, so "Display, Inter, sans-serif" tries each in turn. default `"Inter"`. |
+| `style.size` * | number | Font size in pixels of the output frame. ~64-96 for titles at 1080p, ~36-48 for lower thirds. over 0. |
+| `style.weight` | integer | Font weight 100..1000 (400 regular, 700 bold, 800+ display). Honoured exactly on variable fonts; snapped to the nearest available weight otherwise. default `400`, min 100, max 1000. |
+| `style.italic` | boolean | Render the italic cut of the family, when it has one. default `false`. |
+| `style.color` | string | Text fill color, hex like "#FFFFFF". default `"#FFFFFF"`, pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.stroke` | object | Outline around the glyphs. A 2-4px black stroke keeps white text readable on bright footage. |
+| `style.background` | string | Solid box color behind the text, hex. Omit for no box. pattern `^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$`. |
+| `style.align` | `"left"` \| `"center"` \| `"right"` | How the lines line up, and which side of the text sits on the clip's transform.position x: left starts there, center is centred on it, right ends there. Place the text with the clip's transform, not here. default `"center"`. |
+| `style.animate` | object | Entrance animation. Omit for text that is present from its first frame. |
+| `style.shadow` | object | Drop shadow behind the text, adds depth and readability. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/transition.mdx b/jobs/compose/reference/transition.mdx
new file mode 100644
index 0000000..12e4b2d
--- /dev/null
+++ b/jobs/compose/reference/transition.mdx
@@ -0,0 +1,25 @@
+---
+title: "Transition reference"
+sidebarTitle: "transition fields"
+description: "Add a transition on the clip it brings in, or as a clip of its own, with every field it accepts listed in full."
+icon: "shuffle"
+keywords: ["video transition api", "crossfade json", "wipe transition api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/transition"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+A transition is a clip of its own, placed between the two clips it joins. There is no transition field on an ordinary clip.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `transition` * | `"crossfade"` \| `"fade"` \| `"slide"` \| `"push"` \| `"wipe"` \| `"zoom"` \| `"none"` | crossfade dissolves A into B, fade dips through black, slide moves B in over A, push moves B in and A out together, wipe reveals B in place along an edge or a shape, zoom grows B from the center, none is a hard cut. |
+| `direction` | `"left"` \| `"right"` \| `"up"` \| `"down"` | slide, push and an edge wipe: which way the picture travels. Default left. |
+| `shape` | `"edge"` \| `"circle"` \| `"box"` \| `"barnDoorHorizontal"` \| `"barnDoorVertical"` | wipe only. edge (default) sweeps a straight line across in `direction`, circle opens an iris from the centre, box opens a rectangle from the centre, barnDoorVertical opens a vertical gap from the middle out to the sides, barnDoorHorizontal opens a horizontal gap up and down. |
+| `map` | string | wipe only: a wipe of your own, drawn by a greyscale image (png or jpg) at the frame's shape. Black turns over to the next clip first and white last, so a left-to-right gradient is an edge wipe and a radial one an iris. Takes no `shape` or `direction`. |
+| `duration` * | number | Overlap in seconds. 0.4-0.8 is the professional range. Leave the next clip's `start` out and it is placed this far before the clip before it ends. Two clips placed by hand blend over the overlap they have. over 0. |
+| `type` * | `"transition"` | Marks this track item as a transition. Place it BETWEEN two clips; leave the second clip's `start` out and the engine overlaps them by `duration`. |
+
+The kinds and what each looks like are on [Transitions](/docs/jobs/compose/transitions).
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/reference/video.mdx b/jobs/compose/reference/video.mdx
new file mode 100644
index 0000000..ddab343
--- /dev/null
+++ b/jobs/compose/reference/video.mdx
@@ -0,0 +1,22 @@
+---
+title: "Video asset reference"
+sidebarTitle: "video fields"
+description: "Add a video clip and set its source, trim, speed and volume, with every field it accepts listed in full."
+icon: "film"
+keywords: ["video clip api", "trim video json", "video speed api"]
+canonical: "https://rendobar.com/docs/jobs/compose/reference/video"
+---
+
+{/* GENERATED by scripts/gen-docs-compose-reference.mts. Do not edit. */}
+
+Set `asset.type` to `"video"`. These are its own fields, on top of the [clip fields](/docs/jobs/compose/reference/timeline#clip) every clip takes.
+
+| Field | Type | What it does |
+| --- | --- | --- |
+| `src` * | string | Video URL: an https URL or an uploaded asset's content URL. |
+| `trim` | object | Which part of the source file to use, in source seconds. Omit to play from the start. |
+| `trim.from` * | number | Start second in the SOURCE file. min 0. |
+| `trim.to` * | number | End second in the SOURCE file (exclusive). over 0. |
+| `volume` | number \| object[] | Audio gain for this clip: 1 = source level, 0 mutes, 0.2 ducks it under a music track. Keyframes change it over time. default `1`. |
+
+Fields marked `*` are required. Every table on this page is generated from the schema the API validates with, so it cannot drift from what a render accepts.
diff --git a/jobs/compose/shapes.mdx b/jobs/compose/shapes.mdx
new file mode 100644
index 0000000..e47a670
--- /dev/null
+++ b/jobs/compose/shapes.mdx
@@ -0,0 +1,267 @@
+---
+title: "Shapes and masks"
+sidebarTitle: "Shapes"
+description: "Render rectangles, circles, paths and masks with fills, outlines, corners and shadows, and see which pairs are refused."
+icon: "shapes"
+keywords: ["video shape overlay api", "video mask api", "lower third bar api"]
+canonical: "https://rendobar.com/docs/jobs/compose/shapes"
+---
+
+
+
+A `shape` asset draws a rect, ellipse, circle, triangle, diamond, or any SVG path you paste in. No image to make, no file to host, and it stays sharp at any output resolution.
+
+```json
+{
+ "asset": { "type": "shape", "color": "#0A0A0ACC" },
+ "transform": { "size": { "width": "100%", "height": "22%" }, "position": { "x": "50%", "y": "86%" } }
+}
+```
+
+That is a bar across the lower third, dark enough to read a title on and thin enough to see the footage through. Put it on a track below the text.
+
+A shape is placed the way every clip is placed: by the clip's `transform`. `size` is its box, `position` its centre unless `anchor` says otherwise, and `rotate` turns it about that anchor. The asset itself only says what to draw.
+
+With no `size` a shape fills the frame, which is a scrim or a background in one line. Like text it has no duration of its own, so leaving `length` off runs it to the end of its container.
+
+`width` is a percent of the frame's width and `height` a percent of its height. Both also take pixels as plain numbers.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Fills and edges
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Paths
+
+
+
+
+
+
+
+
+
+
+## Leave the height out to get a circle
+
+
+Omit `size.height` and the box is made **square in pixels**. That is the only way to get a true circle or square on a frame that is not square, and it is the one thing about shapes worth memorising.
+
+A `10%` by `10%` box on a 16:9 frame is 192 by 108 pixels. Rounding it fully gives a pill, not a circle. Omit the height and the engine derives 17.8% for you.
+
+```json
+{
+ "asset": { "type": "shape", "shape": "circle", "color": "#059669" },
+ "transform": { "size": { "width": "12%" }, "position": { "x": "88%", "y": "18%" } }
+}
+```
+
+`shape: "circle"` is `ellipse` with that rule applied, so it rejects an explicit height rather than quietly ignoring one. The same omission works on every kind: a `rect` with no height is a square, a `diamond` with no height is a regular one.
+
+## Anchor
+
+
+`anchor: "topLeft"` points `position` at the box's corner instead of its centre. It is what makes letterbox bars arithmetic-free.
+
+```json
+{
+ "asset": { "type": "shape", "color": "#000000" },
+ "transform": { "anchor": "topLeft", "position": { "x": "0%", "y": "90%" }, "size": { "width": "100%", "height": "10%" } }
+}
+```
+
+## Fills, flat and gradient
+
+
+`color` takes `#RRGGBB`, or the 8-digit `#RRGGBBAA` for a constant translucency the way `#0A0A0ACC` does above.
+
+Do not reach for the clip's `opacity` instead. `opacity` fades the whole clip, so keep it for fades and put a constant translucency in the colour.
+
+Give `color` **two** colours and it is a gradient. `gradient` names the direction: `vertical` (the default), `horizontal`, `diagonal` or `radial`.
+
+```json
+{
+ "asset": { "type": "shape", "color": ["#E11D48", "#3B82F6"], "gradient": "diagonal" },
+ "transform": { "size": { "width": "40%", "height": "30%" }, "position": { "x": "50%", "y": "50%" } }
+}
+```
+
+The gradient maps to the **shape**, not the frame, so a small badge shows the whole ramp instead of a sliver of one. Stops take the 8-digit form too, which is how you get a scrim that fades to nothing: `["#0A0A0A00", "#0A0A0AFF"]`.
+
+## Outlines, with a real hole
+
+
+`stroke` draws a border just inside the shape's edge, its `width` in pixels of the output frame. Give the shape a `color` too and you get a filled shape with a border; `radius` rounds both together.
+
+Leave `color` out and only the outline draws. What it surrounds is genuinely transparent, not filled with black, so the footage shows through the middle. That works on every kind except a path.
+
+```json
+{
+ "asset": { "type": "shape", "shape": "ellipse", "stroke": { "color": "#F59E0B", "width": 5 } },
+ "transform": { "size": { "width": "26%", "height": "40%" }, "position": { "x": "62%", "y": "40%" } }
+}
+```
+
+## Rounded corners
+
+
+`radius` runs from `0` (square) to `1` (fully round) on a `rect`. At `1` a wide box becomes a pill and a square one becomes a circle.
+
+## Drop shadows
+
+
+`shadow` follows the shape, including its outline, its rotation and its animation. Every field has a default, so `"shadow": {}` is a sensible shadow.
+
+```json
+{
+ "asset": {
+ "type": "shape",
+ "color": ["#FFFFFF", "#E5E7EB"],
+ "radius": 0.3,
+ "stroke": { "color": "#111827", "width": 3 },
+ "shadow": { "color": "#00000099", "blur": 14, "x": 8, "y": 10 }
+ },
+ "transform": { "size": { "width": "34%", "height": "40%" }, "position": { "x": "30%", "y": "50%" } }
+}
+```
+
+## Rotation turns about the anchor, not the frame
+
+
+`transform.rotate` is degrees clockwise about the shape's own anchor, so it spins where it sits rather than orbiting the middle of the frame. A diagonal rule is a thin rect and one field:
+
+```json
+{
+ "asset": { "type": "shape", "color": "#FFFFFF" },
+ "transform": { "rotate": 30, "size": { "width": "40%", "height": "0.8%" }, "position": { "x": "50%", "y": "72%" } }
+}
+```
+
+There is no `line` type for the same reason there is no `square` type: a line is a rect with a small height, and `rotate` is what makes it diagonal.
+
+## Softness
+
+
+`softness` runs 0 to 1 and feathers the edge. At 0.2 to 0.4 an ellipse reads as a spotlight or a vignette rather than a hard-edged disc. A full-frame ellipse with `softness: 0.5` and a radial `["#00000000", "#000000CC"]` fill is a vignette with no image asset at all.
+
+## Paths
+
+
+`shape: "path"` takes SVG path data straight out of a design tool. `viewBox` gives the coordinate space it was drawn in, defaulting to `"0 0 100 100"`, and the path is scaled into the transform's `size` so the same data works at any size on any frame.
+
+```json
+{
+ "asset": {
+ "type": "shape",
+ "shape": "path",
+ "viewBox": "0 0 24 24",
+ "path": "M12 21C12 21 3 14.5 3 8.5C3 5.5 5.5 3 8.5 3C10.4 3 12 4.5 12 4.5C12 4.5 13.6 3 15.5 3C18.5 3 21 5.5 21 8.5C21 14.5 12 21 12 21Z",
+ "color": "#E11D48"
+ },
+ "transform": { "size": { "width": "20%" }, "position": { "x": "50%", "y": "50%" } }
+}
+```
+
+`M L H V C S Q T Z` are supported, absolute and relative. Elliptical arcs (`A`) are refused rather than approximated, so re-export with arcs converted to curves. A path takes a fill, a gradient, a shadow, rotation and softness, but not a `stroke`: outlining arbitrary path data needs true path offsetting.
+
+## Animation
+
+
+Everything that moves is [keyframes](#keyframes): `position`, `size` and `rotate` on the transform, `color` and `radius` on the shape. A progress bar is a width that grows.
+
+```json
+{
+ "asset": { "type": "shape", "color": "#FFFFFF" },
+ "transform": {
+ "anchor": "topLeft",
+ "position": { "x": "2%", "y": "86%" },
+ "size": [
+ { "time": "0%", "value": { "width": "0%", "height": "1.5%" }, "easing": "smooth" },
+ { "time": "100%", "value": { "width": "96%", "height": "1.5%" } }
+ ]
+ }
+}
+```
+
+A shape that is square in pixels stays square as it grows, so an animated circle does not turn into an ellipse halfway through. A solid colour can animate into a gradient, and a square can round itself off with keyframes on `radius`. An animated `rotate` has no limit: 0 to 720 is two full turns.
+
+## Combinations the API refuses
+
+
+Two engine primitives draw shapes and they have different abilities, so a few combinations are rejected at submit time rather than rendered wrong. Each error names the fix.
+
+| Not combinable | Why | What to do instead |
+| --- | --- | --- |
+| `radius` on anything but a `rect` | only rectangles have corners | drop it |
+| `radius` with no `color` | an outline-only shape is drawn by the primitive with no corner rounding | give it a fill |
+| `radius` with `rotate` or `softness` | the primitive that rounds corners cannot turn or feather | drop one of them |
+| `stroke` on a `path` | outlining arbitrary path data needs true path offsetting | draw the outline into the path itself |
+| `speed` on a shape | there is no file to play faster | animate it with keyframes |
+
+## What else a shape takes
+
+
+`blendMode` mixes it with the layer below, effects apply to it like any clip, and opacity keyframes fade it in and out. A track holding only shapes composites as a base layer, so a solid colour background is a timeline that references no files at all.
+
+There is no arbitrary path stroke and no per-corner radius. Anything past that is still an `image` asset.
+
+Every shape field is in the [shape reference](/docs/jobs/compose/reference/shape).
diff --git a/jobs/compose/text.mdx b/jobs/compose/text.mdx
new file mode 100644
index 0000000..706d82d
--- /dev/null
+++ b/jobs/compose/text.mdx
@@ -0,0 +1,141 @@
+---
+title: "Text, titles and subtitles"
+sidebarTitle: "Text"
+description: "Add text to a video as a title, a lower third, a running clock, a subtitles clip or a Lottie animation."
+icon: "font"
+keywords: ["video text overlay api", "burn subtitles api", "lower third video api"]
+canonical: "https://rendobar.com/docs/jobs/compose/text"
+---
+
+A text clip carries its own words, its own style and its own box. Three neighbouring clip kinds cover the jobs text is usually asked to do: a clock that counts, a subtitles clip that holds every line, and a Lottie animation exported by a designer.
+
+
+A `text` asset renders a styled title. `style` holds the type: `font`, `size`, `weight`, `color`, `align`, a `stroke`, a `background` box, a `shadow`, and an `animate` entrance (`fade`, `slideUp`, `slideDown`, `slideLeft`, `slideRight`). `font` takes any Google Fonts family by name, or a family from your own file. See [Fonts](#fonts).
+
+Where it sits is the clip's `transform`, the same field that places every other clip.
+
+```json
+{
+ "asset": {
+ "type": "text",
+ "text": "Coastal Escape",
+ "style": {
+ "font": "Inter",
+ "size": 84,
+ "weight": 800,
+ "color": "#FFFFFF",
+ "stroke": { "color": "#000000", "width": 6 },
+ "animate": { "type": "slideUp", "duration": 0.7 }
+ }
+ },
+ "transform": { "position": { "x": "50%", "y": "38%" } },
+ "start": 0,
+ "length": 3
+}
+```
+
+Colours are hex: `#RGB`, `#RRGGBB`, or `#RRGGBBAA` with alpha.
+
+## Keeping text readable
+
+White text over bright footage disappears. Three fields fix it, in order of how much they change the look.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Where the lines sit
+
+
+
+
+
+
+
+
+
+
+## Arriving
+
+
+
+
+
+
+
+
+
+
+## A clock
+
+
+A `timer` counts over the clip's own length, up or down, in the format you name.
+
+```json
+{ "asset": { "type": "timer", "direction": "down", "format": "MM:SS", "style": { "font": "JetBrains Mono", "size": 96, "color": "#FFFFFF" } }, "start": 0, "length": 10 }
+```
+
+`format` is `SS`, `SS.S`, `SS.SS`, `MM:SS`, `MM:SS.SS` or `HH:MM:SS`. It takes the same `style` as text.
+
+
+
+
+
+## Subtitles
+
+
+One `subtitles` clip holds every line, either written in the timeline or read from an `.srt` or `.vtt` file. Times are seconds from the clip's start.
+
+```json
+{ "asset": { "type": "subtitles", "cues": [
+ { "start": 0, "end": 1.8, "text": "Subtitles live in the timeline." },
+ { "start": 2, "end": 4, "text": "One clip, every line." }
+ ], "reveal": "typewriter", "style": { "font": "Inter", "size": 44, "color": "#FFFFFF", "background": "#000000B0" } },
+ "transform": { "position": { "x": "50%", "y": "88%" }, "anchor": "topLeft" } }
+```
+
+Or point it at a file: `{ "type": "subtitles", "src": "https://example.com/captions.srt" }`. For word-level captions burned from speech instead, see [animated captions](/jobs/captions/animate).
+
+
+
+
+
+## Lottie animations
+
+
+An `animation` asset plays a Lottie `.json` file, the kind After Effects, Figma and LottieFiles export, with a transparent background. It plays once, for its own length, and fits the frame like an image.
+
+```json
+{ "asset": { "type": "animation", "src": "https://example.com/logo-reveal.json" }, "transform": { "scale": 0.4, "position": { "x": "50%", "y": "50%" } } }
+```
+
+Export it self-contained: a file that pulls images or fonts from elsewhere is refused, because nothing reaches the renderer except through the checked download.
+
+
+
+
+
+Fonts are their own page: [Fonts and languages](/docs/jobs/compose/fonts). Every text field is in the [text reference](/docs/jobs/compose/reference/text).
diff --git a/jobs/compose/timeline.mdx b/jobs/compose/timeline.mdx
new file mode 100644
index 0000000..0781b66
--- /dev/null
+++ b/jobs/compose/timeline.mdx
@@ -0,0 +1,166 @@
+---
+title: "How the timeline works"
+sidebarTitle: "Timeline"
+description: "Build a compose timeline from tracks and clips, and learn when to leave start and length out of it entirely."
+icon: "layer-group"
+keywords: ["video timeline api", "json video tracks", "video clip timing"]
+canonical: "https://rendobar.com/docs/jobs/compose/timeline"
+---
+
+A timeline is tracks, and a track is clips. The first track is the background and every track after it draws over the one before. That is the whole model, and the rest of this page is what falls out of it.
+
+
+The model is two ideas:
+
+- **Tracks stack.** The first track is the background. Each later track composites on top of the ones below it: overlays, titles, picture-in-picture.
+- **Clips sit in time.** A clip can carry a `start` and a `length` in seconds, and it can leave both out. Two clips on the same track are joined by a transition. A gap between them plays nothing.
+
+So a multi-track timeline is layers (tracks) of sequences (clips). That is the whole structure.
+
+## Timing is optional
+
+
+Every clip sits in a **container**: the scene it belongs to, or the whole video. Two sentences cover every case.
+
+- **Omit `length`** and the clip runs as long as its **content** does, never past its container.
+- **Write `length: "end"`** and it runs to the end of its **container**, whatever its content says.
+
+Content means the source's duration, or the trim span, or the source duration divided by `speed` (so `speed: 0.5` on a 4 second clip occupies 8 seconds), or a composition's own end.
+
+Text and images have no duration of their own, so for them the two sentences say the same thing. A title with no `length` stays up for the whole video, which is what a watermark wants.
+
+`start` follows the same shape: omit it and the clip begins after everything already placed on its track, pulled back by any transition immediately before it. Give it a number and that is seconds from the start of its container.
+
+Set either field wherever you want something else. Every timeline written with full timing keeps rendering exactly as it did.
+
+## Worked examples
+
+
+A music bed. The source is three minutes long, the video is not, and `"end"` is how you say so without totalling the clips above it.
+
+```json
+"tracks": [
+ { "clips": [
+ { "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/river.mp4" } },
+ { "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/forest.mp4" } }
+ ] },
+ { "clips": [
+ { "asset": { "type": "audio", "src": "https://cdn.rendobar.com/assets/showcase/music.mp3", "volume": 0.35, "fadeOut": 2 },
+ "length": "end" }
+ ] }
+]
+```
+
+A watermark. It has no duration of its own, so it needs nothing at all.
+
+```json
+{ "clips": [
+ { "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" },
+ "transform": { "scale": 0.15, "position": { "x": "88%", "y": "12%" } }, "opacity": 0.8 }
+] }
+```
+
+Both stop on the last frame, whatever the clips elsewhere add up to. Add another clip later and they still do.
+
+The music bed above, drawn out. Track 1 is the background and the two clips follow each other because neither names a `start`. The music names `length: "end"`, so it stops on the last frame whatever the video adds up to.
+
+```mermaid
+gantt
+ title Two clips and a music bed, in seconds
+ dateFormat X
+ axisFormat %S
+ section Track 2, music
+ audio, length "end" : 0, 12
+ section Track 1, video
+ river.mp4 : 0, 6
+ forest.mp4 : 6, 12
+```
+
+## The rules that fall out of it
+
+
+A clip running to the end of the **video** is measured against every clip that is not, so two of them on different tracks both land on the real end rather than chasing each other. It runs to the last frame, so nothing may follow it on the same track, and a timeline where nothing has a length of its own has no end to measure and is rejected.
+
+A clip never runs past its container, so a 6 second video in a 3 second scene plays 3 seconds, and a 2 second video in a 5 second scene plays 2. That applies to a length you wrote as well as one the engine worked out.
+
+`"end"` is the opposite request and does not cap: it asks for the container's full extent whatever the source holds. On a source shorter than the container that is impossible, and the job is rejected for outrunning its source. Leave the length off instead, or trim less.
+
+Inside a `composition`, `start` and `length` both stay required. A nested timeline's length is set by the clip hosting it, so there is nothing to append to and no container to measure against.
+
+## 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": "https://cdn.rendobar.com/assets/examples/sea.mp4" }, "start": 0, "length": 3 }
+ ] },
+ { "clips": [
+ { "asset": { "type": "text", "text": "Coastal Escape", "style": { "font": "Inter", "size": 84, "color": "#FFFFFF" } },
+ "start": 0, "length": 3 }
+ ] }
+]
+```
+
+
+
+
+
+
+
+
+
+
+## Scene-first authoring
+
+
+Tracks give you precise multi-track control. For a sequential edit, you can instead describe a list of scenes. Each scene is a self-contained segment with its own clips. A scene's `transition` blends INTO it from the previous scene, so put it on the second of two scenes. `overlays` is a flat list of clips that sit over the whole composition, like a persistent logo or watermark.
+
+```json
+{
+ "scenes": [
+ { "duration": 4,
+ "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/river.mp4" } }] },
+ { "duration": 4, "transition": { "transition": "crossfade", "duration": 0.6 },
+ "clips": [{ "asset": { "type": "video", "src": "https://cdn.rendobar.com/assets/examples/forest.mp4" } }] }
+ ],
+ "overlays": [
+ { "asset": { "type": "image", "src": "https://cdn.rendobar.com/assets/brand/logo-mark.png" },
+ "transform": { "scale": 0.2, "position": { "x": "88%", "y": "12%" } } }
+ ]
+}
+```
+
+A scene's clips can omit `start` and `length` to span the whole scene, and a scene can omit its own `duration` to take the length of the video inside it. Every clip field works the same as in tracks mode. `scenes` and `tracks` are mutually exclusive: a timeline uses one or the other.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## A timeline as one clip
+
+A `composition` asset holds a whole timeline of its own, so a group of layers can be moved, scaled and faded together rather than one at a time.
+
+
+
+
+
+
+
+
+
+
+## Where to go next
+
+Clips blend into each other on [Transitions](/docs/jobs/compose/transitions). Anything that moves is on [Keyframes and easing](/docs/jobs/compose/motion). Every field a clip takes is listed in the [clip reference](/docs/jobs/compose/reference/timeline).
diff --git a/jobs/compose/transitions.mdx b/jobs/compose/transitions.mdx
new file mode 100644
index 0000000..a80ffd9
--- /dev/null
+++ b/jobs/compose/transitions.mdx
@@ -0,0 +1,77 @@
+---
+title: "Transitions between clips"
+sidebarTitle: "Transitions"
+description: "Add a crossfade, fade, slide, push, wipe or zoom between two clips, and see why a hard cut is the right default."
+icon: "shuffle"
+keywords: ["video transitions api", "crossfade video api", "wipe transition json"]
+canonical: "https://rendobar.com/docs/jobs/compose/transitions"
+---
+
+Two clips next to each other on a track cut straight from one to the other. To blend them, put a transition BETWEEN them. It is a clip like any other, with its own entry in the track's list, and there is no transition field on an ordinary clip.
+
+```json
+"clips": [
+ { "asset": { "type": "video", "src": "https://example.com/a.mp4" } },
+ { "type": "transition", "transition": "crossfade", "duration": 0.8 },
+ { "asset": { "type": "video", "src": "https://example.com/b.mp4" } }
+]
+```
+
+A scene takes its own `transition` instead, which describes how that scene arrives from the one before it.
+
+The blend is taken out of the two clips, not added to the timeline, so a transition never makes the render longer.
+
+## The seven kinds
+
+`crossfade` dissolves one picture into the other. `fade` goes through the background colour between them, which reads as a chapter break. `slide` moves the new clip in over the old one, `push` shoves the old one out of frame, and both take a `direction`. `wipe` reveals the new clip along a moving edge whose `shape` you choose. `zoom` scales into the new clip. `none` is the hard cut, and it is the default because it is what professional editors use for almost every join.
+
+Every one below joins the same two shots, so they can be compared.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Wipe shapes
+
+`edge` is a straight line travelling in `direction`. `circle` opens from the centre, `box` opens as a rectangle, and `barnDoorHorizontal` and `barnDoorVertical` split from the middle outwards.
+
+## How long it can be
+
+A transition cannot be longer than either clip it joins. Ask for more and the render clamps it to what fits and the job carries a warning, so a template built for long clips still works when a short one arrives. [Validate](/docs/jobs/compose/output#check-it-before-you-render) reports the same warning before you spend anything.
+
+
+## How to use them
+
+Professionals cut. Keep crossfades under 10% of your joins and pick ONE decorative style (slide or zoom) per video, at section boundaries only.
+
+| Use | Transition | Duration |
+| --- | --- | --- |
+| Almost every join | none (hard cut) | 0 |
+| Time passes, mood softens | `crossfade` | 0.6 to 1.0s |
+| Chapter break | `fade` | 1 to 2s |
+| Energy, social | `slide`, `push` or `zoom` | 0.3 to 0.7s |
+
+Every field is in the [transition reference](/docs/jobs/compose/reference/transition).
diff --git a/mcp-server.mdx b/mcp-server.mdx
index ac2a6a9..dc71b1c 100644
--- a/mcp-server.mdx
+++ b/mcp-server.mdx
@@ -61,11 +61,12 @@ Clients without OAuth support can authenticate with an API key instead: header `
## What can I do with it?
-Call `list_job_types` first. It returns every active job type fresh, so it never goes stale like a hardcoded list would. Pass `type` (for example `"compose"`) to get that one job type's complete parameter schema instead, with per-field documentation: ranges, defaults, units, and usage notes. Agents should do this before authoring a complex payload like a compose timeline.
+Call `list_job_types` first. It returns every active job type fresh, so it never goes stale like a hardcoded list would. Pass `type` (for example `"compose"`) to get that one job type's complete parameter schema instead, with per-field documentation: ranges, defaults, units, and usage notes. Agents should do this before authoring a complex payload like a compose timeline. On `compose` it also carries the editor guide. Add `effect` to read one compose effect's parameters on its own, or `effect: "engine"` for the engine's other filters, instead of pulling the whole reference.
| Tool | What it does | Returns |
|---|---|---|
| `list_job_types` | Every active job type: use cases, what it chains with, a params digest, and an example payload. With `type`, the full annotated parameter schema for that one type. | `{ jobTypes[], guidance }` or `{ type, paramsJsonSchema, example, guidance }` |
+| `validate_compose` | Check compose params without rendering. Free, nothing is stored, and it runs the same checks a submit does. Call it before `submit_job` on any timeline you wrote. | `{ valid, problems[], params?, warnings[] }` |
| `submit_job` | Submit a job. `inputs` accepts a URL string, `{ url }`, `{ content }` (inline text), or `{ job: "job_..." }` for chaining. A URL can be a `storage://` URI, and `destinations` writes the output to a connected bucket. | `{ jobId, status }` |
| `get_job` | Check status and results. `wait: true` blocks up to 45 seconds instead of polling. A completed video job includes an inline poster image. | `{ id, status, output?, cost?, error? }` |
| `list_jobs` | Recent jobs, filterable by `status` and `type`. | `{ jobs[], total }` |
diff --git a/scripts/validate-nav-and-redirects.mjs b/scripts/validate-nav-and-redirects.mjs
index 84da443..dc01a3f 100644
--- a/scripts/validate-nav-and-redirects.mjs
+++ b/scripts/validate-nav-and-redirects.mjs
@@ -54,7 +54,14 @@ function toSlug(filePath) {
return relative(ROOT, filePath).replace(/\\/g, '/').replace(/\.mdx?$/, '');
}
-/** Recursively collect every page string under any `pages` array in the nav. */
+/**
+ * Recursively collect every page string in the nav.
+ *
+ * A page reaches the nav two ways: as an entry in a `pages` array, or as a
+ * group's `root`, which Mintlify renders as the group's own landing page. Only
+ * `pages` was collected here, so giving a group a `root` reported that page as
+ * an orphan even though it is the most reachable page in the section.
+ */
function collectNavSlugs(node, out = new Set()) {
if (Array.isArray(node)) {
for (const item of node) collectNavSlugs(item, out);
@@ -62,7 +69,9 @@ function collectNavSlugs(node, out = new Set()) {
}
if (node && typeof node === 'object') {
for (const [key, value] of Object.entries(node)) {
- if (key === 'pages' && Array.isArray(value)) {
+ if (key === 'root' && typeof value === 'string') {
+ out.add(value);
+ } else if (key === 'pages' && Array.isArray(value)) {
for (const page of value) {
if (typeof page === 'string') out.add(page);
else collectNavSlugs(page, out);