Render API
POST a YAML, poll for the result, get an MP4 URL. The same flow the dashboard uses internally — and the same code path the local CLI runs.
Base URL
https://your-domain.com/api/v1For local dev: http://localhost:3000/api/v1.
Authentication
Every request needs an API key. Create one in your dashboard; it's shown once, stored hashed. Send it as a Bearer token:
Authorization: Bearer cb_live_xxxxxxxxxxxxxThe header form x-api-key: cb_live_… is also accepted for tools that don't play nicely with Authorization.
Endpoints
/v1/rendersSubmit a demo for rendering. Returns immediately with a job ID.
Three body shapes are accepted:
application/json—{ "yaml": "title: ..." }application/json—{ "projectId": "pj_..." }text/yaml— raw YAML body
Example
curl -X POST https://your-domain.com/api/v1/renders \
-H "Authorization: Bearer cb_live_xxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"projectId": "pj_a1b2c3d4"}'
# 202 Accepted
{
"id": "rj_a1b2c3d4e5f6",
"status": "pending"
}/v1/renders/{id}Poll a job's status. Poll every 2-3s; jobs typically take 20-40s.
Example
curl https://your-domain.com/api/v1/renders/rj_a1b2c3d4e5f6 \
-H "Authorization: Bearer cb_live_xxxxxxxxxxxxx"
# 200 OK — pending or running
{ "id": "rj_…", "status": "running",
"videoUrl": null, "manifestUrl": null,
"steps": null, "error": null,
"durationMs": null, "createdAt": "2026-01-15T12:00:00Z" }
# 200 OK — done
{ "id": "rj_…", "status": "succeeded",
"videoUrl": "https://media.your-domain.com/render-…-mp4",
"manifestUrl": "https://media.your-domain.com/manifest-…-json",
"durationMs": 22400,
"steps": [
{ "index": 0, "type": "goto", "status": "ok", "durationMs": 1240 },
{ "index": 1, "type": "click", "status": "ok",
"resolvedSelector": "getByRole(button, name=\"Sign in\")",
"durationMs": 380 }
],
"createdAt": "2026-01-15T12:00:00Z" }
# 200 OK — succeeded with a partial step failure
{ "id": "rj_…", "status": "succeeded",
"videoUrl": "https://…",
"steps": [
{ "index": 0, "type": "goto", "status": "ok" },
{ "index": 1, "type": "click", "status": "failed",
"error": "Could not resolve target { role: \"button\", name: \"Buy\" } …" }
], … }
# 200 OK — failed (CLI-level, no manifest)
{ "id": "rj_…", "status": "failed",
"error": "CLI exited with code 1: …",
"steps": null }videoUrl and manifestUrl are presigned URLs valid for S3_URL_TTL seconds (24h default). steps is the compact per-step summary — index, type, ok/skipped/failed, optional error, optional resolved selector, optional recovery path. The full events.json (cursor paths, anchor screenshots, etc.) lives at manifestUrl.
Status codes
| Code | Meaning |
|---|---|
| 202 | Render queued. Poll the job ID. |
| 200 | Job state retrieved. |
| 400 | Missing or malformed body. Check yaml / projectId. |
| 401 | Missing or invalid API key. |
| 402 | No active subscription. Upgrade. |
| 404 | Project not found, or doesn't belong to the API key's account. |
| 429 | Concurrency limit (3 in flight) or yearly safety net (5,000) hit. |
| 500 | Server error. The job row stays in pending; retry safe. |
Common patterns
Render-and-wait helper
Most CI integrations want a single blocking call. This shell pattern submits and polls until done:
#!/usr/bin/env bash
set -euo pipefail
KEY=${CAPTUREBEAM_KEY:?}
JOB_ID=$(curl -sS -X POST https://your-domain.com/api/v1/renders \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"projectId":"pj_a1b2c3d4"}' | jq -r .id)
while :; do
RES=$(curl -sS https://your-domain.com/api/v1/renders/$JOB_ID \
-H "Authorization: Bearer $KEY")
STATUS=$(echo "$RES" | jq -r .status)
case "$STATUS" in
succeeded) echo "$RES" | jq -r .videoUrl; exit 0 ;;
failed) echo "$RES" | jq -r .error >&2; exit 1 ;;
*) sleep 3 ;;
esac
doneRate limits & soft caps
- Concurrency: max 3 renders in flight per account. The 4th returns 429.
- Yearly safety net: 5,000 successful renders per rolling 12 months. Soft anti-abuse cap, not advertised. Email support to lift.
- No request-rate limit on the polling endpoints.
Webhooks
Webhooks for render completion are on the roadmap. For now, poll the status endpoint. The job row is durable — even if your client reconnects, you can resume polling with the original ID.