Uploading Assets

To fully leverage the Scenario API for content generation, you often need to provide input assets, such as reference images for img2img, audio files for video lip-sync, or 3D models for 3D generation. The Scenario API provides two endpoints for uploading files:

  • POST /v1/assets - For uploading small images (< 6MB) using simple base64 upload
  • POST /v1/uploads - For uploading larger images (≥ 6MB), audio, video, and 3D models using multipart upload

This guide will help you understand when to use each endpoint and how to upload files successfully.


Quick Reference: Which Endpoint to Use?

File TypeFile SizeEndpointMethod
Images (JPEG, PNG, GIF, SVG, WebP, etc.)< 6MB/v1/assetsSimple base64 upload
Images (JPEG, PNG, GIF, SVG, WebP, etc.)≥ 6MB/v1/uploadsMultipart upload
Audio (MP3, WAV, OGG, M4A)Any size/v1/uploadsMultipart upload
Video (MP4, WebM)Any size/v1/uploadsMultipart upload
3D Models (GLB, GLTF, OBJ, FBX, etc.)Any size/v1/uploadsMultipart upload

Uploading Images: /v1/assets

The /v1/assets endpoint is the simplest way to upload small images (under 6MB). It accepts images in base64 format and returns an assetId immediately. For larger images (6MB or more), use the /v1/uploads endpoint with multipart upload instead.

Supported Image Formats

  • JPEG - image/jpeg
  • PNG - image/png
  • GIF - image/gif
  • SVG - image/svg+xml
  • WebP - image/webp
  • AVIF - image/avif
  • TIFF - image/tiff
  • HEIF / HEIC - image/heic

Endpoint Details

Endpoint: POST https://api.cloud.scenario.com/v1/assets?projectId={projectId}

Request Body

ParameterTypeDescription
imagestringRequired. The image in base64 format
namestringRequired. The name of the image file

Optional Parameters:

  • hide - Boolean to hide the asset from the UI
  • parentId - Parent asset ID for organizing assets
  • collectionIds - Array of collection IDs to add the asset to

Code Examples

Python

import requests
import base64
import json

api_key = "YOUR_API_KEY"
api_secret = "YOUR_API_SECRET"
project_id = "YOUR_PROJECT_ID"

url = f"https://api.cloud.scenario.com/v1/assets?projectId={project_id}"

# Read and encode image as base64
file_path = "./my_image.png"
with open(file_path, "rb") as image_file:
    encoded_string = base64.b64encode(image_file.read()).decode("utf-8")

# JSON payload
payload = {
    "image": encoded_string,
    "name": "my_image_base64_upload",
}

headers = {
    "Content-Type": "application/json"
}

# Make request with basic auth
response = requests.post(
    url, 
    headers=headers, 
    data=json.dumps(payload), 
    auth=(api_key, api_secret)
)

# Handle response
if response.status_code == 200:
    data = response.json()
    asset_id = data.get("asset", {}).get("id")
    print(f"Asset uploaded successfully! Asset ID: {asset_id}")
    print("Response:", data)
else:
    print(f"Error uploading asset: {response.status_code} - {response.text}")

Node.js

const fetch = require("node-fetch");
const fs = require("fs");
const path = require("path");

const apiKey = "YOUR_API_KEY";
const apiSecret = "YOUR_API_SECRET";
const projectId = "YOUR_PROJECT_ID";
const credentials = Buffer.from(`${apiKey}:${apiSecret}`).toString("base64");

async function uploadImage() {
  const url = `https://api.cloud.scenario.com/v1/assets?projectId=${projectId}`;
  const filePath = "./my_image.png";

  try {
    // Read and encode image to Base64
    const fileBuffer = fs.readFileSync(filePath);
    const base64Image = fileBuffer.toString("base64");

    // Prepare JSON payload
    const payload = {
      image: base64Image,
      name: path.basename(filePath),
    };

    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Authorization": `Basic ${credentials}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(payload)
    });

    const data = await response.json();

    if (response.ok) {
      const assetId = data.asset.id;
      console.log(`Asset uploaded successfully! Asset ID: ${assetId}`);
      console.log("Response:", data);
    } else {
      console.error(`Error uploading asset: ${response.status} - ${JSON.stringify(data)}`);
    }
  } catch (error) {
    console.error("Error:", error.message || error);
  }
}

uploadImage();

Response

A successful response returns an HTTP 200 OK status code and a JSON object containing details about the uploaded asset:

{
    "asset": {
        "id": "asset_staging_qSACAtKeAbd1EmAEG4YNguea",
        "url": "https://cdn.cloud.scenario.com/assets-transform/asset_staging_qSACAtKeAbd1EmAEG4YNguea?...",
        "mimeType": "image/png",
        "metadata": {
            "type": "uploaded",
            "name": "fileName.png",
            "kind": "image",
            "width": 1024,
            "height": 1024,
            "size": 979892
        },
        "properties": {
            "width": 1024,
            "height": 1024,
            "size": 979892
        },
        "kind": "image",
        "source": "uploaded",
        "ownerId": "proj_eTyi419soRtdN2AexdFHZDqX",
        "authorId": "03223725c4b2d23114ab7d673bcbf850",
        "createdAt": "2025-06-30T15:15:27.153Z",
        "updatedAt": "2025-06-30T15:15:27.153Z",
        "privacy": "private",
        "tags": [],
        "collectionIds": [],
        "status": "success",
        "editCapabilities": [
            "REMOVE_BACKGROUND",
            "REFINE",
            "PIXELATE",
            "PROMPT_EDITING",
            "UPSCALE",
            "UPSCALE_360",
            "DETECTION",
            "VECTORIZATION",
            "SEGMENTATION",
            "REFRAME",
            "GENERATIVE_FILL"
        ]
    }
}

Key fields:

  • id - The unique assetId for referencing in other API calls
  • url - The URL where the asset can be accessed
  • mimeType - The MIME type of the uploaded file
  • properties - Image dimensions and size information

Uploading Larger Images, Audio, Video, and 3D Models: /v1/uploads

The /v1/uploads endpoint uses a multipart upload process for:

  • Large images (6MB or larger) that exceed the /v1/assets size limit
  • Audio files (for video lip-sync, music generation, etc.)
  • Video files
  • 3D models

This endpoint is required for all non-image files and recommended for images that are 6MB or larger.

Supported File Types

Images (Large Files)

  • JPEG - image/jpeg
  • PNG - image/png
  • GIF - image/gif
  • SVG - image/svg+xml
  • WebP - image/webp
  • AVIF - image/avif
  • TIFF - image/tiff
  • HEIF / HEIC - image/heic

Note: For images under 6MB, use /v1/assets for simpler upload. For images 6MB or larger, use /v1/uploads with kind: "image".

Audio Files

  • MP3 - audio/mpeg or audio/mp3
  • WAV - audio/wav
  • OGG - audio/ogg
  • M4A - audio/m4a

Video Files

  • MP4 - video/mp4
  • WebM - video/webm

3D Models

  • GLB - model/gltf-binary
  • GLTF - model/gltf+json
  • OBJ - model/obj
  • FBX - application/vnd.autodesk.fbx
  • STL - model/stl
  • PLY - model/ply
  • VOX - model/x-3d-vox
  • And other 3D formats

Multipart Upload Process

The /v1/uploads endpoint uses a 4-step multipart upload process:

  1. Initialize the upload - Create an upload entry and get presigned URLs
  2. Upload file parts - Upload your file to the presigned URLs
  3. Complete the upload - Signal that all parts have been uploaded
  4. Retrieve the assetId - Poll the upload status until processing is complete

Step-by-Step Guide: Multipart Upload

Step 1: Initialize the Upload

Make a POST request to /v1/uploads with file metadata:

Endpoint: POST /v1/uploads?projectId={projectId}

Request Body (Large Image Example):

{
  "fileName": "large-image.png",
  "contentType": "image/png",
  "kind": "image",
  "parts": 1,
  "fileSize": 8388608
}

Request Body (Audio Example):

{
  "fileName": "audio.mp3",
  "contentType": "audio/mpeg",
  "kind": "audio",
  "parts": 1,
  "fileSize": 1234567
}

Request Body (Video Example):

{
  "fileName": "video.mp4",
  "contentType": "video/mp4",
  "kind": "video",
  "parts": 1,
  "fileSize": 5678901
}

Request Body (3D Model Example):

{
  "fileName": "model.glb",
  "contentType": "model/gltf-binary",
  "kind": "3d",
  "parts": 1,
  "fileSize": 2345678
}

Parameters:

  • fileName (required) - The name of your file
  • contentType (required) - The MIME type of your file:
    • Images: image/jpeg, image/png, image/gif, etc.
    • Audio: audio/mpeg, audio/wav, audio/ogg, audio/m4a
    • Video: video/mp4, video/webm
    • 3D Models: model/gltf-binary, model/gltf+json, model/obj, etc.
  • kind (required) - The file type: "image", "audio", "video", or "3d"
  • parts (required) - Number of parts (use 1 for files < 5MB)
  • fileSize (required) - Size of the file in bytes

Response:

{
  "upload": {
    "id": "upload_abc123",
    "status": "pending",
    "parts": [
      {
        "number": 1,
        "url": "https://s3.amazonaws.com/...",
        "expires": "2024-01-01T12:00:00Z"
      }
    ],
    "jobId": "job_xyz789"
  }
}

Save the upload.id and the parts array for the next steps.

Step 2: Upload File Parts

For each part in the parts array, make a PUT request to the presigned URL with the file data.

For single-part uploads (files < 5MB):

const file = fs.readFileSync('audio.mp3');
const part = upload.parts[0];

await fetch(part.url, {
  method: 'PUT',
  body: file
});

For multi-part uploads (files > 5MB): Split your file into chunks and upload each chunk:

const fileBuffer = fs.readFileSync('large-file.mp3');
const partSize = Math.ceil(fileBuffer.length / partsCount);

for (let i = 0; i < partsCount; i++) {
  const start = i * partSize;
  const end = Math.min((i + 1) * partSize, fileBuffer.length);
  const chunk = fileBuffer.slice(start, end);
  const part = upload.parts[i];
  
  await fetch(part.url, {
    method: 'PUT',
    body: chunk
  });
}

Important Notes:

  • Presigned URLs expire after a certain time (check the expires field)
  • Each part must be at least 5MB (except the last part)
  • Maximum number of parts is 10,000
  • Maximum file size is 5TB

Step 3: Complete the Upload

After all parts have been uploaded, signal completion:

Endpoint: POST /v1/uploads/{uploadId}/action?projectId={projectId}

Request Body:

{
  "action": "complete"
}

Response:

{
  "upload": {
    "id": "upload_abc123",
    "status": "validating",
    "entityId": null
  }
}

The upload status will change from "pending""validating""validated""imported" when processing is complete.

Step 4: Retrieve the Asset ID

Poll the upload status until the entityId field is populated. This entityId is your assetId that you can use in other API calls.

Endpoint: GET /v1/uploads/{uploadId}?projectId={projectId}

Response (while processing):

{
  "upload": {
    "id": "upload_abc123",
    "status": "validating",
    "entityId": null
  }
}

Response (when complete):

{
  "upload": {
    "id": "upload_abc123",
    "status": "imported",
    "entityId": "asset_xyz789"
  }
}

Complete Code Examples

Example 1: Upload Image (Simple)

import requests
import base64

def upload_image(file_path, project_id, api_key, api_secret):
    url = f"https://api.cloud.scenario.com/v1/assets?projectId={project_id}"
    
    with open(file_path, "rb") as f:
        encoded = base64.b64encode(f.read()).decode("utf-8")
    
    response = requests.post(
        url,
        headers={"Content-Type": "application/json"},
        json={"image": encoded, "name": file_path.split("/")[-1]},
        auth=(api_key, api_secret)
    )
    
    return response.json()["asset"]["id"]

# Usage
asset_id = upload_image("./image.png", "proj_...", "key_...", "secret_...")
print(f"Image uploaded! Asset ID: {asset_id}")

Example 2: Upload Large Image

import requests
import time
import os

def upload_large_image(file_path, project_id, api_key):
    """Upload an image that is 6MB or larger using multipart upload."""
    API_BASE = "https://api.cloud.scenario.com"
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }
    
    file_size = os.path.getsize(file_path)
    file_name = os.path.basename(file_path)
    
    # Determine content type based on extension
    ext = file_name.split('.')[-1].lower()
    content_types = {
        'jpg': 'image/jpeg',
        'jpeg': 'image/jpeg',
        'png': 'image/png',
        'gif': 'image/gif',
        'webp': 'image/webp',
        'svg': 'image/svg+xml'
    }
    content_type = content_types.get(ext, 'image/jpeg')
    
    # Step 1: Initialize upload
    init_response = requests.post(
        f'{API_BASE}/v1/uploads',
        params={'projectId': project_id},
        headers=headers,
        json={
            'fileName': file_name,
            'contentType': content_type,
            'kind': 'image',  # Note: kind is 'image' for images
            'parts': 1,
            'fileSize': file_size
        }
    )
    init_response.raise_for_status()
    upload = init_response.json()['upload']
    upload_id = upload['id']
    
    # Step 2: Upload file part
    with open(file_path, 'rb') as f:
        image_data = f.read()
    
    part = upload['parts'][0]
    requests.put(part['url'], data=image_data).raise_for_status()
    
    # Step 3: Complete upload
    requests.post(
        f'{API_BASE}/v1/uploads/{upload_id}/action',
        params={'projectId': project_id},
        headers=headers,
        json={'action': 'complete'}
    ).raise_for_status()
    
    # Step 4: Wait for processing and get assetId
    while True:
        time.sleep(3)
        status_response = requests.get(
            f'{API_BASE}/v1/uploads/{upload_id}',
            params={'projectId': project_id},
            headers={'Authorization': f'Bearer {api_key}'}
        )
        upload_status = status_response.json()['upload']
        
        if upload_status['status'] == 'imported' and upload_status.get('entityId'):
            return upload_status['entityId']
        elif upload_status['status'] == 'failed':
            raise Exception(f"Upload failed: {upload_status.get('errorMessage')}")

# Usage
asset_id = upload_large_image('./large-image.png', 'proj_...', 'key_...')
print(f"Large image uploaded! Asset ID: {asset_id}")

Example 3: Upload Audio for VEED Fabric

import requests
import time
import os

def upload_audio_file(file_path, project_id, api_key):
    API_BASE = "https://api.cloud.scenario.com"
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }
    
    file_size = os.path.getsize(file_path)
    file_name = os.path.basename(file_path)
    
    # Determine content type
    ext = file_name.split('.')[-1].lower()
    content_types = {
        'mp3': 'audio/mpeg',
        'wav': 'audio/wav',
        'ogg': 'audio/ogg',
        'm4a': 'audio/m4a'
    }
    content_type = content_types.get(ext, 'audio/mpeg')
    
    # Step 1: Initialize upload
    init_response = requests.post(
        f'{API_BASE}/v1/uploads',
        params={'projectId': project_id},
        headers=headers,
        json={
            'fileName': file_name,
            'contentType': content_type,
            'kind': 'audio',
            'parts': 1,
            'fileSize': file_size
        }
    )
    init_response.raise_for_status()
    upload = init_response.json()['upload']
    upload_id = upload['id']
    
    # Step 2: Upload file part
    with open(file_path, 'rb') as f:
        audio_data = f.read()
    
    part = upload['parts'][0]
    requests.put(part['url'], data=audio_data).raise_for_status()
    
    # Step 3: Complete upload
    requests.post(
        f'{API_BASE}/v1/uploads/{upload_id}/action',
        params={'projectId': project_id},
        headers=headers,
        json={'action': 'complete'}
    ).raise_for_status()
    
    # Step 4: Wait for processing and get assetId
    while True:
        time.sleep(3)
        status_response = requests.get(
            f'{API_BASE}/v1/uploads/{upload_id}',
            params={'projectId': project_id},
            headers={'Authorization': f'Bearer {api_key}'}
        )
        upload_status = status_response.json()['upload']
        
        if upload_status['status'] == 'imported' and upload_status.get('entityId'):
            return upload_status['entityId']
        elif upload_status['status'] == 'failed':
            raise Exception(f"Upload failed: {upload_status.get('errorMessage')}")

# Usage
asset_id = upload_audio_file('./audio.mp3', 'proj_...', 'key_...')
print(f"Audio uploaded! Asset ID: {asset_id}")

# Use with VEED Fabric
response = requests.post(
    'https://api.cloud.scenario.com/v1/inferences',
    params={'projectId': 'proj_...'},
    headers={
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    },
    json={
        'modelId': 'veed-fabric-1.0',
        'parameters': {
            'audioUrl': asset_id,
            # ... other parameters
        }
    }
)

Example 4: Generic File Upload Function (JavaScript)

const API_KEY = 'your-api-key';
const PROJECT_ID = 'your-project-id';
const API_BASE = 'https://api.cloud.scenario.com';

async function uploadFile(filePath, kind, contentType) {
  const fs = require('fs');
  const file = fs.readFileSync(filePath);
  const fileSize = fs.statSync(filePath).size;
  const fileName = filePath.split('/').pop();
  
  // Step 1: Initialize upload
  const initResponse = await fetch(
    `${API_BASE}/v1/uploads?projectId=${PROJECT_ID}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        fileName: fileName,
        contentType: contentType,
        kind: kind, // 'audio', 'video', or '3d'
        parts: 1,
        fileSize: fileSize
      })
    }
  );
  
  const { upload } = await initResponse.json();
  const uploadId = upload.id;
  
  // Step 2: Upload file part
  const part = upload.parts[0];
  await fetch(part.url, {
    method: 'PUT',
    body: file
  });
  
  // Step 3: Complete upload
  await fetch(
    `${API_BASE}/v1/uploads/${uploadId}/action?projectId=${PROJECT_ID}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        action: 'complete'
      })
    }
  );
  
  // Step 4: Wait for processing and get assetId
  let assetId = null;
  while (!assetId) {
    await new Promise(resolve => setTimeout(resolve, 3000));
    
    const statusResponse = await fetch(
      `${API_BASE}/v1/uploads/${uploadId}?projectId=${PROJECT_ID}`,
      {
        headers: {
          'Authorization': `Bearer ${API_KEY}`
        }
      }
    );
    
    const { upload: uploadStatus } = await statusResponse.json();
    
    if (uploadStatus.status === 'imported' && uploadStatus.entityId) {
      assetId = uploadStatus.entityId;
    } else if (uploadStatus.status === 'failed') {
      throw new Error(`Upload failed: ${uploadStatus.errorMessage}`);
    }
  }
  
  return assetId;
}

// Usage examples:
(async () => {
  // Upload large image (6MB+)
  const largeImageAssetId = await uploadFile('./large-image.png', 'image', 'image/png');
  console.log('Large image assetId:', largeImageAssetId);
  
  // Upload audio
  const audioAssetId = await uploadFile('./audio.mp3', 'audio', 'audio/mpeg');
  console.log('Audio assetId:', audioAssetId);
  
  // Upload video
  const videoAssetId = await uploadFile('./video.mp4', 'video', 'video/mp4');
  console.log('Video assetId:', videoAssetId);
  
  // Upload 3D model
  const modelAssetId = await uploadFile('./model.glb', '3d', 'model/gltf-binary');
  console.log('3D model assetId:', modelAssetId);
})();

Common Issues and Solutions

Issue: "Either 'image' or 'canvas' must be provided"

Problem: You're trying to upload audio, video, or 3D models to /v1/assets, which only accepts images. Or your image is too large (> 6MB).

Solution:

  • For images 6MB or larger: Use /v1/uploads with kind: "image"
  • For audio, video, or 3D models: Use /v1/uploads with the appropriate kind ("audio", "video", or "3d")

Issue: "Unhandled image format"

Problem: You're trying to upload a non-image file (audio, video, 3D model) as an image, or your image is too large.

Solution:

  • For small images (< 6MB): Use /v1/assets with base64 encoded image
  • For large images (≥ 6MB): Use /v1/uploads with kind: "image" and the correct contentType
  • For other files: Use /v1/uploads with the correct kind and contentType

Issue: "Missing required mimeType parameter for asset kind audio/video/3d"

Problem: You're trying to pass an external URL directly to a parameter that expects an assetId.

Solution: Upload the file first using /v1/uploads to get an assetId, then use that assetId in your API call.

Issue: Upload status stuck at "validating"

Problem: The file might be corrupted or in an unsupported format.

Solution:

  • Verify the file is valid
  • Check that the contentType matches the actual file format
  • Ensure the file was uploaded completely (all parts uploaded successfully)

Issue: Presigned URL expired

Problem: You waited too long between getting the presigned URLs and uploading the file.

Solution: Upload the file parts immediately after receiving the presigned URLs. If needed, re-initialize the upload to get fresh URLs.


Upload Status Values

For multipart uploads (/v1/uploads), the status progresses through these values:

  • pending - Upload initialized, waiting for parts to be uploaded
  • validating - All parts uploaded, file is being validated
  • validated - File validated successfully, processing
  • imported - Upload complete, entityId (assetId) is available
  • failed - Upload failed, check errorMessage for details

Summary

For Small Images (< 6MB):

  1. ✅ Use POST /v1/assets with base64 encoded image
  2. ✅ Include image and name in the request body
  3. ✅ Get assetId immediately from the response

For Large Images (≥ 6MB), Audio, Video, and 3D Models:

  1. ✅ Use POST /v1/uploads (not /v1/assets)
  2. ✅ Set the correct kind ("image", "audio", "video", or "3d") and matching contentType
  3. ✅ Upload file parts to presigned URLs
  4. ✅ Complete the upload with action: "complete"
  5. ✅ Poll until status: "imported" and retrieve entityId (this is your assetId)

Key Points:

  • /v1/assets is for small images only (< 6MB) - simple, single-request upload
  • /v1/uploads is for large images (≥ 6MB), audio, video, and 3D models - multipart upload process
  • Both endpoints return an assetId that can be used in other API calls
  • The assetId from either endpoint works the same way in subsequent API requests
  • For images, choose the endpoint based on file size: use /v1/assets for small files, /v1/uploads for large files

Additional Resources