Uploading With a Pre-Signed URL
When This Option Is Ideal
Use this when you have the video bytes already (on your server, in a browser upload, or on a device) and want the fastest, most reliable upload path.
The upload goes directly to object storage. Your servers never proxy the file.
The Two-Step Process
- Request upload URLs: Tell AntCDN how you want the video processed. You get back two upload URLs for the same video, both valid for one week.
- Upload bytes: Send the file to one of them.
Processing begins automatically after the upload finishes.
| Upload URL | How you send the file | If the connection drops | Use it for |
|---|---|---|---|
resumableUploadURL | In chunks, each with a Content-Range header | Ask how many bytes were stored and continue from there | Large files, slow or unreliable networks, anything that can take more than a few minutes |
signedURL | One PUT with the whole file | Start again from byte 0 | Small files on reliable networks |
Step 1: Submit Video Config
POST https://api.antcdn.net/v1/videos/upload-url
Required Parameters
| Field | Type | Description |
|---|---|---|
fileName | string | The name of the file including its extension (e.g. demo.mp4) |
allowPublicAccess | boolean | Whether playback can be accessed without signing |
Optional Parameters
| Field | Type | Default | Description |
|---|---|---|---|
videoName | string | - | Display name for the video. Useful for finding the video in the dashboard or via API. |
videoQuality | string | standard | Quality preset: standard, high, ultra. See Quality Presets & Resolutions for the renditions each one produces. |
maxEdgeLength | integer | -1 | Max short-edge resolution (e.g. 720, 1080, 2160). -1 means “auto”. |
maxFrameRate | integer | -1 | Max frame rate. -1 means “auto”. |
encodingMode | string | fast | fast or slow |
audioCodec | string | aac | aac, aac-he, aac-he-v2, mp3, ac3, eac3 |
generateCaptions | boolean | false | Whether captions should be generated automatically (if enabled for your plan) |
encryptSegments | boolean | false | AES-128 encrypt every media segment. Requires the Growth plan; playback then requires a signed session. See Encrypted Video |
uploadOrigin | string | Request’s Origin | Origin of the web page that will upload the file, e.g. https://app.example.com. Set it when a browser sends the bytes to resumableUploadURL but your server requests the upload URLs. Omit it for server-side uploads. |
Caution
encryptSegments can only be set at upload time — it cannot be enabled on an existing video.
Response body
{ "body": { "assetId": "vid_XXX", "resumableUploadURL": "https://storage.googleapis.com/upload/storage/v1/b/...&upload_id=...", "signedURL": "https://<storage-provider>/<path>?<signature>", "contentType": "video/mp4", "bucketPath": "<internal-bucket-path>", "expiresAt": 1640995200 }}| Field | Type | Description |
|---|---|---|
assetId | string | Your new video (asset) ID |
resumableUploadURL | string | Resumable upload session. Recommended for large files. |
signedURL | string | Accepts the whole file in a single PUT |
contentType | string | The Content-Type header a PUT to signedURL must send. Not used with resumableUploadURL. |
expiresAt | integer | Unix timestamp (seconds) after which neither URL accepts data. One week after the request. |
Caution
Both URLs authorize uploads to your account without an API key. Do not log them or share them beyond the client that uploads the file.
Step 2a: Resumable Upload (Recommended For Large Files)
resumableUploadURL is a Google Cloud Storage resumable upload session. You can use any HTTP client. The protocol has two kinds of request, both PUT to resumableUploadURL.
Send a chunk. Every chunk except the last must be a multiple of 256 KiB. We recommend 8–64 MiB.
PUT {resumableUploadURL}Content-Range: bytes 0-16777215/52428800
<16 MiB of the file>308means the chunk was accepted and more is expected. TheRangeheader, such asbytes=0-16777215, says what is stored. Send the next chunk from the byte after its upper value. That may be earlier than the end of what you sent.200or201means the file is complete and processing has started.
Ask how much is stored. Do this after a dropped connection, a timeout, a 5xx, or a 429, and when your process restarts.
PUT {resumableUploadURL}Content-Range: bytes */52428800Content-Length: 0308with aRangeheader means resume from the byte after its upper value.308without aRangeheader means nothing is stored yet, so start at byte 0.200or201means the upload already finished.
A 404 or 410 means the session expired. Request new upload URLs and upload again.
Tip
Save resumableUploadURL alongside the file path, for example in your database. If your uploader crashes or the machine restarts, read it back, ask how much is stored, and continue. The session stays valid until expiresAt.
Python
import osimport timeimport requests
CHUNK_BYTES = 16 * 1024 * 1024 # must be a multiple of 256 KiB
def next_offset(response): stored = response.headers.get("Range") # e.g. "bytes=0-16777215" return int(stored.split("-")[1]) + 1 if stored else 0
def stored_offset(session_url, total): r = requests.put(session_url, headers={"Content-Range": f"bytes */{total}"}, timeout=60) if r.status_code in (200, 201): return total if r.status_code == 308: return next_offset(r) r.raise_for_status() raise RuntimeError(f"unexpected status {r.status_code}")
def upload_resumable(session_url, file_path): total = os.path.getsize(file_path) offset = stored_offset(session_url, total) failures = 0 with open(file_path, "rb") as f: while offset < total: f.seek(offset) chunk = f.read(CHUNK_BYTES) end = offset + len(chunk) - 1 try: r = requests.put( session_url, data=chunk, headers={"Content-Range": f"bytes {offset}-{end}/{total}"}, timeout=300, ) except requests.RequestException: r = None
if r is not None and r.status_code in (200, 201): return if r is not None and r.status_code == 308: offset = next_offset(r) failures = 0 continue if r is not None and r.status_code in (404, 410): raise RuntimeError("upload session expired; request new upload URLs") if r is not None and r.status_code < 500 and r.status_code not in (408, 429): r.raise_for_status()
failures += 1 if failures > 10: raise RuntimeError("upload keeps failing; run again later to resume") time.sleep(min(60, 2 ** failures)) offset = stored_offset(session_url, total)Go
const chunkBytes = 16 * 1024 * 1024 // must be a multiple of 256 KiB
func nextOffset(resp *http.Response) int64 { stored := resp.Header.Get("Range") // e.g. "bytes=0-16777215" if stored == "" { return 0 } var last int64 fmt.Sscanf(stored, "bytes=0-%d", &last) return last + 1}
func storedOffset(client *http.Client, sessionURL string, total int64) (int64, error) { req, _ := http.NewRequest(http.MethodPut, sessionURL, nil) req.Header.Set("Content-Range", fmt.Sprintf("bytes */%d", total)) resp, err := client.Do(req) if err != nil { return 0, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK, http.StatusCreated: return total, nil case http.StatusPermanentRedirect: // 308 return nextOffset(resp), nil } return 0, fmt.Errorf("status query failed: %s", resp.Status)}
func uploadResumable(sessionURL, filePath string) error { file, err := os.Open(filePath) if err != nil { return err } defer file.Close() info, err := file.Stat() if err != nil { return err } total := info.Size() client := &http.Client{Timeout: 5 * time.Minute}
offset, err := storedOffset(client, sessionURL, total) if err != nil { return err } failures := 0 for offset < total { n := min(int64(chunkBytes), total-offset) req, _ := http.NewRequest(http.MethodPut, sessionURL, io.NewSectionReader(file, offset, n)) req.ContentLength = n req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+n-1, total))
resp, err := client.Do(req) if err == nil { io.Copy(io.Discard, resp.Body) resp.Body.Close() switch { case resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated: return nil case resp.StatusCode == http.StatusPermanentRedirect: offset = nextOffset(resp) failures = 0 continue case resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusGone: return fmt.Errorf("upload session expired; request new upload URLs") case resp.StatusCode < 500 && resp.StatusCode != http.StatusRequestTimeout && resp.StatusCode != http.StatusTooManyRequests: return fmt.Errorf("upload failed: %s", resp.Status) } }
failures++ if failures > 10 { return fmt.Errorf("upload keeps failing; run again later to resume") } time.Sleep(min(time.Minute, time.Duration(1<<failures)*time.Second)) if offset, err = storedOffset(client, sessionURL, total); err != nil { return err } } return nil}Browser
A resumable upload from a browser needs CORS permission for the page’s origin. That permission is fixed when the upload URLs are created:
- If the browser calls
upload-urlitself, itsOriginheader is used automatically. - If your server calls
upload-urland hands the URL to a browser, pass the browser page’s origin asuploadOrigin.
const CHUNK_BYTES = 16 * 1024 * 1024; // must be a multiple of 256 KiB
function nextOffset(res) { const stored = res.headers.get("Range"); // e.g. "bytes=0-16777215" return stored ? Number(stored.split("-")[1]) + 1 : 0;}
async function storedOffset(sessionURL, total) { const res = await fetch(sessionURL, { method: "PUT", headers: { "Content-Range": `bytes */${total}` }, }); if (res.status === 200 || res.status === 201) return total; if (res.status === 308) return nextOffset(res); throw new Error(`status query failed: ${res.status}`);}
async function uploadResumable(sessionURL, file, onProgress) { const total = file.size; let offset = await storedOffset(sessionURL, total); let failures = 0; while (offset < total) { const end = Math.min(offset + CHUNK_BYTES, total); let res = null; try { res = await fetch(sessionURL, { method: "PUT", headers: { "Content-Range": `bytes ${offset}-${end - 1}/${total}` }, body: file.slice(offset, end), }); } catch { // Network error: fall through to retry. }
if (res && (res.status === 200 || res.status === 201)) return; if (res && res.status === 308) { offset = nextOffset(res); failures = 0; onProgress?.(offset / total); continue; } if (res && (res.status === 404 || res.status === 410)) { throw new Error("Upload session expired; request new upload URLs"); } if (res && res.status < 500 && res.status !== 408 && res.status !== 429) { throw new Error(`Upload failed: ${res.status}`); }
failures++; if (failures > 10) throw new Error("Upload keeps failing; try again later to resume"); await new Promise((r) => setTimeout(r, Math.min(60_000, 1000 * 2 ** failures))); offset = await storedOffset(sessionURL, total); }}To resume after the user reloads the page, store resumableUploadURL in localStorage, keyed by the file’s name, size and lastModified. When the same file is selected again, call uploadResumable with the stored URL instead of requesting new ones.
Step 2b: Single PUT
Send the whole file in one PUT to signedURL. The request must include a Content-Type header equal to contentType from the response, or storage rejects it with 403.
file, _ := os.Open(filePath)defer file.Close()putReq, _ := http.NewRequest("PUT", uploadResp.Body.SignedURL, file)putReq.Header.Set("Content-Type", uploadResp.Body.ContentType)client.Do(putReq)The upload must finish in one request. If the connection drops, the next PUT starts again from byte 0.
Code Examples
const APIKEY = "YOUR_API_KEY"; // Created in the AntCDN dashboard
interface UploadURLRequestOptions { fileName: string; videoQuality: "standard" | "high" | "ultra"; allowPublicAccess: boolean; videoName?: string; maxEdgeLength?: 480 | 720 | 1080 | 1440 | 2160; maxFrameRate?: number; encodingMode?: "fast" | "slow"; audioCodec?: "aac" | "mp3" | "ac3" | "eac3";}
async function uploadVideo(file: File, options: UploadURLRequestOptions): Promise<string> { // Step 1: Request a pre-signed URL for direct upload const res = await fetch( `https://api.antcdn.net/v1/videos/upload-url`, { method: "POST", headers: { "X-Api-Key": APIKEY, "Content-Type": "application/json" }, body: JSON.stringify(options) } );
if (!res.ok) throw new Error(`upload-url failed: ${res.status}`); const { body } = (await res.json()) as { body: { assetId: string; signedURL: string; contentType: string }; };
// Step 2: Upload bytes directly to object storage. // Content-Type must equal the contentType returned in step 1. // For large files, use resumableUploadURL instead (see the pre-signed URL guide). const putRes = await fetch(body.signedURL, { method: "PUT", headers: { "Content-Type": body.contentType }, body: file });
if (!putRes.ok) throw new Error(`upload PUT failed: ${putRes.status}`);
// Processing starts automatically after upload completes. return body.assetId;}
// Minimal usage example (browser)const input = document.querySelector<HTMLInputElement>("#video")!;const file = input.files?.[0];if (!file) throw new Error("No file selected");
const assetId = await uploadVideo(file, { fileName: file.name, videoQuality: "high", allowPublicAccess: true});import requests
APIKEY = "YOUR_API_KEY" # Created in the AntCDN dashboard
def upload_video(file_path: str, file_name: str) -> str: # Step 1: Request a pre-signed URL for direct upload upload_response = requests.post( 'https://api.antcdn.net/v1/videos/upload-url', headers={ 'X-Api-Key': APIKEY, 'Content-Type': 'application/json' }, json={ 'fileName': file_name, 'videoQuality': 'high', 'allowPublicAccess': True } )
upload_response.raise_for_status() body = upload_response.json()['body']
# Step 2: Upload bytes directly to object storage. # Content-Type must equal the contentType returned in step 1. # For large files, use resumableUploadURL instead (see the pre-signed URL guide). with open(file_path, 'rb') as f: put_res = requests.put(body['signedURL'], data=f, headers={'Content-Type': body['contentType']}) put_res.raise_for_status()
# Processing starts automatically after upload completes. return body['assetId']package main
import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os")
const APIKEY = "YOUR_API_KEY" // Created in the AntCDN dashboard
type UploadURLRequest struct { FileName string `json:"fileName"` VideoQuality string `json:"videoQuality"` AllowPublicAccess bool `json:"allowPublicAccess"`}
type UploadURLResponse struct { Body struct { SignedURL string `json:"signedURL"` ContentType string `json:"contentType"` AssetId string `json:"assetId"` } `json:"body"`}
func uploadVideo(filePath, fileName string) (string, error) { // Step 1: Request a pre-signed URL for direct upload reqBody, _ := json.Marshal(UploadURLRequest{ FileName: fileName, VideoQuality: "high", AllowPublicAccess: true, })
req, _ := http.NewRequest("POST", "https://api.antcdn.net/v1/videos/upload-url", bytes.NewBuffer(reqBody)) req.Header.Set("X-Api-Key", APIKEY) req.Header.Set("Content-Type", "application/json")
client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close()
var uploadResp UploadURLResponse if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil { return "", err }
// Step 2: Upload bytes directly to object storage file, err := os.Open(filePath) if err != nil { return "", err } defer file.Close()
// Content-Type must equal the contentType returned in step 1. // For large files, use resumableUploadURL instead (see the pre-signed URL guide). putReq, _ := http.NewRequest("PUT", uploadResp.Body.SignedURL, file) putReq.Header.Set("Content-Type", uploadResp.Body.ContentType) putResp, err := client.Do(putReq) if err != nil { return "", err } defer putResp.Body.Close() io.Copy(io.Discard, putResp.Body) if putResp.StatusCode < 200 || putResp.StatusCode >= 300 { return "", fmt.Errorf("upload PUT failed: %s", putResp.Status) }
// Processing starts automatically after upload completes. return uploadResp.Body.AssetId, nil}const APIKEY = "YOUR_API_KEY"; // Created in the AntCDN dashboard
interface UploadVideoOptions { // Common options fileName: string; allowPublicAccess: boolean; videoQuality?: "standard" | "high" | "ultra";
// Optional videoName?: string; maxEdgeLength?: 480 | 720 | 1080 | 1440 | 2160; maxFrameRate?: number; encodingMode?: "fast" | "slow"; audioCodec?: "aac" | "aac-he" | "aac-he-v2" | "mp3" | "ac3" | "eac3";}
async function uploadVideo(file: File, options: UploadVideoOptions): Promise<string> { // Step 1: Request a pre-signed URL for direct upload const uploadResponse = await fetch( `https://api.antcdn.net/v1/videos/upload-url`, { method: "POST", headers: { "X-Api-Key": APIKEY, "Content-Type": "application/json" }, body: JSON.stringify(options) } );
if (!uploadResponse.ok) throw new Error(`upload-url failed: ${uploadResponse.status}`); const { body } = (await uploadResponse.json()) as { body: { assetId: string; signedURL: string; contentType: string; resumableUploadURL: string; bucketPath: string; expiresAt: number; }; };
// Step 2: Upload bytes directly to object storage. // Content-Type must equal the contentType returned in step 1. // For large files, use resumableUploadURL instead (see the pre-signed URL guide). const putRes = await fetch(body.signedURL, { method: "PUT", headers: { "Content-Type": body.contentType }, body: file });
if (!putRes.ok) throw new Error(`upload PUT failed: ${putRes.status}`);
// Processing starts automatically after upload completes. return body.assetId;}
// Example usage with all optionsconst file = new File([/* bytes */], "video.mp4", { type: "video/mp4" });const assetId = await uploadVideo(file, { fileName: file.name, allowPublicAccess: true, videoQuality: "ultra", videoName: "My Firat Video", maxEdgeLength: 1080, maxFrameRate: 60, encodingMode: "slow", audioCodec: "aac"});import requestsfrom typing import Optional, Literal
APIKEY = "YOUR_API_KEY" # Created in the AntCDN dashboard
def upload_video( file_path: str, file_name: str, allow_public_access: bool, video_quality: Literal["standard", "high", "ultra"] = "standard", # Optional parameters video_name: Optional[str] = None, max_edge_length: Optional[int] = None, max_frame_rate: Optional[int] = None, encoding_mode: Optional[Literal["fast", "slow"]] = None, audio_codec: Optional[Literal["aac", "aac-he", "aac-he-v2", "mp3", "ac3", "eac3"]] = None) -> str: # Step 1: Build request body with all options request_body = { 'fileName': file_name, 'allowPublicAccess': allow_public_access, 'videoQuality': video_quality, }
# Add optional parameters if provided if video_name: request_body['videoName'] = video_name if max_edge_length: request_body['maxEdgeLength'] = max_edge_length if max_frame_rate: request_body['maxFrameRate'] = max_frame_rate if encoding_mode: request_body['encodingMode'] = encoding_mode if audio_codec: request_body['audioCodec'] = audio_codec
# Step 2: Request a pre-signed URL for direct upload upload_response = requests.post( 'https://api.antcdn.net/v1/videos/upload-url', headers={ 'X-Api-Key': APIKEY, 'Content-Type': 'application/json' }, json=request_body )
upload_response.raise_for_status() body = upload_response.json()['body']
# Step 3: Upload bytes directly to object storage # Content-Type must equal the contentType returned in the previous step. # For large files, use resumableUploadURL instead (see the pre-signed URL guide). with open(file_path, 'rb') as f: put_res = requests.put(body['signedURL'], data=f, headers={'Content-Type': body['contentType']}) put_res.raise_for_status()
# Processing starts automatically after upload completes. return body['assetId']
# Example usage with all optionsvideo_id = upload_video( file_path="/path/to/video.mp4", file_name="video.mp4", allow_public_access=False, video_quality="ultra", video_name="Product Demo 2024", max_edge_length=1080, max_frame_rate=60, encoding_mode="slow", audio_codec="aac")package main
import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os")
const APIKEY = "YOUR_API_KEY" // Created in the AntCDN dashboard
type UploadURLRequest struct { FileName string `json:"fileName"` AllowPublicAccess bool `json:"allowPublicAccess"` VideoQuality string `json:"videoQuality,omitempty"`
// Optional VideoName *string `json:"videoName,omitempty"` MaxEdgeLength *int `json:"maxEdgeLength,omitempty"` MaxFrameRate *int `json:"maxFrameRate,omitempty"` EncodingMode *string `json:"encodingMode,omitempty"` AudioCodec *string `json:"audioCodec,omitempty"`}
type UploadURLResponse struct { Body struct { SignedURL string `json:"signedURL"` ContentType string `json:"contentType"` AssetId string `json:"assetId"` } `json:"body"`}
func uploadVideo(filePath string, req UploadURLRequest) (string, error) { // Step 1: Request a pre-signed URL for direct upload reqBody, _ := json.Marshal(req)
httpReq, _ := http.NewRequest("POST", "https://api.antcdn.net/v1/videos/upload-url", bytes.NewBuffer(reqBody)) httpReq.Header.Set("X-Api-Key", APIKEY) httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{} resp, err := client.Do(httpReq) if err != nil { return "", err } defer resp.Body.Close()
var uploadResp UploadURLResponse json.NewDecoder(resp.Body).Decode(&uploadResp)
// Step 2: Upload bytes directly to object storage file, _ := os.Open(filePath) defer file.Close()
// Content-Type must equal the contentType returned in step 1. // For large files, use resumableUploadURL instead (see the pre-signed URL guide). putReq, _ := http.NewRequest("PUT", uploadResp.Body.SignedURL, file) putReq.Header.Set("Content-Type", uploadResp.Body.ContentType) putResp, err := client.Do(putReq) if err != nil { return "", err } defer putResp.Body.Close() io.Copy(io.Discard, putResp.Body) if putResp.StatusCode < 200 || putResp.StatusCode >= 300 { return "", fmt.Errorf("upload PUT failed: %s", putResp.Status) }
// Processing starts automatically after upload completes. return uploadResp.Body.AssetId, nil}
// Example usage with all optionsfunc main() { videoName := "Product Demo 2024" maxEdge := 1080 frameRate := 60 encoding := "slow" codec := "aac"
assetId, _ := uploadVideo("/path/to/video.mp4", UploadURLRequest{ FileName: "video.mp4", AllowPublicAccess: false, VideoQuality: "ultra", VideoName: &videoName, MaxEdgeLength: &maxEdge, MaxFrameRate: &frameRate, EncodingMode: &encoding, AudioCodec: &codec, })
fmt.Println("Video ID:", assetId)}How To Play The Video You Just Uploaded
After you upload your video, it will begin processing. Once it’s ready, create (or use) an Edge Key and embed it:
<!-- Replace {edgeKey} with an Edge Key --><iframe src="https://player.antcdn.net/v1/{edgeKey}/master.m3u8?autoplay=true&muted=true" frameborder="0" allowfullscreen></iframe>Check out our “Video Player” docs for more information.
Bonus: Upload Progress Tracking
The resumable examples above report progress after each chunk. For a single PUT from a browser, XMLHttpRequest reports progress while the file is sent:
const uploadWithProgress = async (file, signedURL, contentType) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log(`Upload progress: ${percentComplete.toFixed(2)}%`); } });
xhr.addEventListener('load', () => { if (xhr.status === 200) { resolve('Upload complete'); } else { reject(new Error('Upload failed')); } });
xhr.open('PUT', signedURL); xhr.setRequestHeader('Content-Type', contentType); xhr.send(file); });};