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 returnsstage_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:
- first-video.mjs: the complete video workflow.
- upload-video.mjs: API requests, saved request tokens, and resumable uploads.
- poll-job.mjs: bounded job polling.
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.
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.jsonThe 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:
export SOVRAN_MAX_CREDITS="1"
node first-video.mjs ./hook.mp4 ./body.mp4 ./first-video-state.jsonThe 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.
{ "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:
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:
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.
const renderBody = { ...settings, planFingerprint: estimate.planFingerprint }Acceptance returns HTTP 202. This is an example response; IDs differ for each job:
{
"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:
{
"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.