Record a video
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.
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/jsonsource
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:
navigateurl
string (uri)
REQUIRED
wait_until
string
Enum:
load, domcontentloaded, networkidletimeout_ms
integer
Option 2
kind
string
REQUIRED
Enum:
clicktarget
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, middlecount
integer
modifiers
string[]
Modifier keys held during the action.
Array of:
...
Option 3
kind
string
REQUIRED
Enum:
hovertarget
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:
typetarget
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:
keykey
string
REQUIRED
modifiers
string[]
Modifier keys held during the action.
Array of:
...
Option 6
kind
string
REQUIRED
Enum:
scrolltarget
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:
selecttarget
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:
waitms
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:
truetimeout_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, karaokesidecars
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, 60formats
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/jsonid
string
REQUIRED
object
string
REQUIRED
Enum:
videostatus
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, canceledcreated_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:
artifactkind
string
REQUIRED
Enum:
image, mp4, gif, poster, vtt, srtcontent_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": {}}
API Playground
Try this endpoint
POST
/v1/render/videos