YouTube takes 15 Shorts per upload: batch-render 30 episodes

YouTube's help says you can upload up to 15 Shorts at a time. Render a 30-episode series in two groups of 15 with Sume Timeline, idempotent, for $3.00.

1 min readSume
All posts

YouTube's upload page for Shorts says you can upload up to 15 short videos at a time and finish each in an edit-draft step. If you ship a series, that sets the natural batch size: render 15 files, upload them together, render the next 15. With Sume's Timeline you can submit a group as 15 jobs with stable idempotency keys, so a retry never bills twice.

The 15 limit is from Upload YouTube Shorts (read 2026-10-03), which also gives the shape and length rules: up to 3 minutes, square or vertical. The page describes the upload form, not an API, so the grouping here is a workflow choice, not something YouTube requires.

The cost of a series

Timeline 1.0 bills $0.10 per started output minute. Every 30-second episode is one billable minute, so 30 episodes cost $3.00, and two groups of 15 cost $1.50 each. Episodes that run 61 seconds or more bill two minutes, so if you are close, 59 seconds saves half the money.

Batch cost at $0.10 per billable minute, from the Sume Timeline docs; 15-per-upload limit per YouTube (read 2026-10-03)
Episode lengthBillable minutes each15 episodes30 episodes
30 s1$1.50$3.00
59 s1$1.50$3.00
75 s2$3.00$6.00
170 s3$4.50$9.00

Submit one group

Render requires an Idempotency-Key. Derive it from the series and episode number, so a rerun of the script returns the same job instead of a new one. The default mode is async, so each call returns a job and the loop does not wait.

import json, os, time, urllib.request as u

def call(method, path, body=None, key=None):
    h = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
         "Content-Type": "application/json"}
    if key:
        h["Idempotency-Key"] = key
    data = json.dumps(body).encode() if body is not None else None
    req = u.Request("https://api.sume.com" + path, data, h, method=method)
    with u.urlopen(req) as r:
        out = json.load(r)
    return out.get("data", out)

SERIES = "s1"
FIRST, LAST = 1, 15  # next group: 16, 30
for n in range(FIRST, LAST + 1):
    url = "https://media.sume.com/artifacts/artf_demo/ep%02d.mp4" % n
    body = {"audio": {"mode": "silence", "duration_seconds": 30},
            "video": [{"source_url": url, "start": 0, "duration": 30}]}
    job = call("POST", "/v1/timeline-1.0/render", body, f"{SERIES}-ep{n:02d}")
    print(n, job.get("id"))

Next group

For the second group set FIRST, LAST to 16 and 30. SERIES stays the same and the episode number is in each key, so keys never collide. The silence audio mode here is for clips that carry no spine of their own; for episodes with narration, pass the voice file in audio.url and its length as duration_seconds. Then poll GET /v1/jobs/:id/status for each job id before you upload the group.

Keep a manifest

Write the series, episode number and job id to a file as each submit returns. If the script dies at episode 9, rerunning it replays episodes 1 to 8 as the same jobs because the keys match, then submits 9 onward. Nothing bills twice, and the manifest tells you which files are ready to drag into the upload form.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume