ScramboREST API

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

SettingValue
Base URLhttps://api.scrambo.dev
AuthenticationAuthorization: Bearer <token> · Request API access
JSON requestsContent-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.

  1. 01

    Start

    Open a workspace for one video.

    1. 1Create session
      POST/v2/sessions

      Keep sessionId

  2. 02

    Add media

    Register the file, then send its bytes.

    1. 2Declare asset
      POST/v2/sessions/{sessionId}/assets

      Keep assetId, SHA-256, size

    2. 3Upload bytes
      PUT/v2/sessions/{sessionId}/assets/{assetId}/content

      Keep upload receipt

  3. 03

    Make the edit

    Submit direction and follow the work.

    1. 4Submit turn
      POST/v2/sessions/{sessionId}/turns

      Keep requestId, operationId

    2. 5Poll progress
      GET/v2/operations/{operationId}?after={cursor}

      Keep status, latest cursor

  4. 04

    Deliver

    Choose a browser view or render handoff.

    1. 6Create snapshot
      POST/v2/sessions/{sessionId}/view-edit-snapshots

      Keep URL, operation, expiry

    2. 7Create handoff
      POST/v2/sessions/{sessionId}/render-handoffs

      Keep operationId

    3. 8Download handoff
      GET/v2/sessions/{sessionId}/render-handoffs/{handoffId}

      Keep manifest

    4. 9Close session
      POST/v2/sessions/{sessionId}/close

      Keep 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:

GoalAgentWhat to send
Understand the footagesource.workWhat to find or transcribe
Plan the storyplanner.compileThe desired length, structure, and emphasis
Build the edittimeline.authorOmit 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

ResourceThink of it asKeep
SessionOne user's video workspacesessionId, currentEditId
AssetOne verified media fileassetId, SHA-256, byte size
TurnOne question or editing instructionYour unique requestId
OperationThe work running in the backgroundoperationId, latest cursor
SnapshotA temporary browser view of one editviewUrl, expiry
Render handoffThe edit and asset manifest for a rendererhandoffId

Endpoint reference

MethodPathPurpose
POST/v2/sessionsCreate an empty stateful editing session.
GET/v2/sessions/{sessionId}Read session state, assets, and currentEditId.
POST/v2/sessions/{sessionId}/forksStart 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}/assetsDeclare an asset's name, size, MIME type, and SHA-256 digest.
PUT/v2/sessions/{sessionId}/assets/{assetId}/contentUpload the declared asset as raw bytes.
POST/v2/sessions/{sessionId}/turnsSubmit an idempotent ask or edit turn.
POST/v2/sessions/{sessionId}/narrative-repairsCheck 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-snapshotsCreate a detached browser view of an edit.
POST/v2/sessions/{sessionId}/render-handoffsStart a renderer-neutral handoff for an edit.
GET/v2/sessions/{sessionId}/render-handoffs/{handoffId}Download an authenticated handoff manifest.
POST/v2/sessions/{sessionId}/closeCancel active work and close the private session.

JSON request objects are closed: unknown fields are rejected.

Sessions and assets

POST /v2/sessions accepts:

FieldRequiredMeaning
projectYesProject name, 1–120 characters.
canvasNo[width, height]; each value must be a positive integer no larger than 16384.
providerNoObject 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.

AgentWhat it doesMessage and revision rules
source.workUnderstands uploaded media and publishes the current source brief.Requires message; never accepts baseEditId.
source.generateGenerates media and publishes a source brief over the results.Requires message and at least one of img2video or voiceover; never accepts baseEditId.
planner.compileTurns the current source brief into a grounded plan.Requires message; never accepts baseEditId.
timeline.authorCreates the structural edit.Omit message and baseEditId to build the current plan as a new root; use message for direct authoring.
timeline.graphicsAdds shapes, panels, backings, and accents.Requires message and the latest baseEditId.
timeline.soundAdds music, sound effects, balance, and ducking.Requires message and the latest baseEditId.
timeline.titlesAdds titles, cards, lower thirds, and typography.Requires message and the latest baseEditId.
timeline.captionsAdds transcript-aligned captions.Requires message and the latest baseEditId.
timeline.reframeReframes 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:

FieldMeaning
requestIdRequired idempotency key, 1–200 characters, scoped to the session.
modeedit.
agentOne agent ID from the table above.
messageNatural-language instruction, up to 20,000 characters. Only plan-backed Author may omit it.
baseEditIdRevision the timeline specialist must extend. See Revision rules.
providerOptional agent settings: name, model, thinking. Also accepted by ask turns. This is separate from tool provider settings in toolConfig.
toolsUp to four explicitly granted capabilities. Grants apply only to this turn.
toolConfigPer-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.

ToolAllowed agentsConfiguration
transcribesource.work, planner.compile, timeline.authorprovider, model, language, diarize; word timestamps are always enabled.
detect_eventssource.work, planner.compile, timeline.authorsampleDensity: sparse or dense; maxFrames: 1–2400.
detect_beatssource.workNo configuration.
maskingsource.work, source.generate, planner.compile, timeline.author, timeline.graphics, timeline.sound, timeline.titles, timeline.captionsNo caller configuration.
audio_boundariesplanner.compile, narrative repairOptional searchRadius: 0.1–3.0 seconds.
img2videosource.generateGeneration settings such as vendor, resolution, duration, and aspectRatio.
voiceoversource.generateGeneration 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=0

The response contains:

FieldMeaning
statusqueued, running, succeeded, failed, or cancelled.
cursorLatest event cursor. Send it as the next after value.
eventsProgress events newer than after.
resultPresent after success. Its type selects the result shape.
errorPresent after failure or cancellation.
Result typeImportant fields
answeranswer
sourceagent, artifact, editId
planagent, artifact, editId
editagent, editId, previousEditId, artifact
narrative_repairpassed, acceptedPlanId, repairs, stopReason, models
view_edit_snapshoteditId
render_handoffhandoffId, 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.author turn omits baseEditId.
  • Plan-backed timeline.author omits both message and baseEditId; it creates a new root revision from the current plan.
  • Every later timeline turn, including timeline.reframe, sends the latest successful editId as baseEditId.
  • 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 accept baseEditId.

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.

On this page