Sovran
API documentation

Create your first video

Upload two clips, estimate one video, and read its finished output URL.

This guide joins a hook clip and a body clip into one vertical video. The runnable Node.js example carries each returned ID into the next request. It waits for media preparation, checks the credit estimate, and reads the finished output URL.

Before you start

  • Use Node.js 22 or later and pnpm.
  • Create an API key with Read, Write, and Generate permissions in Developer.
  • Use a workspace with a current paid plan and available render credits.
  • Prepare two MP4, MOV, or WebM files. Their combined size must be at most 1 GiB (1,073,741,824 bytes).
  • Complete the first read request. If it returns api_disabled, access is closed on that host. If it returns stage_unavailable, this feature is closed. Stop before you upload files.

No music or provider connection is needed for this example. Captions are off.

Run the example

Save these files in the same local directory:

The commands below run the example on the production host. Use http://localhost:3000 for the local host. Use a key from the workspace on that host. See Authentication.

Set up and review the first estimate
pnpm add tus-js-client@4.3.1
export SOVRAN_API_ORIGIN="https://sovran.ai"
# Set SOVRAN_API_KEY through your secret store or server environment.
export SOVRAN_MAX_CREDITS="0"
node first-video.mjs ./hook.mp4 ./body.mp4 ./first-video-state.json

The first run creates a project, uploads both clips, waits for preparation, creates a sequence, and prints the estimate. With a credit limit of 0, it stops before render submission. Read the reported creditCost. This example estimates one finished video at one render credit.

After you accept that charge, run the same command with a limit of 1:

Submit the saved plan and read its output
export SOVRAN_MAX_CREDITS="1"
node first-video.mjs ./hook.mp4 ./body.mp4 ./first-video-state.json

The second run uses the saved resources. It sends the estimated planFingerprint, waits for the accepted job, and prints renderId, filename, url, and expiresAt. It does not download the finished video.

Keep the state file when a request times out or the result is unknown. The example saves the exact request body and its Idempotency-Key before each change. A repeated run reuses them. Do not delete the state file to resolve an uncertain result. Use the same host, key, files, and state file. Run one process per state file.

The state file contains resource IDs and limited upload tokens. It does not contain the API key. Keep it private and out of version control. If preparation or job polling reaches its time limit, a repeated run reads the existing work. It does not submit another render batch.

Follow the requests

The paths below include the /api/v1 prefix. Successful JSON responses wrap the result in data and include requestId. The downloaded createApi helper returns data.

1. Create a project

POST /api/v1/projects returns HTTP 201. Save data.id as projectId.

POST /api/v1/projects — request body
{ "name": "API first video" }

Each change needs its own request token. The example stores one token for project creation and reuses it only for that same request.

2. Upload and prepare both clips

For each file, call POST /api/v1/projects/{projectId}/uploads. Save the returned upload id and assetId. Transfer the file to the returned TUS endpoint with its headers, metadata, and chunk size. Then call POST /api/v1/uploads/{uploadId}/complete with {} and a separate saved request token.

The upload guide has the complete standalone example. Read GET /api/v1/assets/{assetId} until data.preparation.renderReady is true. A completed transfer is not a prepared video. Stop if preparation.state is failed.

3. Create a sequence with the returned asset IDs

POST /api/v1/projects/{projectId}/sequences returns HTTP 201. Save data.id as sequenceId. The runnable example builds this body with the two returned asset IDs:

POST /api/v1/projects/{projectId}/sequences — request body
const body = {
  name: 'API first video',
  blocks: [
    { position: 0, label: 'Hook', assetIds: [hookAsset.id] },
    { position: 1, label: 'Body', assetIds: [bodyAsset.id] },
  ],
  musicTracks: [],
  voiceoverTracks: [],
  durationLimits: { voiceover: true, music: true, loopMusic: false },
}

4. Select clips and estimate credits

Read GET /api/v1/sequences/{sequenceId}/combinations?limit=1. Save the first items[].id, inputFingerprint, and sequenceUpdatedAt. These values protect the selected clips and sequence revision.

Use them in POST /api/v1/sequences/{sequenceId}/render-estimate:

POST /api/v1/sequences/{sequenceId}/render-estimate — request body
const settings = {
  expectedUpdatedAt: manifest.sequenceUpdatedAt,
  combinationIds: [manifest.items[0].id],
  inputFingerprint: manifest.inputFingerprint,
  maxOutputs: 1,
  aspectRatio: '9:16',
  resolution: '1080',
  quality: 'high',
  fps: 30,
  captionsEnabled: false,
}

The estimate returns HTTP 200. It includes outputCount, creditCost, featureId, planFingerprint, inputFingerprint, namingPattern, requiredCustomFields, maximumSourceBytes, and maxSourceBytes. Save planFingerprint. An estimate does not submit a render or charge credits.

5. Submit the estimated plan

POST /api/v1/sequences/{sequenceId}/renders needs Generate permission. Use the same settings, add the estimated fingerprint, and send a saved Idempotency-Key.

POST /api/v1/sequences/{sequenceId}/renders — request body
const renderBody = { ...settings, planFingerprint: estimate.planFingerprint }

Acceptance returns HTTP 202. This is an example response; IDs differ for each job:

POST /api/v1/sequences/{sequenceId}/renders — HTTP 202
{
  "data": {
    "jobId": "10000000-0000-4000-8000-000000000004",
    "statusUrl": "/api/v1/jobs/10000000-0000-4000-8000-000000000004",
    "outputCount": 1
  },
  "requestId": "10000000-0000-4000-8000-000000000005"
}

If the sequence or render inputs changed, the API rejects the old revision or plan. Read the current resources and obtain a new estimate. Keep the old request token with its original body. Use a new token for a reviewed, changed request. Do not create a new token after an unknown result.

6. Poll the job and read the output URL

Read GET /api/v1/jobs/{jobId}. The sample reads the job immediately, then increases its delay from 1 second to at most 10 seconds. It stops after 5 minutes. It stops for a failure or a result that needs review. See Jobs and recovery.

This is an example success response:

GET /api/v1/jobs/{jobId} — HTTP 200
{
  "data": {
    "id": "10000000-0000-4000-8000-000000000004",
    "type": "sequence_render",
    "status": "succeeded",
    "counts": { "total": 1, "succeeded": 1, "failed": 0, "pending": 0 },
    "progress": 100,
    "outputs": [
      {
        "renderId": "10000000-0000-4000-8000-000000000006",
        "aspectRatio": "9:16",
        "status": "ready",
        "error": null
      }
    ],
    "error": null,
    "createdAt": "2026-10-04T16:00:00.000Z",
    "completedAt": "2026-10-04T16:02:00.000Z"
  },
  "requestId": "10000000-0000-4000-8000-000000000007"
}

Take the ready output's renderId and read GET /api/v1/renders/{renderId}/outputs. Select the entry with status: "ready" and the required aspectRatio. Its url is a signed media URL. Use expiresAt to check its lifetime. Read this route again for a new URL when it expires.

Create more variations

Add more video asset IDs to a block to create clip choices. Music and voiceover choices also multiply the finished output count. Set maxOutputs to a hard limit from 1 to 100 videos. A larger batch is rejected before a charge; it is not shortened. The selection must contain at most 100,000 clip combinations.

Each finished video's distinct source files must total at most 1 GiB (1,073,741,824 bytes). See Limits for upload and caption limits. Render resolution values are preview, mobile, sd, hd, and 1080. Quality values are verylow, low, medium, high, and veryhigh. Frame rates are 25, 30, and 60.

To replace a saved sequence configuration, use PUT /api/v1/sequences/{sequenceId}/configuration with its current expectedUpdatedAt and all configuration fields. A stale revision returns HTTP 409. Sequences with saved timeline edits use the composition workflow.

Was this article helpful?

On this page