5. Big files (> 100 MB)

POST /jobs/upload sends the file through the CDN, which caps request bodies at ~100 MB. Anything larger goes straight to storage first, and then you submit the job by URL instead of by file — two steps instead of one:

# 1. get the file into storage (one of the two ways below) → you get a `url`
# 2. submit it by URL:
curl -s https://mp.dvocorp.com/api/v1/jobs/api \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"service_slug":"clipconvert","operation_key":"compress",
       "params":{"file_url":"<url from step 1>","level":"medium"}}'

Then poll GET /jobs/{id} exactly as before.

Both upload methods return { url, name, content_type, kind }; url is what goes in params.file_url.

Option A — single presigned PUT (simplest):

curl -s https://mp.dvocorp.com/api/v1/uploads/presign \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"filename":"clip.mp4","content_type":"video/mp4"}'
# -> {"put_url":"https://...signed...","url":"https://.../clip.mp4", ...}

curl -X PUT "<put_url>" -H 'Content-Type: video/mp4' --data-binary @clip.mp4

Option B — multipart (adds per-part retry and parallelism; what the web UI uses). Hard cap: MAX_UPLOAD_GB, default 3 GB. Part size 64 MB, up to 10 000 parts.

# 1. create
curl -s https://mp.dvocorp.com/api/v1/uploads/multipart/create \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"filename":"movie.mp4","size":2147483648,"content_type":"video/mp4"}'
# -> {"key":"tmp/<token>/movie.mp4","upload_id":"...","part_size":67108864}

# 2. sign one URL per part (1..10000)
curl -s https://mp.dvocorp.com/api/v1/uploads/multipart/sign-part \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"key":"tmp/<token>/movie.mp4","upload_id":"...","part_number":1}'

# 3. PUT each slice and KEEP the ETag response header
curl -si -X PUT "<url>" --data-binary @part-0001.bin | grep -i etag

# 4. complete
curl -s https://mp.dvocorp.com/api/v1/uploads/multipart/complete \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"key":"tmp/<token>/movie.mp4","upload_id":"...","content_type":"video/mp4",
       "parts":[{"part_number":1,"etag":"\"<etag1>\""}]}'

# on failure — free the stored parts (204)
curl -s -X POST https://mp.dvocorp.com/api/v1/uploads/multipart/abort \
  -H 'X-Api-Key: ca_live_...' -H 'Content-Type: application/json' \
  -d '{"key":"tmp/<token>/movie.mp4","upload_id":"..."}'

Both presigned modes need S3/R2 storage. On local-disk deployments multipart/create returns 501 — fall back to POST /uploads.

R2 bucket CORS (needed for browser uploads): allow PUT and GET from your site origins and expose ETagAllowedMethods: ["PUT","GET"], AllowedHeaders: ["*"], ExposeHeaders: ["ETag"]. Without ExposeHeaders the browser cannot read each part's ETag and the upload aborts.