REST API reference
Upload footage, request an edit, and deliver a browser view or render handoff.
Scrambo turns uploaded footage into a revisioned edit. Your integration only needs to manage a session, a few returned IDs, and the output your product needs.
Before you start
| Setting | Value |
|---|---|
| Base URL | https://api.scrambo.dev |
| Authentication | Authorization: Bearer <token> · Request API access |
| JSON requests | Content-Type: application/json |
V2 does not use the X-Scrambo-SDK-Version header.
Keep the bearer credential on your server. Do not embed it in browser code.
Each authenticated identity can have one active project. Creating a session closes that identity's previous active session. Give independently operating users or integrations distinct credential identities.
Request lifecycle
One session. IDs connect each request to the next.
- 01
Start
Open a workspace for one video.
- 1Create sessionPOST
/v2/sessionsKeep sessionId
- 02
Add media
Register the file, then send its bytes.
- 2Declare assetPOST
/v2/sessions/{sessionId}/assetsKeep assetId, SHA-256, size
- 3Upload bytesPUT
/v2/sessions/{sessionId}/assets/{assetId}/contentKeep upload receipt
- 03
Make the edit
Submit direction and follow the work.
- 4Submit turnPOST
/v2/sessions/{sessionId}/turnsKeep requestId, operationId
- 5Poll progressGET
/v2/operations/{operationId}?after={cursor}Keep status, latest cursor
- 04
Deliver
Choose a browser view or render handoff.
- 6Create snapshotPOST
/v2/sessions/{sessionId}/view-edit-snapshotsKeep URL, operation, expiry
- 7Create handoffPOST
/v2/sessions/{sessionId}/render-handoffsKeep operationId
- 8Download handoffGET
/v2/sessions/{sessionId}/render-handoffs/{handoffId}Keep manifest
- 9Close sessionPOST
/v2/sessions/{sessionId}/closeKeep terminal state
The first five steps get you to a working edit. From there, create a browser snapshot for review, or create and download a render handoff for your own renderer. Close the session when the user is done.
Your first edit
The TypeScript tabs use Node.js native fetch and built-in modules; no SDK is
needed. Run them on your server, with SCRAMBO_API_TOKEN set in its environment.
The examples in steps 1–4 share helpers and variables: put them in one
TypeScript module and run them in order. Never use a NEXT_PUBLIC_ variable for
the token or include it in client code, URLs, or logs.
1. Create a session and upload media
Create the session and keep its sessionId. For every file, declare its name,
byte size, MIME type, and SHA-256 digest; keep the returned assetId, then
upload the exact bytes.
ExampleCreate a session and upload a file
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
const apiUrl = "https://api.scrambo.dev";
const token = process.env.SCRAMBO_API_TOKEN;
if (!token) throw new Error("Set SCRAMBO_API_TOKEN on your server.");
async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers);
headers.set("Authorization", `Bearer ${token}`);
if (init.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const response = await fetch(`${apiUrl}${path}`, {
...init,
headers,
signal: AbortSignal.timeout(60_000),
});
if (!response.ok) {
// Keep credentials and response internals out of logs.
throw new Error(`Scrambo request failed (HTTP ${response.status}).`);
}
return await response.json() as T;
}
const { sessionId } = await api<{ sessionId: string }>("/v2/sessions", {
method: "POST",
body: JSON.stringify({ project: "first-edit", canvas: [1080, 1920] }),
});
// Read this small demo file once: hash and upload the same bytes.
// For large videos, stream the file instead of loading it all into memory.
const videoPath = "./interview.mp4";
const bytes = await readFile(videoPath);
const { assetId } = await api<{ assetId: string }>(
`/v2/sessions/${sessionId}/assets`,
{
method: "POST",
body: JSON.stringify({
name: basename(videoPath),
size: bytes.byteLength,
mimeType: "video/mp4",
sha256: createHash("sha256").update(bytes).digest("hex"),
}),
},
);
await api(`/v2/sessions/${sessionId}/assets/${assetId}/content`, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: new Uint8Array(bytes),
});2. Submit editing turns
Most agent instructions go to the /turns endpoint. A dependable first edit
uses three turns in order:
| Goal | Agent | What to send |
|---|---|---|
| Understand the footage | source.work | What to find or transcribe |
| Plan the story | planner.compile | The desired length, structure, and emphasis |
| Build the edit | timeline.author | Omit message to build the current plan |
Wait for each turn to succeed before submitting the next. Keep the returned
operationId; a successful Author result also gives you the current editId.
ExampleSubmit the three-turn editing sequence
import { randomUUID } from "node:crypto";
import { setTimeout as delay } from "node:timers/promises";
type Operation = {
status: "queued" | "running" | "succeeded" | "failed" | "cancelled";
cursor: number;
result?: { type: string; editId?: string };
};
async function waitForOperation(operationId: string): Promise<Operation> {
let cursor = 0;
const deadline = Date.now() + 15 * 60_000;
while (Date.now() < deadline) {
const operation = await api<Operation>(
`/v2/operations/${operationId}?after=${cursor}`,
);
cursor = operation.cursor;
if (operation.status === "succeeded") return operation;
if (operation.status === "failed" || operation.status === "cancelled") {
throw new Error(`Scrambo operation ${operation.status}.`);
}
await delay(2_000);
}
throw new Error("Operation timed out; retain its ID to resume polling.");
}
// Generate each ID once. Reuse it only when retrying the same body.
const runId = randomUUID();
async function submitTurn(body: Record<string, unknown>): Promise<string> {
const { operationId } = await api<{ operationId: string }>(
`/v2/sessions/${sessionId}/turns`,
{ method: "POST", body: JSON.stringify(body) },
);
return operationId;
}
// 1. Understand the footage.
const sourceOperationId = await submitTurn({
requestId: `${runId}:source`,
mode: "edit",
agent: "source.work",
message: "Transcribe the dialogue and identify the strongest moments.",
tools: ["transcribe"],
});
await waitForOperation(sourceOperationId);
// 2. Plan the story.
const planOperationId = await submitTurn({
requestId: `${runId}:plan`,
mode: "edit",
agent: "planner.compile",
message: "Plan an energetic 30-second story. Open with the strongest hook.",
});
await waitForOperation(planOperationId);
// 3. Build the planned edit.
const operationId = await submitTurn({
requestId: `${runId}:author`,
mode: "edit",
agent: "timeline.author",
});
const completed = await waitForOperation(operationId);
if (completed.result?.type !== "edit" || !completed.result.editId) {
throw new Error("Author did not return an edit.");
}
const editId = completed.result.editId;3. Poll the operation
Poll with the latest cursor until status is succeeded, failed, or
cancelled. Send the returned cursor as the next after value so you only
receive new progress events.
ExamplePoll for progress
// Read one progress page for the Author operation from step 2.
// On subsequent polls, use the returned cursor as the next `after` value.
const progress = await api<Operation>(
`/v2/operations/${operationId}?after=0`,
);
const nextCursor = progress.cursor;
// Use progress.status to update your UI. Step 2's helper polls to completion.4. Deliver the result
Create a snapshot when a user needs a browser review. Create a render handoff when another renderer needs the edit and verified asset manifest.
ExampleCreate a browser snapshot, then close the session
const snapshot = await api<{ viewUrl: string; operationId: string }>(
`/v2/sessions/${sessionId}/view-edit-snapshots`,
{ method: "POST", body: JSON.stringify({ editId }) },
);
// Return snapshot.viewUrl to the authorized user for browser review.
// Open it before polling: the browser connection completes hydration.
// Treat the URL as private; do not log it.
// Run this later, when the user has finished reviewing the edit.
await api(`/v2/sessions/${sessionId}/close`, {
method: "POST",
body: JSON.stringify({}),
});Concepts in plain English
| Resource | Think of it as | Keep |
|---|---|---|
| Session | One user's video workspace | sessionId, currentEditId |
| Asset | One verified media file | assetId, SHA-256, byte size |
| Turn | One question or editing instruction | Your unique requestId |
| Operation | The work running in the background | operationId, latest cursor |
| Snapshot | A temporary browser view of one edit | viewUrl, expiry |
| Render handoff | The edit and asset manifest for a renderer | handoffId |
Endpoint reference
| Method | Path | Purpose |
|---|---|---|
POST | /v2/sessions | Create an empty stateful editing session. |
GET | /v2/sessions/{sessionId} | Read session state, assets, and currentEditId. |
POST | /v2/sessions/{sessionId}/forks | Start a new session from the accepted edit. |
PUT | /v2/sessions/{sessionId}/provider-credentials/{provider} | Attach or replace a transient agent key using Scrambo-Provider-Key. |
POST | /v2/sessions/{sessionId}/assets | Declare an asset's name, size, MIME type, and SHA-256 digest. |
PUT | /v2/sessions/{sessionId}/assets/{assetId}/content | Upload the declared asset as raw bytes. |
POST | /v2/sessions/{sessionId}/turns | Submit an idempotent ask or edit turn. |
POST | /v2/sessions/{sessionId}/narrative-repairs | Check and repair the current narration plan. |
GET | /v2/operations/{operationId}?after={cursor} | Poll status and receive events newer than cursor. |
POST | /v2/sessions/{sessionId}/view-edit-snapshots | Create a detached browser view of an edit. |
POST | /v2/sessions/{sessionId}/render-handoffs | Start a renderer-neutral handoff for an edit. |
GET | /v2/sessions/{sessionId}/render-handoffs/{handoffId} | Download an authenticated handoff manifest. |
POST | /v2/sessions/{sessionId}/close | Cancel active work and close the private session. |
JSON request objects are closed: unknown fields are rejected.
Sessions and assets
POST /v2/sessions accepts:
| Field | Required | Meaning |
|---|---|---|
project | Yes | Project name, 1–120 characters. |
canvas | No | [width, height]; each value must be a positive integer no larger than 16384. |
provider | No | Object with name and optional model and thinking; omit it to use service defaults. |
Choose the agent provider when creating the session. For example:
// Uses the api() helper from step 1.
const providerSession = await api<{ sessionId: string }>("/v2/sessions", {
method: "POST",
body: JSON.stringify({
project: "first-edit",
canvas: [1080, 1920],
provider: {
name: "codex",
thinking: "high",
},
}),
});
// Use providerSession.sessionId for subsequent requests.
// Creating it closes the identity's previous active session.The choice sets the default for agent turns. Individual ask and edit turns can select a different provider, model, or thinking level. Available providers and model settings depend on the deployment.
To upload a file, first declare it with name, size, sha256, and optional
mimeType. Then PUT the raw bytes to the returned asset URL. An asset is only
available to agents after its byte count and digest have been verified.
Once an edit has been accepted, send {"project":"new-branch"} to
POST /v2/sessions/{sessionId}/forks to continue in a new session. Optionally
include "provider":{"name":"codex","thinking":"medium"}
and "canvas":[1080,1920]. Omitted canvas and provider settings inherit from
the source; a new provider uses its own model and thinking defaults. A changed
canvas is the target for the branch's next Reframe or Author edit; its inherited
currentEditId still replays at the original canvas. The response contains the
new sessionId and currentEditId. The source session becomes a closed branch.
Bring your own agent key
Supply Scrambo-Provider-Key once when creating the session. Use codex for
an OpenAI API key or claude for an Anthropic API key. Keep your Scrambo
credential in Authorization. Keys must contain 16–4096 characters without
whitespace; invalid values return 400 invalid_input before changing a session:
POST /v2/sessions
Authorization: Bearer <scrambo-credential>
Scrambo-Provider-Key: <openai-api-key>
Content-Type: application/json
{"project":"customer-edit","provider":{"name":"codex","thinking":"high"}}The key is a sensitive header, never a JSON field or URL parameter. Send it over HTTPS and exclude it from application and proxy logs. Subsequent turns, uploads, and polls need only the Scrambo credential. Keys cover agent model calls; generation, transcription, masking, and other services retain their separate billing. Omitting the header at creation uses service-funded agent credentials.
Session responses include credentialMode (customer or server) and
credentialProviders (the names of providers with attached keys). Attachment
does not validate provider access; the first agent call does. Raw keys are
never returned or included in saved session artifacts. Scrambo handles the
plaintext key while executing provider calls.
Keys are transient: closing, expiry, or terminal failure discards them. Server
restarts also lose them. Customer-mode sessions then reject new agent work with
409 provider_credential_required; they never fall back to service-funded keys.
Reattach or replace a key while the session is idle:
PUT /v2/sessions/{sessionId}/provider-credentials/codex
Authorization: Bearer <scrambo-credential>
Scrambo-Provider-Key: <replacement-openai-api-key>This endpoint needs no body and returns the updated session. It accepts
codex or claude, and switches all agent work to customer credentials.
Queued/running operations return 409 server_busy; closed sessions reject
attachment. Attach a claude key before a turn or narrative repair/judge
override selects that provider. Model/thinking changes use the existing key.
The key header is rejected on turns and other unrelated endpoints.
Forks inherit available keys within the same owner before closing the parent.
An explicit key header on POST /v2/sessions/{sessionId}/forks replaces only
the fork's selected-provider key. Changing provider requires a matching key
already attached to the source or supplied on the fork request:
POST /v2/sessions/{sessionId}/forks
Authorization: Bearer <scrambo-credential>
Scrambo-Provider-Key: <anthropic-api-key>
Content-Type: application/json
{"project":"claude-alternative","provider":{"name":"claude","thinking":"high"}}Closing or replacing the parent's key does not change the fork's binding. Revoking the key at its provider affects both. Forking an already-closed branch, or after restart, requires a fresh key. Fork requests rejected by the API leave the active session intact. Cross-owner forks are not supported.
Turns
Ask and single-agent edit turns use POST /v2/sessions/{sessionId}/turns.
There are two request modes. Narrative repair has its own endpoint below.
An ask turn performs read-only analysis and returns text:
{
"requestId": "ask-1",
"mode": "ask",
"message": "Which moments best support a 30-second customer story?"
}An edit turn runs one allowlisted specialist:
{
"requestId": "captions-1",
"mode": "edit",
"agent": "timeline.captions",
"baseEditId": "latest-edit-id",
"message": "Add bold, readable captions timed to the dialogue."
}Add provider to an ask or edit turn to select its agent model for that
operation. For example, this Planner Compile call requests medium thinking:
{
"requestId": "plan-override-1",
"mode": "edit",
"agent": "planner.compile",
"message": "Plan a 30-second story around the strongest claim.",
"provider": {"thinking": "medium"}
}The same field works with timeline.captions and every other turn agent.
Omitted fields inherit the session settings. To change providers, include
name, such as
{"name":"claude","thinking":"medium"}.
When name changes, omitted model and thinking values use the new
provider's defaults. The choice lasts for one operation; later turns use
the session defaults. Use a new requestId when changing these settings.
Customer-key sessions need an attached key for the selected provider; see
Bring your own agent key.
| Agent | What it does | Message and revision rules |
|---|---|---|
source.work | Understands uploaded media and publishes the current source brief. | Requires message; never accepts baseEditId. |
source.generate | Generates media and publishes a source brief over the results. | Requires message and at least one of img2video or voiceover; never accepts baseEditId. |
planner.compile | Turns the current source brief into a grounded plan. | Requires message; never accepts baseEditId. |
timeline.author | Creates the structural edit. | Omit message and baseEditId to build the current plan as a new root; use message for direct authoring. |
timeline.graphics | Adds shapes, panels, backings, and accents. | Requires message and the latest baseEditId. |
timeline.sound | Adds music, sound effects, balance, and ducking. | Requires message and the latest baseEditId. |
timeline.titles | Adds titles, cards, lower thirds, and typography. | Requires message and the latest baseEditId. |
timeline.captions | Adds transcript-aligned captions. | Requires message and the latest baseEditId. |
timeline.reframe | Reframes an accepted edit while preserving cuts and audio. | Requires message and the latest baseEditId; accepts no tools. Use before visual overlays. |
Common edit-turn fields:
| Field | Meaning |
|---|---|
requestId | Required idempotency key, 1–200 characters, scoped to the session. |
mode | edit. |
agent | One agent ID from the table above. |
message | Natural-language instruction, up to 20,000 characters. Only plan-backed Author may omit it. |
baseEditId | Revision the timeline specialist must extend. See Revision rules. |
provider | Optional agent settings: name, model, thinking. Also accepted by ask turns. This is separate from tool provider settings in toolConfig. |
tools | Up to four explicitly granted capabilities. Grants apply only to this turn. |
toolConfig | Per-tool configuration. Every configured tool must also appear in tools. |
source.generate additionally accepts budgetUsd (default 5.0), maxCalls
(default 4), and name (1–80 characters). Requests over the available
generation limit fail with generation_limit_exceeded.
For example:
{
"requestId": "generate-tour-1",
"mode": "edit",
"agent": "source.generate",
"message": "Create a narrated 16:9 property tour from the uploaded photos.",
"tools": ["img2video", "voiceover"],
"toolConfig": {
"img2video": {
"resolution": "720p",
"duration": 6,
"aspectRatio": "16:9"
}
},
"budgetUsd": 5,
"maxCalls": 10,
"name": "listing-tour-assets"
}Reframe an accepted edit
Use timeline.reframe after Author and before adding titles, captions, or
other visual overlays. Send the current editId as baseEditId. The agent
keeps source windows and audio in place while adapting the picture to a
narrower canvas. A fixed full-height crop is the default; request adaptive
widening if padding is acceptable. It accepts no tool grants.
{
"requestId": "reframe-1",
"mode": "edit",
"agent": "timeline.reframe",
"baseEditId": "current-edit-id",
"message": "Reframe the speaker and product for a vertical canvas. Preserve every cut and the audio."
}Check and repair a narration plan
After planner.compile, use
POST /v2/sessions/{sessionId}/narrative-repairs to check a straight-cut,
normal-speed narration plan before Author builds the timeline. It updates
the current plan, not the edit chain.
{
"requestId": "repair-story-1",
"acceptance": {
"coherence": [
{"id": "through-line", "text": "Does the narration have a clear through-line?"}
],
"transitions": [
{"id": "joins", "text": "Does each adjacent join sound intentional?"}
],
"duration": {"minSeconds": 25, "maxSeconds": 30},
"noMidwordCuts": {"enabled": true}
},
"limits": {"maxRepairs": 3},
"repairProvider": {"thinking": "medium"},
"judgeProvider": {"thinking": "medium"},
"tools": ["audio_boundaries"]
}The request needs a current grounded plan. Both coherence and
transitions need at least one question. audio_boundaries is optional.
Poll the returned operation, then check result.passed before authoring.
When no candidate passes every check, the operation may still select an
improved plan; result.acceptedPlanId identifies the selected plan.
repairProvider configures the plan-writing agent; judgeProvider
configures the independent semantic judge. Each accepts name, model,
and thinking. On Codex sessions, omitted roles use service defaults.
Other providers use their session settings.
Within a role override, omitted fields inherit that role's settings;
changing name uses the new provider's defaults. Poll the operation's
result.models to see the effective settings for both roles.
Tool grants
Tools are opt-in capabilities, not separate HTTP endpoints. The specialist decides whether and how to use the capabilities granted for that turn.
| Tool | Allowed agents | Configuration |
|---|---|---|
transcribe | source.work, planner.compile, timeline.author | provider, model, language, diarize; word timestamps are always enabled. |
detect_events | source.work, planner.compile, timeline.author | sampleDensity: sparse or dense; maxFrames: 1–2400. |
detect_beats | source.work | No configuration. |
masking | source.work, source.generate, planner.compile, timeline.author, timeline.graphics, timeline.sound, timeline.titles, timeline.captions | No caller configuration. |
audio_boundaries | planner.compile, narrative repair | Optional searchRadius: 0.1–3.0 seconds. |
img2video | source.generate | Generation settings such as vendor, resolution, duration, and aspectRatio. |
voiceover | source.generate | Generation settings such as vendor and voice. |
Operations and results
Turns, narrative repairs, browser snapshots, and render handoffs are asynchronous. A submission
returns 202 with an operationId and its current status (normally queued or
running; an idempotent replay can already be terminal). Poll:
GET /v2/operations/{operationId}?after=0The response contains:
| Field | Meaning |
|---|---|
status | queued, running, succeeded, failed, or cancelled. |
cursor | Latest event cursor. Send it as the next after value. |
events | Progress events newer than after. |
result | Present after success. Its type selects the result shape. |
error | Present after failure or cancellation. |
Result type | Important fields |
|---|---|
answer | answer |
source | agent, artifact, editId |
plan | agent, artifact, editId |
edit | agent, editId, previousEditId, artifact |
narrative_repair | passed, acceptedPlanId, repairs, stopReason, models |
view_edit_snapshot | editId |
render_handoff | handoffId, editId |
Artifacts include name, kind, executionId, fingerprint, result,
llmInvoked, and cached.
Every error has a stable code, a safe message, and retryable. Use
retryable rather than guessing from the message. Synchronous HTTP errors use
the same envelope:
{
"error": {
"code": "edit_conflict",
"message": "baseEditId is stale",
"retryable": false
}
}Idempotency and revision rules
requestId is an idempotency key scoped to one session. Replaying the same ID
with the same canonical body returns the original operation. Reusing it for a
different body returns 409 idempotency_conflict.
Timeline revisions form one linear chain:
- A direct first
timeline.authorturn omitsbaseEditId. - Plan-backed
timeline.authoromits bothmessageandbaseEditId; it creates a new root revision from the current plan. - Every later timeline turn, including
timeline.reframe, sends the latest successfuleditIdasbaseEditId. - A stale or missing base revision returns
409 edit_conflict. source.work,source.generate,planner.compile, and narrative repair do not advance the edit chain. They do not acceptbaseEditId.
Read currentEditId from GET /v2/sessions/{sessionId} when recovering local
client state.
Browser snapshots and render handoffs
A browser snapshot is a detached view of one revision. Pass an optional
editId; omitting it selects the current revision. Snapshot creation returns a
viewUrl immediately. Open it before waiting for the operation because the
browser connection completes hydration. Later agent turns do not change the
snapshot, and manual changes in that browser are not durable API revisions.
A render handoff is an authenticated, renderer-neutral manifest for a selected revision:
{
"requestId": "render-handoff-1",
"editId": "edit-id-to-render"
}Submit it to /v2/sessions/{sessionId}/render-handoffs, poll the returned
operation, then download
/v2/sessions/{sessionId}/render-handoffs/{handoffId}. Uploaded media is
identified by asset ID, SHA-256 digest, byte size, and MIME type so an external
renderer can resolve and verify its local copy.
