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.

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.
| Episode length | Billable minutes each | 15 episodes | 30 episodes |
|---|---|---|---|
| 30 s | 1 | $1.50 | $3.00 |
| 59 s | 1 | $1.50 | $3.00 |
| 75 s | 2 | $3.00 | $6.00 |
| 170 s | 3 | $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
- YouTube .sbv vs .srt: the timestamp difference and a Python converter
YouTube accepts .sbv and .srt as basic caption files. They differ in time format and numbering. A short Python converter turns timed cues into either file.
- YouTube Shorts export settings: 1080x1920 and fps in a Timeline render
A Sume Timeline render defaults to 1080x1920, a vertical Shorts frame. You can set fps to 24, 25, 30 or 60 and width/height to even values from 256 to 2160.
- YouTube Shorts originality checklist: run it before you upload
A pre-upload checklist from YouTube's monetization pages, plus a Python preflight that checks length and orientation from a Sume video inspect probe.
- Can an AI-made Short be Creative Commons on YouTube?
YouTube's status.license takes youtube or creativeCommon, but a Content ID claim blocks CC BY. What to check before you set it on an AI-made Short.
Written by Sume