Docs

Video upload

Upload videos to your storage and optionally prepare them for streaming.

Upload an original video directly to storage, then complete the Upload to create its Source. You can also prepare a video asset automatically for streaming.

Your server creates an Upload session with the SDK. The file is transferred to the selected storage using the session's signed URLs. Completion verifies the uploaded file and creates the Source that references it.

Run SDK calls on your server with a Project access token. Keep that token out of browser code. Replace the example paths and metadata with your own values.

Use the SDK procedures below for server uploads. For uploads from your application's frontend, use Chunkify Uploader.

If your video is already hosted at a URL or in your storage, you can also create a Source from the existing file.

Choose where to upload

The Upload request's storage field selects the original file's destination. Omit storage.id to use the Project default, or set another storage configuration's ID from the same Project.

Destination for the original What to pass
Your storage Set storage.path to a unique object key including the filename, such as incoming/video-123.mp4.
Chunkify temporary storage Omit storage.path. Chunkify chooses the key.

This is the original video's upload destination. If you request a video asset, its generated files use the Project's default storage saved when the Upload session is created.

For your storage, storage.path is required even when it is the Project default. Chunkify uses this exact key without adding the output base_prefix. Uploading to an existing key can overwrite that file.

With an initialized SDK client, this creates a single-PUT session that uploads the original to incoming/video-123.mp4 in your Project's default storage:

const upload = await client.uploads.create({
    upload_mode: 'single',
    storage: {
        path: 'incoming/video-123.mp4',
    },
});

The storage credentials must allow reads and writes of the original. Multipart uploads also need part listing, completion, and abort permissions. Follow your provider's storage setup guide.

Originals uploaded to Chunkify temporary storage are eligible for deletion after 24 hours. With your storage, you control retention. Keep the original available for processing and later outputs that need it.

Choose what to create

Completion always creates a Source. Set create_asset: true to also prepare a video asset automatically. Omit it or set it to false when you only need the Source, such as for a separate transcoding Job.

Chunkify copies Upload metadata to the Source and any automatically created video asset. Include every metadata value required by the Project's video asset path template. Missing or empty values fail session creation.

Add create_asset: true to the same session creation request:

const upload = await client.uploads.create({
    upload_mode: 'single',
    create_asset: true,
    storage: {
        path: 'incoming/video-123.mp4',
    },
});

The video asset uses the Project's output storage, video settings, and default Workflow saved when the Upload session is created. The Upload's storage field selects only where the original file goes.

With create_asset: true, set workflow_id to another Workflow's ID to override the default, or set it to null to disable automatic actions. See Workflows.

Both upload examples below use create_asset: true and include metadata.VideoID. They upload the original to your storage configured as the Project default. For Chunkify temporary storage, omit storage.

Choose the upload mode

Mode How the file is transferred file_size_bytes
single The whole file in one signed PUT request. Optional
multipart File slices uploaded to separate signed part URLs. Required
auto or omitted Single PUT at or below 100 MiB; multipart above 100 MiB. Required

file_size_bytes must be the file's exact size in bytes. Chunkify checks the team's Source size limit. Single PUT supports up to 5 GiB, subject to the provider's limit; multipart supports up to 5 TiB.

AWS S3 and Cloudflare R2 support multipart uploads. Verify other providers' support or choose single. A multipart setup error does not trigger an automatic switch to single PUT.

The following procedures select each mode explicitly. If you use auto, follow the flow indicated by the returned upload_mode. For browser uploads, Chunkify Uploader handles both modes.

Upload with a single PUT

The server example performs the whole flow:

  1. Create a session with upload_mode: "single".
  2. Send the whole file to upload_url with one PUT request.
  3. Complete the Upload through the SDK using the token from completion_url.

The storage.path below is where the original file will be uploaded. The generated video asset files use the Project's saved output storage.

The TypeScript example runs in Node.js. The PHP transfer uses Symfony HTTP Client; install it with composer require symfony/http-client if needed.

import { openAsBlob } from 'node:fs';
import Chunkify from '@chunkify/chunkify';

const client = new Chunkify({
    projectAccessToken: 'My Project Access Token',
});
const file = await openAsBlob('video.mp4', { type: 'video/mp4' });

const upload = await client.uploads.create({
    upload_mode: 'single',
    create_asset: true,
    storage: {
        path: 'incoming/video-123.mp4',
    },
    metadata: {
        VideoID: 'video-123',
    },
});
if (!upload.upload_url || !upload.completion_url) {
    throw new Error('Missing upload session URLs');
}

// Transfer the file directly to storage.
const response = await fetch(upload.upload_url, {
    method: 'PUT',
    body: file,
    headers: {
        'Content-Type': 'video/mp4',
    },
});
if (!response.ok) {
    throw new Error(`File transfer failed: ${response.status}`);
}

const token = new URL(upload.completion_url).pathname.split('/').pop();
if (!token) throw new Error('Missing completion token');
await client.uploads.complete(token);

console.log('Upload completed');

Completion returns 204 No Content, with no JSON body. Chunkify creates the Source and starts preparing the requested video asset. The video asset becomes playable separately; see After the upload.

Upload in multiple parts

Multipart uploads have a different transfer and completion sequence:

  1. Create a session with upload_mode: "multipart" and the exact file size.
  2. Upload each file slice to its signed URL and save the returned ETag.
  3. Complete the Upload with all part numbers and ETags in ascending order.

Create the multipart session

Create the multipart Upload session on your server. Provide the file's exact size, a unique destination key for the original file, and your application's video ID.

import Chunkify from '@chunkify/chunkify';

export async function createMultipartUploadSession(
    fileSizeBytes: number,
    objectKey: string,
    videoId: string,
) {
    const client = new Chunkify({
        projectAccessToken: 'My Project Access Token',
    });

    return client.uploads.create({
        upload_mode: 'multipart',
        file_size_bytes: fileSizeBytes,
        create_asset: true,
        storage: {
            path: objectKey,
        },
        metadata: {
            VideoID: videoId,
        },
    });
}

Save the returned Upload ID to correlate later events with your video. The response contains completion_url and multipart. Its signed URLs are returned only at creation and are absent from later reads, lists, and webhook payloads.

For a 100 MB file, or 100,000,000 bytes, the Upload object returned by the SDK contains these transfer fields. Other fields are omitted, and the signed URLs are abbreviated:

{
  "id": "upl_2G6MJiNz71bHQGNzGwKx5cJwPFS",
  "upload_mode": "multipart",
  "file_size_bytes": 100000000,
  "expires_at": "2026-01-01T14:00:00Z",
  "completion_url": "https://api.chunkify.dev/v1/uploads/completion/EXAMPLE_TOKEN",
  "multipart": {
    "part_size_bytes": 33554432,
    "parts": [
      {
        "part_number": 1,
        "upload_url": "https://storage.example.com/incoming/video-123.mp4?partNumber=1&uploadId=EXAMPLE&SIGNED_QUERY"
      },
      {
        "part_number": 2,
        "upload_url": "https://storage.example.com/incoming/video-123.mp4?partNumber=2&uploadId=EXAMPLE&SIGNED_QUERY"
      },
      {
        "part_number": 3,
        "upload_url": "https://storage.example.com/incoming/video-123.mp4?partNumber=3&uploadId=EXAMPLE&SIGNED_QUERY"
      }
    ],
    "expires_at": "2026-01-01T14:00:00Z"
  }
}

Upload the parts

Use the returned multipart fields to divide the file:

Field Purpose
part_size_bytes The size of each slice except the final slice.
parts Every signed upload_url and its one-based part_number, in ascending order.
expires_at The signed part URLs' expiration time.

Each slice starts at (part_number - 1) * part_size_bytes. The final slice contains the remaining bytes. Send each slice with PUT to its matching URL, check for success, and save the response's exact ETag, including quotation marks.

In this example, parts 1 and 2 each contain 33,554,432 bytes (32 MiB). Part 3 contains the remaining 32,891,136 bytes. Each part goes to its own upload_url.

Repeat for every part. A failed part can be retried with the same URL and bytes before expiry. Successful parts do not need to be sent again. A multipart session has no top-level upload_url.

The following server helpers upload one part and return its ETag. For the first part, pass the first entry's upload_url and part_number from multipart.parts, along with multipart.part_size_bytes.

import { openAsBlob } from 'node:fs';

export async function uploadPart(
    filePath: string,
    uploadUrl: string,
    partNumber: number,
    partSizeBytes: number,
): Promise<string> {
    const file = await openAsBlob(filePath);
    const start = (partNumber - 1) * partSizeBytes;
    const end = Math.min(start + partSizeBytes, file.size);
    if (start >= file.size) throw new Error('Part starts beyond the file');

    const response = await fetch(uploadUrl, {
        method: 'PUT',
        body: file.slice(start, end),
        headers: {
            'Content-Type': 'application/octet-stream',
        },
    });
    if (!response.ok) {
        throw new Error(`Part upload failed: ${response.status}`);
    }

    const etag = response.headers.get('ETag');
    if (!etag) throw new Error('Storage did not return an ETag');
    return etag;
}

Pair the returned ETag with that part's part_number. Repeat for all parts and pass the complete list to the completion step below. Preserve the ETag's quotation marks.

Complete the multipart session

After every part succeeds, pass the full part list to the SDK. Each entry has part_number and the exact storage etag. For the 100 MB example above, include all three parts:

The ETags below are placeholders. Replace each with the ETag response header returned by storage for that part's successful PUT. The uploadPart helper above returns this value. Preserve its quotation marks.

{
  "parts": [
    {
      "part_number": 1,
      "etag": "\"etag-for-part-1\""
    },
    {
      "part_number": 2,
      "etag": "\"etag-for-part-2\""
    },
    {
      "part_number": 3,
      "etag": "\"etag-for-part-3\""
    }
  ]
}

These server helpers accept completion_url and that part list. They extract the token from the URL's last path segment and call the SDK's completion method.

import Chunkify from '@chunkify/chunkify';

type CompletedPart = {
    part_number: number;
    etag: string;
};

export async function completeMultipartUpload(
    completionUrl: string,
    parts: CompletedPart[],
) {
    const client = new Chunkify({
        projectAccessToken: 'My Project Access Token',
    });
    const token = new URL(completionUrl).pathname.split('/').pop();
    if (!token) throw new Error('Missing completion token');

    await client.uploads.complete(token, {
        parts,
    });
}

Chunkify checks the uploaded parts and assembled file, creates the Source, and starts preparing the requested video asset. Successful completion returns 204 No Content.

Upload from a browser

Use Chunkify Uploader to handle file selection, single PUT or multipart transfer, progress, retries, and completion.

Your server authorizes the upload and creates its session with file_size_bytes from the selected file's size. Use auto or omit upload_mode to let Chunkify choose the transfer mode.

Return only the session fields the component needs. Keep the Project token and storage credentials on your server:

Returned mode Session fields
single upload_mode, upload_url, completion_url, expires_at
multipart upload_mode, completion_url, expires_at, multipart

Configure bucket CORS to allow your website's origin, PUT, and the Content-Type request header. Include https://chunkify.dev for uploads through the Chunkify dashboard.

Expose the storage response's ETag header for multipart uploads. Expose Retry-After too if you want the uploader to honor storage retry delays when that header is returned.

If your site uses a Content Security Policy, allow https://api.chunkify.dev and the storage endpoint in connect-src. See Uploader setup for browser integration examples.

After the upload

The upload.completed event confirms Source creation. Its payload contains data.source and data.upload. If you requested a video asset, data.upload.asset_id identifies it.

Upload completion confirms that the original file is available. The video asset is processed afterward. Handle asset.playback_ready to receive its playback URLs when streaming becomes available.

Also handle upload.failed, upload.expired, and asset.failed as appropriate for your application. See Webhooks for setup and payload details.

Retries and expiration

Retry a network failure, 429, or 5xx completion response with bounded attempts and increasing delays before expires_at. Reuse the same completion URL. For multipart, resend the same complete part list.

A valid completion retry does not create duplicate Sources or video assets. For other errors, inspect the response. A failed or expired Upload needs a new session.

A video asset output folder conflict returns 409. Correct the Project's path settings or the Upload metadata before creating another session. See Output paths.

Both transfer and completion must finish before expiry. Single PUT defaults to 2 hours; multipart uses a file-size-based default between 2 and 48 hours. Use validity_timeout to adjust the deadline within the API limits.

Storage credentials and bucket policies can shorten signed URL validity. The Upload API cannot refresh or reissue part URLs. Keep the URLs and returned ETags until completion, and create a new session after expiry or if its URLs are lost.

Deleting or expiring an Upload does not abort its incomplete multipart parts. Configure your bucket's lifecycle rules to remove abandoned multipart uploads.

Use a file that is already online

If the original video is already accessible, you can register it as a Source using its URL or storage path. These methods reference the existing file without uploading or copying it.

Use a hosted URL

Create a Source that references an existing file at a direct HTTP or HTTPS URL. Chunkify must be able to read the file:

import Chunkify from '@chunkify/chunkify';

const client = new Chunkify({
    projectAccessToken: 'My Project Access Token',
});

const source = await client.sources.create({
    url: 'https://media.example.com/video.mp4',
});

console.log(source.id);

Keep the original accessible while Chunkify processes it and for later outputs that need it. Creating the Source registers the file without copying it. See Source videos.

Use a file in your storage

Add your storage configuration to Chunkify, then create a Source using the existing file's exact object key. The file must already be present at that path.

These examples read sources/video.mp4 from your Project's default storage configuration:

import Chunkify from '@chunkify/chunkify';

const client = new Chunkify({
    projectAccessToken: 'My Project Access Token',
});

const source = await client.sources.create({
    storage: {
        path: 'sources/video.mp4',
    },
});

console.log(source.id);

Set storage.id to another storage configuration's ID to read the original from there. It must belong to the same Project and allow reads of the file. This input method requires your storage; Chunkify temporary storage cannot be used.

Here, storage.path identifies the existing original. It includes the filename, and the storage configuration's output base_prefix is not added. Provide either url or storage when creating a Source.

The file stays in its bucket. This request creates a Source record, and no Upload session is needed. Changing the Project default later does not change the Source's saved file location.

Use the Source ID to create a video asset or create a transcoding.