Render a narrative as video
POST /v2/external/video/narrative_to_video
Start an express video from a completed NarrativeRequest. Samsar copies the stored themeJson, narrativeJson, and movieResourceList into a new VideoSession, skips prompt generation, and continues with image, speech, music, AI-video, frame, and final-video generation.
The source can be narrative_type: "singular" or narrative_type: "branched". The source request is never modified.
Request body
{
"input": {
"narrative_request_id": "687b4127b503e2b4acdf9876",
"image_model": "GPTIMAGE2",
"video_model": "RUNWAYML"
},
"webhookUrl": "https://example.com/webhooks/video"
}
| Field | Type | Required | Description |
|---|---|---|---|
narrative_request_id | string | Yes | Owned, completed singular or branched NarrativeRequest ID. |
image_model | string | No | Express image-model override. The authenticated user's configured default is used when omitted. |
video_model | string | No | Express video-model override. The authenticated user's configured default is used when omitted. |
webhookUrl | string | No | Top-level URL that receives one terminal session webhook. |
The ID aliases narrativeRequestId, session_id, sessionId, request_id, and requestId are accepted. Model aliases may be camel-cased. Do not provide prompt or duration; both are inherited from the source request and overrides are rejected.
Submit and poll
curl -X POST "https://api.samsar.one/v2/external/video/narrative_to_video" \
-H "Authorization: Bearer $SAMSAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"narrative_request_id": "687b4127b503e2b4acdf9876"
}
}'
HTTP 200 returns the new video request ID:
{
"request_id": "687b4d244d7798b1fbec0123",
"session_id": "687b4d244d7798b1fbec0123",
"source_narrative_request_id": "687b4127b503e2b4acdf9876",
"status": "PENDING"
}
Poll the video route, not the narrative status route:
curl "https://api.samsar.one/v2/external/video/status?request_id=687b4d244d7798b1fbec0123" \
-H "Authorization: Bearer $SAMSAR_API_KEY"
Branch rendering
For a singular source, the session follows the existing linear pipeline and returns one video.
For a branched source, each leaf node is a complete linear viewing path. Samsar generates shared image, speech, music, and scene-video assets once, then generates path-specific frames and one final video for every leaf. A tree with num_levels: 2 therefore returns four videos.
Compact branch status
The normal status response includes a normalized branching manifest with schema branched_video_status.v1. It is compact enough for regular polling: it contains tree choices, path-level frame and video status, aggregate progress, and completed path URLs, but it does not contain rendered frame lists or duplicate the shared media assets.
The example below shows one path waiting for final video generation while its sibling is still generating frames:
{
"request_id": "687b4d244d7798b1fbec0123",
"session_id": "687b4d244d7798b1fbec0123",
"status": "PENDING",
"narrative_type": "branched",
"render_plan_version": 1,
"default_path_id": "root.1",
"branching": {
"schema": "branched_video_status.v1",
"status": "PENDING",
"is_complete": false,
"finalized": false,
"render_plan_version": 1,
"default_path_id": "root.1",
"summary": {
"total_paths": 2,
"completed_paths": 0,
"pending_paths": 2,
"failed_paths": 0,
"cancelled_paths": 0,
"frame_paths_completed": 1,
"video_paths_completed": 0,
"progress_percent": 25
},
"tree": {
"root_node_id": "root",
"num_levels": 1,
"branching_factor": 2,
"node_count": 3,
"leaf_node_ids": ["root.1", "root.2"],
"branch_scene_indices": [2],
"choice_points": [
{
"branch_point_id": "branch-point:root",
"parent_node_id": "root",
"level": 1,
"divergence_scene_index": 2,
"switch_at_seconds": 15,
"options": [
{
"child_node_id": "root.1",
"branch_ordinal": 1,
"path_name": "Follow the signal",
"path_description": "Trace the mysterious broadcast to its source.",
"leaf_path_ids": ["root.1"]
},
{
"child_node_id": "root.2",
"branch_ordinal": 2,
"path_name": "Shut it down",
"path_description": "Destroy the receiver before the signal spreads.",
"leaf_path_ids": ["root.2"]
}
]
}
]
},
"paths": [
{
"path_id": "root.1",
"leaf_node_id": "root.1",
"ordinal": 0,
"is_default": true,
"node_ids": ["root", "root.1"],
"duration": 60,
"status": "PENDING",
"current_stage": "video_generation",
"stages": {
"frame_generation": "COMPLETED",
"video_generation": "PENDING"
},
"stage_details": {
"frame_generation": {
"status": "COMPLETED",
"pending": false
},
"video_generation": {
"status": "PENDING",
"pending": true
}
},
"selection_trail": [
{
"branch_point_id": "branch-point:root",
"node_id": "root.1",
"parent_node_id": "root",
"level": 1,
"divergence_scene_index": 2,
"switch_at_seconds": 15,
"path_name": "Follow the signal"
}
]
},
{
"path_id": "root.2",
"leaf_node_id": "root.2",
"ordinal": 1,
"is_default": false,
"node_ids": ["root", "root.2"],
"duration": 60,
"status": "PENDING",
"current_stage": "frame_generation",
"stages": {
"frame_generation": "PENDING",
"video_generation": "INIT"
}
}
],
"outputs": {
"ready": false,
"default_path_id": "root.1"
}
}
}
branching.summary.progress_percent describes the path-specific frame/final-video fan-out. The shared image, speech, music, and scene-video stages remain available in expressGenerationStatus. Each branching.paths[] entry reports its own current_stage, normalized stages, and detailed pending/error state.
Detailed branch preview
Use status_detailed when the client needs to preview a path before all final videos are ready:
curl "https://api.samsar.one/v2/external/video/status_detailed?request_id=687b4d244d7798b1fbec0123" \
-H "Authorization: Bearer $SAMSAR_API_KEY"
The response includes the detailed manifest at both top-level branching and session.branching. Both copies add normalized path timelines:
{
"status_detail_schema": "video_session_preview.v1",
"session": {
"layers": [
{
"id": "687b4e1a4d7798b1fbec1001",
"startTime": 0,
"endTime": 5,
"duration": 5,
"preview": {
"stage": "ai_video_generation",
"type": "video",
"url": "https://cdn.example.com/shared-scene-1.mp4"
}
}
],
"audioLayers": [
{
"id": "687b4e1a4d7798b1fbec2001",
"type": "speech",
"status": "COMPLETED",
"startTime": 0,
"endTime": 5,
"duration": 5,
"url": "https://cdn.example.com/shared-scene-1.mp3"
}
],
"branching": {
"schema": "branched_video_status.v1",
"default_path_id": "root.1",
"paths": [
{
"path_id": "root.1",
"timeline": [
{
"sequence_index": 0,
"scene_index": 0,
"layer_id": "687b4e1a4d7798b1fbec1001",
"start_time": 0,
"end_time": 5,
"duration": 5,
"frame_generation": {
"status": "COMPLETED",
"pending": false
}
}
],
"audio_timeline": [
{
"sequence_index": 0,
"audio_layer_id": "687b4e1a4d7798b1fbec2001",
"connected_layer_id": "687b4e1a4d7798b1fbec1001",
"start_time": 0,
"end_time": 5,
"duration": 5
}
]
}
]
}
}
}
The media catalog is normalized: session.layers and session.audioLayers contain each generated asset once, while every path timeline refers to those assets by layer_id or audio_layer_id. Build an ID lookup table, walk the selected path's timeline in sequence_index order, and resolve each reference to its current preview asset. The API intentionally omits internal rendered-frame arrays from polling responses.
Final outputs and interactive switching
A branched request reaches top-level status: "COMPLETED" only when every required leaf path has a completed final render and URL. At that point:
result_urlis the deterministic default-path video, preserving the singular-video contract.result_urlscontains every leaf video in stable path order.branching.outputs.readyistrue, andbranching.outputs.pathsmaps eachpath_idto its URL.branch_resultsremains available as a compact compatibility alias for existing clients.
Selected terminal fields look like this:
{
"status": "COMPLETED",
"narrative_type": "branched",
"default_path_id": "root.1",
"result_url": "https://cdn.example.com/root.1.mp4",
"result_urls": [
"https://cdn.example.com/root.1.mp4",
"https://cdn.example.com/root.2.mp4"
],
"branch_results": [
{
"path_id": "root.1",
"leaf_node_id": "root.1",
"ordinal": 0,
"status": "COMPLETED",
"result_url": "https://cdn.example.com/root.1.mp4"
},
{
"path_id": "root.2",
"leaf_node_id": "root.2",
"ordinal": 1,
"status": "COMPLETED",
"result_url": "https://cdn.example.com/root.2.mp4"
}
],
"branching": {
"schema": "branched_video_status.v1",
"status": "COMPLETED",
"is_complete": true,
"finalized": true,
"default_path_id": "root.1",
"summary": {
"total_paths": 2,
"completed_paths": 2,
"pending_paths": 0,
"failed_paths": 0,
"frame_paths_completed": 2,
"video_paths_completed": 2,
"progress_percent": 100
},
"outputs": {
"ready": true,
"default_path_id": "root.1",
"default_url": "https://cdn.example.com/root.1.mp4",
"paths": [
{
"path_id": "root.1",
"leaf_node_id": "root.1",
"ordinal": 0,
"is_default": true,
"url": "https://cdn.example.com/root.1.mp4"
},
{
"path_id": "root.2",
"leaf_node_id": "root.2",
"ordinal": 1,
"is_default": false,
"url": "https://cdn.example.com/root.2.mp4"
}
]
}
}
}
For an interactive player, show a choice when playback reaches tree.choice_points[].switch_at_seconds. Each option supplies the descendant leaf_path_ids; use successive choices to narrow the candidate set to one leaf output. The matching path's selection_trail records the exact ordered choices and switch times. Because every leaf video preserves its shared parent prefix, the client can switch to the selected leaf video at the same playback time.
If any required path fails, the aggregate request is FAILED. Completed sibling URLs remain visible on their path entries and in legacy branch_results for diagnosis or recovery, but branching.outputs.ready remains false and the top-level terminal result_urls set is not published as complete.
When webhookUrl is configured, the terminal success webhook and the persisted external-request response include the same compact branching, branch_results, default_path_id, and ordered result_urls fields. Failure webhooks include the normalized branch state for diagnosis but never publish the aggregate result_urls set; that set is reserved for successful full completion. For external requests, webhook request_id and session_id contain the public extreq_... ID; upstream_session_id contains the internal VideoSession ID for server-side correlation.
samsar-js
const queued = await samsar.createExternalVideoFromNarrative({
narrative_request_id: '687b4127b503e2b4acdf9876',
image_model: 'GPTIMAGE2',
video_model: 'RUNWAYML',
});
const status = await samsar.getV2ExternalVideoStatus(
queued.data.request_id,
);
const manifest = status.data.branching;
for (const path of manifest?.paths ?? []) {
console.log(path.path_id, path.current_stage, path.result_url);
}
const detailed = await samsar.getV2ExternalVideoStatusDetailed(
queued.data.request_id,
);
const previewManifest = detailed.data.session?.branching;
const selectedPath = previewManifest?.paths.find(
(path) => path.path_id === previewManifest.default_path_id,
);
Clients that have not adopted the normalized manifest can continue reading branch_results, result_url, and result_urls. Singular video responses retain their existing shape and do not require branch-aware handling.
Validation and billing
The source must belong to the authenticated user, be terminally successful, have settled narrative billing, and contain valid stored artifacts. Branched sources are revalidated as narrative trees before media work starts.
Narrative inference is not run or billed again. The new VideoSession is billed at the selected model's full express-render unit price. A singular request uses its linear playback duration. A branched request instead uses the cumulative duration of the normalized layer catalog:
branched render credits = selected model unit price × cumulative unique layer duration
Shared parent layers count once, every divergent and leaf-only layer counts once, and a shared outro counts once. Complete leaf-path durations are not added together, so rendering multiple leaf videos does not repeatedly bill their shared prefixes. Samsar fixes this cumulative duration when it materializes the canonical layer catalog; later speech or video timing adjustments can change playback timing but do not make different pipeline stages use different billing durations. Optional express add-ons remain separately billable when selected.