POST /v1/render/videos
Starts a job that records a scripted demo of `source.url`. Narration (`say`) is synthesized first with `/v1/infer/audio/speech`; the director then places each beat's steps on its narration, plans cursor and camera, records segments in parallel on a virtual clock, composes at the output frame rate and encodes MP4 (H.264, BT.709), GIF, poster and captions. Artifacts appear as they finish (`video.artifact` events, poster first). Bills `render.video_s` per second of output.

Authentication

Bearer Token (b3os_sk_ API key or b3os_pat_ token) API Key (cookie: __Host-b3os_session)

Query Parameters

wait integer optional query
Seconds to wait for the job to finish before responding, from 0 to 60. If it finishes in time the response is `200` with the finished job; otherwise `202` with the job as it is, a `Location` to poll and `Retry-After`. `0` (the default) returns immediately.

Header Parameters

Idempotency-Key string optional header
A unique value (for example a UUID) that makes this request safe to retry. Scoped to your project for 24 hours: a retry with the same key, method, path and body returns the stored response with `Idempotent-Replayed: true`, and only to the same API key. Reusing the key for a different request returns `409 idempotency_key_conflict`.

Request Body required

application/json
source object REQUIRED
url string (uri) REQUIRED
Where the recording starts.
profile string
A vision profile ID or name to start signed in. Responses show the resolved `prof_` ID.
beats object[] REQUIRED
Array of:
id string
Optional ID; generated as `b1`, `b2`, … when omitted.
title string
say string
Narration for this beat. Omit for a silent beat.
do (object | object | object | object | object | object | object | object)[]
Array of:
One of:
Option 1
kind string REQUIRED
Enum: navigate
url string (uri) REQUIRED
wait_until string
Enum: load, domcontentloaded, networkidle
timeout_ms integer
Option 2
kind string REQUIRED
Enum: click
target unknown | unknown | unknown REQUIRED
How an action finds its element (the same rules as render timelines, `timeline.schema.json#/$defs/target`): candidates match `selector` (CSS), `text` (visible text, case-insensitive substring unless `exact`) or `role` plus accessible `name`, limited to `within`; the best-scored candidate wins, or the `nth` match when set. An element that stays outside the viewport after scrolling it into view fails with `target_not_found`.
Any of:
Option 1...
Option 2...
Option 3...
button string
Enum: left, right, middle
count integer
modifiers string[]
Modifier keys held during the action.
Array of: ...
Option 3
kind string REQUIRED
Enum: hover
target unknown | unknown | unknown REQUIRED
How an action finds its element (the same rules as render timelines, `timeline.schema.json#/$defs/target`): candidates match `selector` (CSS), `text` (visible text, case-insensitive substring unless `exact`) or `role` plus accessible `name`, limited to `within`; the best-scored candidate wins, or the `nth` match when set. An element that stays outside the viewport after scrolling it into view fails with `target_not_found`.
Any of:
Option 1...
Option 2...
Option 3...
Option 4
kind string REQUIRED
Enum: type
target unknown | unknown | unknown
How an action finds its element (the same rules as render timelines, `timeline.schema.json#/$defs/target`): candidates match `selector` (CSS), `text` (visible text, case-insensitive substring unless `exact`) or `role` plus accessible `name`, limited to `within`; the best-scored candidate wins, or the `nth` match when set. An element that stays outside the viewport after scrolling it into view fails with `target_not_found`.
Any of:
Option 1...
Option 2...
Option 3...
text string REQUIRED
interval_ms integer
Delay between characters on the video's clock.
Option 5
kind string REQUIRED
Enum: key
key string REQUIRED
modifiers string[]
Modifier keys held during the action.
Array of: ...
Option 6
kind string REQUIRED
Enum: scroll
target unknown | unknown | unknown
How an action finds its element (the same rules as render timelines, `timeline.schema.json#/$defs/target`): candidates match `selector` (CSS), `text` (visible text, case-insensitive substring unless `exact`) or `role` plus accessible `name`, limited to `within`; the best-scored candidate wins, or the `nth` match when set. An element that stays outside the viewport after scrolling it into view fails with `target_not_found`.
Any of:
Option 1...
Option 2...
Option 3...
to object
A position in CSS pixels.
x number REQUIRED
y number REQUIRED
by object
A position in CSS pixels.
x number REQUIRED
y number REQUIRED
duration_ms integer
Option 7
kind string REQUIRED
Enum: select
target unknown | unknown | unknown REQUIRED
How an action finds its element (the same rules as render timelines, `timeline.schema.json#/$defs/target`): candidates match `selector` (CSS), `text` (visible text, case-insensitive substring unless `exact`) or `role` plus accessible `name`, limited to `within`; the best-scored candidate wins, or the `nth` match when set. An element that stays outside the viewport after scrolling it into view fails with `target_not_found`.
Any of:
Option 1...
Option 2...
Option 3...
value string REQUIRED
Option 8
kind string REQUIRED
Enum: wait
ms integer
for object
Exactly one condition to wait for (the same as a timeline `wait` action's `for`).
selector string
An element matching this CSS selector is visible.
text string
This text is visible.
url string
The page URL contains this substring.
network_idle boolean
The network has been quiet.
Enum: true
timeout_ms integer
hold_ms integer
Extra time to stay on the beat after its narration and steps end.
voice object
model string REQUIRED
A speech model from `GET /v1/infer/models`, such as `kokoro-82m`.
voice string REQUIRED
A voice from `GET /v1/infer/audio/voices`.
speed number
captions object
burn_in string
`none`: sidecar files only. `static`: draw each cue. `karaoke`: also highlight the spoken word.
Enum: none, static, karaoke
sidecars string[]
Array of: string
max_chars_per_line integer
max_lines integer
max_chars_per_second number
style object
Style presets plus overrides, the same object as a timeline's `style` (restyle replaces exactly this). Field values inside `overrides` follow the compositor's style type (padding, corner radius, shadow, colors, gradients); strings are escaped before they reach any HTML or CSS.
background string
A preset ID from `GET /v1/render/presets`, such as `midnight`, `browser-dark` or `mac`.
frame string
A preset ID from `GET /v1/render/presets`, such as `midnight`, `browser-dark` or `mac`.
cursor string
A preset ID from `GET /v1/render/presets`, such as `midnight`, `browser-dark` or `mac`.
captions string
A preset ID from `GET /v1/render/presets`, such as `midnight`, `browser-dark` or `mac`.
click_effect string
A preset ID from `GET /v1/render/presets`, such as `midnight`, `browser-dark` or `mac`.
overrides object
Style fields that override the presets.
camera object
auto_zoom boolean
Zoom toward the element each step acts on.
max_zoom number
viewport object
Browser viewport in CSS pixels.
width integer REQUIRED
height integer REQUIRED
output object
width integer REQUIRED
height integer REQUIRED
fps integer REQUIRED
Enum: 24, 25, 30, 50, 60
formats string[]
Array of: string
seed integer
Seeds the page's randomness so a re-render is identical.
start_time string (date-time)
The page's `Date` at the start of the video; the clock then advances with the video.
recording object
Where the page's network traffic comes from. By default every video loads the site live and records that load (its capture segments replay the recording, so they all see the same content). With `reuse: true` the video replays the newest recording another video of this project made of the same source URL, steps, viewport, seed and start time within `max_age_s`, instead of loading the site again: faster, and the content is what the site served then. When no such recording exists the site is loaded and recorded as usual. `result.recording` says which happened. Recordings hold the page's responses; they are deleted 24 hours after they were made, or earlier with the last video that used them.
reuse boolean
max_age_s integer
The oldest recording `reuse` accepts, in seconds.
webhook object
Where to deliver this job's events. Deliveries are `POST`s of a `WebhookEvent` signed with `B3OS-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` using the endpoint's secret (`POST /v1/webhooks/secret`). Failed deliveries are retried with backoff for 24 hours. Delivery goes through the egress guard, so private and loopback addresses are refused.
url string (uri) REQUIRED
An `https://` URL.
events string[]
Event types to deliver. Defaults to the terminal events (`job.succeeded`, `job.failed`, `job.canceled`); `["*"]` delivers every event.
Array of: string
metadata object
Up to 16 string key-value pairs you attach to an object for your own use. Keys are 1 to 40 characters of `A-Z a-z 0-9 _ . : -`; values are at most 500 characters. B3OS never interprets them.

Responses

200 The video finished within `wait` seconds, or the event stream when `Accept` is `text/event-stream`.
application/json
id string REQUIRED
object string REQUIRED
Enum: video
status string REQUIRED
`queued` → `running` → one of the terminal statuses `succeeded`, `failed` or `canceled`. A job never leaves a terminal status.
Enum: queued, running, succeeded, failed, canceled
created_at string (date-time) REQUIRED
updated_at string (date-time) REQUIRED
started_at string (date-time) | null
finished_at string (date-time) | null
progress object
How far a job has got, in the job's own unit (pages, items, steps).
completed integer REQUIRED
Units finished, successfully or not.
failed integer
Units that failed (included in `completed`).
total integer | null
Units expected, when known.
unit string REQUIRED
What is counted, such as `pages`, `items` or `steps`.
result object
duration_ms integer REQUIRED
width integer REQUIRED
height integer REQUIRED
fps integer REQUIRED
artifacts object[] REQUIRED
Array of:
id string REQUIRED
object string REQUIRED
Enum: artifact
kind string REQUIRED
Enum: image, mp4, gif, poster, vtt, srt
content_type string REQUIRED
bytes integer
width integer
height integer
duration_ms integer
url string (uri)
Signed URL on `usercontent.b3os.org`, `https://usercontent.b3os.org/a/<id in host-safe form>/<name>?node=&exp=&sig=`: an Ed25519 signature by the node's key over the method, path and expiry (`renderGetArtifactContent`), no cookies. It works as soon as the artifact is reported, while the upload to storage is still finishing.
url_expires_at string (date-time)
created_at string (date-time)
expires_at string (date-time)
When the artifact is deleted.
recording object
reused boolean REQUIRED
True when the video replayed an earlier recording (`recording.reuse`) instead of loading the site.
recorded_at string (date-time)
When the replayed or new recording was made.
error object
An RFC 9457 problem. `code` is stable and listed in the errors catalog; `type` links to its entry. `detail` is human-readable and may change. Extension members appear only for the codes that document them; ignore members you do not recognize.
type string (uri) REQUIRED
`https://docs.beta.b3os.org/errors#<code>`.
title string REQUIRED
Short summary of the problem type.
status integer REQUIRED
The HTTP status code, repeated for convenience.
code string REQUIRED
Stable machine-readable error code from the errors catalog.
detail string REQUIRED
Explanation of this occurrence. Never contains secrets.
instance string
The request ID (`req_…`) of the request that failed. Include it when you contact support.
errors object[]
Validation problems, one per field (`invalid_request` and the other codes that list `errors`).
Array of:
pointer string REQUIRED
Where the problem is: a JSON pointer into the request body (`/url`, `/steps/0/uses`), or `#query/<name>`, `#path/<name>` or `#header/<name>` for parameters.
detail string REQUIRED
What is wrong there.
reason string
Finer-grained cause for codes that document one (`unauthorized`, `forbidden`, `egress_denied`, `upstream_unreachable`, …).
required_scope string
The scope that was missing (`forbidden` with `reason: scope`).
retry_after_s integer
Same value as the `Retry-After` header, for clients that only read the body.
limit object
The limit an error refers to. IDs and current values are listed by `GET /v1/limits`.
id string REQUIRED
Limit ID, such as `vision.scrape.rpm` or `sandboxes.concurrent`.
limit number REQUIRED
The limit's value.
unit string REQUIRED
What the limit counts, such as `requests`, `sandboxes`, `vcpu` or `MiB`.
window_s integer
Window length in seconds for rate limits and quotas.
current number
Current usage against the limit, when known.
current_status string
The object's current status, for state conflicts such as `sandbox_not_running`.
upstream_status integer
The HTTP status the upstream returned (`upstream_error`, `action_output_invalid`).
step string
The failed step, as a step ID (routines), an index (`act`) or a path such as `beats/2/do/0` (render).
pack_error object
The error a pack's error mapping chose (`upstream_error` from pack actions).
code string REQUIRED
message string REQUIRED
retryable boolean REQUIRED
timing object
Server-side phase durations in milliseconds with microsecond resolution: the same phases as the `Server-Timing` header, keyed `<phase>_ms`. `total_ms` is always present. Phase names are product-specific and documented per operation. Example values in this reference are illustrative, not measurements.
total_ms number REQUIRED
expires_at string (date-time) | null
When the job's results are deleted (retention); `null` while the job is not terminal.
metadata object
Up to 16 string key-value pairs you attach to an object for your own use. Keys are 1 to 40 characters of `A-Z a-z 0-9 _ . : -`; values are at most 500 characters. B3OS never interprets them.
webhook object
Where to deliver this job's events. Deliveries are `POST`s of a `WebhookEvent` signed with `B3OS-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` using the endpoint's secret (`POST /v1/webhooks/secret`). Failed deliveries are retried with backoff for 24 hours. Delivery goes through the egress guard, so private and loopback addresses are refused.
url string (uri) REQUIRED
An `https://` URL.
events string[]
Event types to deliver. Defaults to the terminal events (`job.succeeded`, `job.failed`, `job.canceled`); `["*"]` delivers every event.
Array of: string
source object REQUIRED
url string (uri) REQUIRED
Where the recording starts.
profile string
A vision profile ID or name to start signed in. Responses show the resolved `prof_` ID.
restyled_from string | null REQUIRED
The source video when this video is a restyle.
202 The video job was accepted and is still running.
400 The request failed validation (`invalid_request`, `invalid_url`).
401 Missing, malformed, expired or revoked credentials (`unauthorized`).
402 The organization has no credits left, or the key reached its spend cap.
403 The egress guard refused the source URL (`egress_denied`).
409 The object's state does not allow this operation, or the idempotency key conflicts.
413 The request body exceeds this operation's limit.
415 The Content-Type is not accepted by this operation.
422 The video request failed validation (`script_invalid`, `style_invalid`, `limit_exceeded`).
429 A rate, concurrency or quota limit applies (`rate_limited`, `concurrency_limited`, `quota_exceeded`).
500 Something failed inside B3OS (`internal`). Retry with backoff.
501 `profile` was given but this node cannot load browser profiles into render yet (`not_implemented`): render reads profiles through vision's profile API, which is not on the node. Capture without `profile`.
503 The node or the product is not available right now (`node_unavailable`, `product_unavailable`, `capacity_unavailable`).
curl -X POST 'https://beta-api.b3os.org/v1/render/videos' \  -H 'Authorization: Bearer YOUR_API_TOKEN' \  -H 'Content-Type: application/json' \  -d '{  "source": {    "url": "https://app.example.com",    "profile": "acme-staging"  },  "beats": [    {      "say": "Open the billing page.",      "do": [        {          "kind": "click",          "target": {            "role": "link",            "name": "Billing"          }        }      ]    },    {      "say": "Switch to annual and watch the price update.",      "do": [        {          "kind": "click",          "target": {            "text": "Annual",            "exact": true          }        }      ]    }  ],  "voice": {    "model": "kokoro-82m",    "voice": "af_heart"  },  "captions": {    "burn_in": "karaoke",    "sidecars": [      "vtt"    ]  },  "style": {    "background": "midnight",    "frame": "browser-dark",    "cursor": "mac"  },  "formats": [    "mp4",    "gif",    "poster"  ]}'
const response = await fetch('https://beta-api.b3os.org/v1/render/videos', {  method: 'POST',  headers: {      "Authorization": "Bearer YOUR_API_TOKEN",      "Content-Type": "application/json"  },  body: JSON.stringify({    "source": {      "url": "https://app.example.com",      "profile": "acme-staging"    },    "beats": [      {        "say": "Open the billing page.",        "do": [          {            "kind": "click",            "target": {              "role": "link",              "name": "Billing"            }          }        ]      },      {        "say": "Switch to annual and watch the price update.",        "do": [          {            "kind": "click",            "target": {              "text": "Annual",              "exact": true            }          }        ]      }    ],    "voice": {      "model": "kokoro-82m",      "voice": "af_heart"    },    "captions": {      "burn_in": "karaoke",      "sidecars": [        "vtt"      ]    },    "style": {      "background": "midnight",      "frame": "browser-dark",      "cursor": "mac"    },    "formats": [      "mp4",      "gif",      "poster"    ]  })});const data = await response.json();console.log(data);
import requestsheaders = {    'Authorization': 'Bearer YOUR_API_TOKEN'}response = requests.post('https://beta-api.b3os.org/v1/render/videos', headers=headers, json={  "source": {    "url": "https://app.example.com",    "profile": "acme-staging"  },  "beats": [    {      "say": "Open the billing page.",      "do": [        {          "kind": "click",          "target": {            "role": "link",            "name": "Billing"          }        }      ]    },    {      "say": "Switch to annual and watch the price update.",      "do": [        {          "kind": "click",          "target": {            "text": "Annual",            "exact": true          }        }      ]    }  ],  "voice": {    "model": "kokoro-82m",    "voice": "af_heart"  },  "captions": {    "burn_in": "karaoke",    "sidecars": [      "vtt"    ]  },  "style": {    "background": "midnight",    "frame": "browser-dark",    "cursor": "mac"  },  "formats": [    "mp4",    "gif",    "poster"  ]})print(response.json())
200 Response
{  "id": "vid_01m5t4c5ep9bdfhtx7nk2k3vq7",  "object": "video",  "status": "succeeded",  "source": {    "url": "https://app.example.com",    "profile": "prof_01m5t3v4507bhcb5jqrgzfvfsk"  },  "restyled_from": null,  "created_at": "2026-10-25T18:50:00.000Z",  "updated_at": "2026-10-25T18:50:06.420Z",  "started_at": "2026-10-25T18:50:00.010Z",  "finished_at": "2026-10-25T18:50:06.420Z",  "progress": {    "completed": 6,    "total": 6,    "unit": "phases"  },  "result": {    "duration_ms": 9480,    "width": 1920,    "height": 1080,    "fps": 60,    "artifacts": [      {        "id": "art_01m5t5mh213we8gzwkr08x6ef9",        "object": "artifact",        "kind": "mp4",        "content_type": "video/mp4",        "bytes": 2210388,        "width": 1920,        "height": 1080,        "duration_ms": 9480,        "url": "https://usercontent.b3os.org/a/art-01m5t5mh213we8gzwkr08x6ef9/video.mp4?node=n1&exp=1793040606&sig=TLNvLS_42-1c-rA5z8mLZn7wLwstijGhGIFm3VyqZ-qCNuNOJ9QZ97k1ZGQGRNB_qt3AueFtNg1WeoX0wjVeCA",        "url_expires_at": "2026-10-26T18:50:06.000Z",        "created_at": "2026-10-25T18:50:06.400Z",        "expires_at": "2026-11-01T18:50:06.400Z"      }    ]  },  "timing": {    "voice_ms": 210.3,    "plan_ms": 6.1,    "capture_ms": 3302.8,    "compose_ms": 1210.4,    "encode_ms": 1642,    "total_ms": 6410.2  },  "expires_at": "2026-11-01T18:50:06.420Z",  "metadata": {}}
Ask a question... ⌘I