15 Shorts in one YouTube upload batch: 15 timeline renders cost $1.50

YouTube's upload page says you can upload up to 15 videos at a time. Fifteen 60 second Sume Timeline renders cost $1.50; the loop and the idempotency keys.

5 min readSume
All posts

Fifteen 60 second Shorts rendered on Sume Timeline cost $1.50 in total: each render bills ceil(60 / 60) = 1 minute at $0.10. YouTube's upload help page (read 2026-10-08) says you can upload up to 15 videos at a time, so one batch of 15 is the largest single upload session. The same page lists a 100 character title limit and a 5,000 character description limit.

The arithmetic

Timeline bills per ceil output minute, so a clip that runs 61 seconds bills 2 minutes. Keep Shorts at 60 seconds or less to stay at one minute each. The table shows the batch cost at three lengths.

Cost of one batch of 15 Timeline renders at $0.10 per ceil minute (computed)
Short lengthBilled minutes eachCost eachCost for 15
30 s1$0.10$1.50
60 s1$0.10$1.50
61 s2$0.20$3.00
180 s3$0.30$4.50

Submitting 15 renders

Each render needs its own Idempotency-Key, so a retry of the same submit does not bill twice. The default mode is async: the call returns a job, and you poll GET /v1/jobs/:id/status and read GET /v1/jobs/:id/result when it is ready. This Python loop submits 15 renders from a list of already-imported media.sume.com URLs and prints the job ids.

import json, os, urllib.request

KEY = os.environ["SUME_API_KEY"]
clips = [f"https://media.sume.com/artifacts/artf_demo/clip{i}.mp4" for i in range(15)]
voice = "https://media.sume.com/artifacts/artf_demo/voice.wav"

def submit(i, url):
    body = {
        "audio": {"url": voice, "duration_seconds": 60},
        "video": [{"source_url": url, "start": 0, "duration": 60}],
    }
    req = urllib.request.Request(
        "https://api.sume.com/v1/timeline-1.0/render",
        data=json.dumps(body).encode(),
        headers={
            "Authorization": f"Bearer {KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": f"batch-short-{i:02d}",
        },
    )
    with urllib.request.urlopen(req) as r:
        return json.load(r)

for i, u in enumerate(clips):
    print(i, submit(i, u).get("request_id"))

Before you spend

Run POST /v1/timeline-1.0/plan on one of the bodies first. It is unbilled and returns billable_minutes and estimated_cost_usd_micros, so you can confirm 1 billable minute per clip. The default output is 1080x1920, the largest size a Short needs. A monthly version of this workflow is priced in the faceless Shorts post.

If you have more than 15, split them into batches of 15 to match the upload limit on the YouTube side.

Handling a failed render in the batch

With 15 jobs in flight, expect that one may fail while the others finish. The Idempotency-Key makes a retry safe: sending the same key with the same body does not create a second billed job. Poll each job at GET /v1/jobs/:id/status and read GET /v1/jobs/:id/result only for the ones in result_ready. The jobs-and-results docs describe the envelope and the stable error codes.

Keep a small table of key, job id and status as you submit. If a source URL was wrong, the API returns a stable code such as source_not_found, and the fix is to import the clip again and send a new key, because the old key is bound to the old body. Warnings such as padded or looped short sources are soft and do not fail the job, so read warnings[] before you upload and watch for any clip that was shorter than the 60 seconds you asked for.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume