Bulk Create Jobs
curl --request POST \
--url https://api.tornadoapi.io/jobs/bulk \
--header 'Content-Type: application/json' \
--header 'x-api-key: <x-api-key>' \
--data '
{
"jobs": [
{}
],
"jobs[].url": "<string>",
"jobs[].filename": "<string>",
"folder": "<string>",
"format": "<string>",
"video_codec": "<string>",
"audio_codec": "<string>",
"audio_bitrate": "<string>",
"video_quality": 123,
"audio_only": true,
"download_subtitles": true,
"download_thumbnail": true,
"quality_preset": "<string>",
"max_resolution": "<string>",
"clip_start": "<string>",
"clip_end": "<string>",
"live_recording": true,
"live_from_start": true,
"max_duration": 123,
"wait_for_video": true,
"webhook_url": "<string>"
}
'import requests
url = "https://api.tornadoapi.io/jobs/bulk"
payload = {
"jobs": [{}],
"jobs[].url": "<string>",
"jobs[].filename": "<string>",
"folder": "<string>",
"format": "<string>",
"video_codec": "<string>",
"audio_codec": "<string>",
"audio_bitrate": "<string>",
"video_quality": 123,
"audio_only": True,
"download_subtitles": True,
"download_thumbnail": True,
"quality_preset": "<string>",
"max_resolution": "<string>",
"clip_start": "<string>",
"clip_end": "<string>",
"live_recording": True,
"live_from_start": True,
"max_duration": 123,
"wait_for_video": True,
"webhook_url": "<string>"
}
headers = {
"x-api-key": "<x-api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<x-api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
jobs: [{}],
'jobs[].url': '<string>',
'jobs[].filename': '<string>',
folder: '<string>',
format: '<string>',
video_codec: '<string>',
audio_codec: '<string>',
audio_bitrate: '<string>',
video_quality: 123,
audio_only: true,
download_subtitles: true,
download_thumbnail: true,
quality_preset: '<string>',
max_resolution: '<string>',
clip_start: '<string>',
clip_end: '<string>',
live_recording: true,
live_from_start: true,
max_duration: 123,
wait_for_video: true,
webhook_url: '<string>'
})
};
fetch('https://api.tornadoapi.io/jobs/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tornadoapi.io/jobs/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'jobs' => [
[
]
],
'jobs[].url' => '<string>',
'jobs[].filename' => '<string>',
'folder' => '<string>',
'format' => '<string>',
'video_codec' => '<string>',
'audio_codec' => '<string>',
'audio_bitrate' => '<string>',
'video_quality' => 123,
'audio_only' => true,
'download_subtitles' => true,
'download_thumbnail' => true,
'quality_preset' => '<string>',
'max_resolution' => '<string>',
'clip_start' => '<string>',
'clip_end' => '<string>',
'live_recording' => true,
'live_from_start' => true,
'max_duration' => 123,
'wait_for_video' => true,
'webhook_url' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <x-api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tornadoapi.io/jobs/bulk"
payload := strings.NewReader("{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<x-api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tornadoapi.io/jobs/bulk")
.header("x-api-key", "<x-api-key>")
.header("Content-Type", "application/json")
.body("{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tornadoapi.io/jobs/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<x-api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"batch_id": "550e8400-e29b-41d4-a716-446655440001",
"total_jobs": 3,
"job_ids": [
"660f9511-f30c-52e5-b827-557766551111",
"660f9511-f30c-52e5-b827-557766551112",
"660f9511-f30c-52e5-b827-557766551113"
]
}
Bulk
Bulk Create Jobs
Create multiple download jobs at once
POST
/
jobs
/
bulk
Bulk Create Jobs
curl --request POST \
--url https://api.tornadoapi.io/jobs/bulk \
--header 'Content-Type: application/json' \
--header 'x-api-key: <x-api-key>' \
--data '
{
"jobs": [
{}
],
"jobs[].url": "<string>",
"jobs[].filename": "<string>",
"folder": "<string>",
"format": "<string>",
"video_codec": "<string>",
"audio_codec": "<string>",
"audio_bitrate": "<string>",
"video_quality": 123,
"audio_only": true,
"download_subtitles": true,
"download_thumbnail": true,
"quality_preset": "<string>",
"max_resolution": "<string>",
"clip_start": "<string>",
"clip_end": "<string>",
"live_recording": true,
"live_from_start": true,
"max_duration": 123,
"wait_for_video": true,
"webhook_url": "<string>"
}
'import requests
url = "https://api.tornadoapi.io/jobs/bulk"
payload = {
"jobs": [{}],
"jobs[].url": "<string>",
"jobs[].filename": "<string>",
"folder": "<string>",
"format": "<string>",
"video_codec": "<string>",
"audio_codec": "<string>",
"audio_bitrate": "<string>",
"video_quality": 123,
"audio_only": True,
"download_subtitles": True,
"download_thumbnail": True,
"quality_preset": "<string>",
"max_resolution": "<string>",
"clip_start": "<string>",
"clip_end": "<string>",
"live_recording": True,
"live_from_start": True,
"max_duration": 123,
"wait_for_video": True,
"webhook_url": "<string>"
}
headers = {
"x-api-key": "<x-api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<x-api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
jobs: [{}],
'jobs[].url': '<string>',
'jobs[].filename': '<string>',
folder: '<string>',
format: '<string>',
video_codec: '<string>',
audio_codec: '<string>',
audio_bitrate: '<string>',
video_quality: 123,
audio_only: true,
download_subtitles: true,
download_thumbnail: true,
quality_preset: '<string>',
max_resolution: '<string>',
clip_start: '<string>',
clip_end: '<string>',
live_recording: true,
live_from_start: true,
max_duration: 123,
wait_for_video: true,
webhook_url: '<string>'
})
};
fetch('https://api.tornadoapi.io/jobs/bulk', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tornadoapi.io/jobs/bulk",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'jobs' => [
[
]
],
'jobs[].url' => '<string>',
'jobs[].filename' => '<string>',
'folder' => '<string>',
'format' => '<string>',
'video_codec' => '<string>',
'audio_codec' => '<string>',
'audio_bitrate' => '<string>',
'video_quality' => 123,
'audio_only' => true,
'download_subtitles' => true,
'download_thumbnail' => true,
'quality_preset' => '<string>',
'max_resolution' => '<string>',
'clip_start' => '<string>',
'clip_end' => '<string>',
'live_recording' => true,
'live_from_start' => true,
'max_duration' => 123,
'wait_for_video' => true,
'webhook_url' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <x-api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tornadoapi.io/jobs/bulk"
payload := strings.NewReader("{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<x-api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tornadoapi.io/jobs/bulk")
.header("x-api-key", "<x-api-key>")
.header("Content-Type", "application/json")
.body("{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tornadoapi.io/jobs/bulk")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<x-api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"jobs\": [\n {}\n ],\n \"jobs[].url\": \"<string>\",\n \"jobs[].filename\": \"<string>\",\n \"folder\": \"<string>\",\n \"format\": \"<string>\",\n \"video_codec\": \"<string>\",\n \"audio_codec\": \"<string>\",\n \"audio_bitrate\": \"<string>\",\n \"video_quality\": 123,\n \"audio_only\": true,\n \"download_subtitles\": true,\n \"download_thumbnail\": true,\n \"quality_preset\": \"<string>\",\n \"max_resolution\": \"<string>\",\n \"clip_start\": \"<string>\",\n \"clip_end\": \"<string>\",\n \"live_recording\": true,\n \"live_from_start\": true,\n \"max_duration\": 123,\n \"wait_for_video\": true,\n \"webhook_url\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"batch_id": "550e8400-e29b-41d4-a716-446655440001",
"total_jobs": 3,
"job_ids": [
"660f9511-f30c-52e5-b827-557766551111",
"660f9511-f30c-52e5-b827-557766551112",
"660f9511-f30c-52e5-b827-557766551113"
]
}
Overview
Create up to 100 download jobs in a single API request. All jobs share the same encoding options but can have individual filenames. The response includes a groupingbatch_id and individual job_ids; follow each job with GET /jobs/{id}.
Use this endpoint for high-volume submission: a bulk request counts as one HTTP request toward the API’s default 1,000 requests per second per authenticated client ceiling (configurable for the deployment/key). All direct API keys in the same organization share that budget across API instances. If you receive 429 with error: "rate_limit_exceeded", respect Retry-After before retrying. Bulk requires a direct API key; it is not available through marketplace authentication. Storage allowances and server capacity checks still apply.
Header Parameters
string
required
Your API key for authentication
Request
array
required
Array of job items (max 100)
Job Item
string
required
Video URL to download
string
Custom filename for this specific job
Shared Options
string
Storage folder prefix for all jobs
string
Output format for all jobs. Video:
mp4, mkv, webm, mov. Audio: m4a, mp3, ogg, opus. Default: mp4 on this bulk route. Set format: "m4a" explicitly for audio-only M4A outputstring
Video codec for all jobs:
copy, h264, h265/hevc, vp9, vp8string
Audio codec for all jobs:
copy, aac, opus, mp3, vorbis. Unknown codecs return 400. Incompatible codec/format combinations are corrected as on POST /jobs (codec auto-correction)string
Audio bitrate for all jobs: integer kilobits per second from
8k through 512k (for example 128k or 320k)integer
Video quality CRF for all jobs (0-51)
boolean
default:"false"
Extract audio only for all jobs. Set
format explicitly, for example m4a, mp3, ogg, or opus. Re-encoding depends on source codec, requested bitrate and clipping.boolean
default:"false"
Download subtitles for all jobs
boolean
default:"false"
Download thumbnails for all jobs
string
Quality preset for all jobs:
highest, high, medium, low, loweststring
Video resolution preference for all jobs:
best, lowest, 2160, 1440, 1080, 720, 480, 360, 240, 144. A numeric value chooses the best available resolution at or below the value, or the smallest available format if none fits. lowest always requests the smallest available video format. See single-job resolution and audio selection for details.string
Start timestamp for video clipping (all jobs). Format:
HH:MM:SS or seconds.string
End timestamp for video clipping (all jobs). Format:
HH:MM:SS or seconds.boolean
default:"false"
Enable live stream recording mode for all jobs.
boolean
default:"false"
Record from stream beginning (VOD mode) for all live stream jobs.
integer
Maximum recording duration in seconds for all jobs.
boolean
default:"false"
Wait for scheduled streams to start for all jobs.
string
Webhook URL applied to every job of the batch. Same rules and validation as the single-job
webhook_url; each job sends its own completion webhook.Request Example
{
"jobs": [
{
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"filename": "never-gonna-give-you-up"
},
{
"url": "https://www.youtube.com/watch?v=9bZkp7q19f0",
"filename": "gangnam-style"
},
{
"url": "https://www.youtube.com/watch?v=kJQP7kiw5Fk"
}
],
"folder": "music-videos",
"format": "mp4",
"max_resolution": "1080",
"video_codec": "h264",
"audio_codec": "aac",
"audio_bitrate": "192k"
}
Each item in the
jobs array only requires a url. The filename is optional - if not provided, the original video title will be used.All other options (folder, format, video_codec, etc.) are applied to all jobs in the batch.Response
string
Batch ID for tracking all jobs
integer
Number of jobs accepted for queueing/parking. Compare with the submitted count: individual enqueue failures can produce a smaller result
array
List of individual job IDs
Examples
curl -X POST "https://api.tornadoapi.io/jobs/bulk" \
-H "x-api-key: sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"jobs": [
{"url": "https://youtube.com/watch?v=video1", "filename": "my-first-video"},
{"url": "https://youtube.com/watch?v=video2", "filename": "my-second-video"},
{"url": "https://youtube.com/watch?v=video3"}
],
"folder": "my-downloads",
"format": "mp4"
}'
const response = await fetch('https://api.tornadoapi.io/jobs/bulk', {
method: 'POST',
headers: {
'x-api-key': 'sk_your_api_key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
jobs: [
{url: 'https://youtube.com/watch?v=video1', filename: 'my-first-video'},
{url: 'https://youtube.com/watch?v=video2', filename: 'my-second-video'},
{url: 'https://youtube.com/watch?v=video3'}
],
folder: 'my-downloads',
format: 'mp4'
})
});
const data = await response.json();
console.log(`Created ${data.total_jobs} jobs`);
console.log(`Batch ID: ${data.batch_id}`);
import requests
response = requests.post(
'https://api.tornadoapi.io/jobs/bulk',
headers={'x-api-key': 'sk_your_api_key'},
json={
'jobs': [
{'url': 'https://youtube.com/watch?v=video1', 'filename': 'my-first-video'},
{'url': 'https://youtube.com/watch?v=video2', 'filename': 'my-second-video'},
{'url': 'https://youtube.com/watch?v=video3'}
],
'folder': 'my-downloads',
'format': 'mp4'
}
)
data = response.json()
print(f"Created {data['total_jobs']} jobs")
curl -X POST "https://api.tornadoapi.io/jobs/bulk" \
-H "x-api-key: sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"jobs": [
{"url": "https://youtube.com/watch?v=song1"},
{"url": "https://youtube.com/watch?v=song2"},
{"url": "https://youtube.com/watch?v=song3"}
],
"folder": "music",
"audio_only": true,
"format": "m4a",
"audio_bitrate": "320k"
}'
Success Response
{
"batch_id": "550e8400-e29b-41d4-a716-446655440001",
"total_jobs": 3,
"job_ids": [
"660f9511-f30c-52e5-b827-557766551111",
"660f9511-f30c-52e5-b827-557766551112",
"660f9511-f30c-52e5-b827-557766551113"
]
}
Error Responses
{
"error": "No jobs provided"
}
{
"error": "Maximum 100 jobs per bulk request"
}
{
"error": "Invalid folder name: path traversal not allowed"
}
{
"error": "Missing x-api-key header"
}
Notes
- Maximum 100 jobs per bulk request
- All jobs share the same encoding options and folder
- Each job can have a unique filename
webhook_url(optional) is shared by all jobs of the batch; each job sends its own webhook- Use
/jobs/{id}to poll individual job status - The batch_id is for reference only - jobs are processed independently; it does not create a
/batch/{id}status resource - This route does not accept inline
storage, per-item options other than URL/filename, multi-clipclips, or progress webhooks cookies(top-level or per item) is refused with400: submit each age-restricted video toPOST /jobswith its cookies- Validation differs from
POST /jobs: unknown formats fall back to MP4; do not depend on unknown codecs or clip timestamps being rejected at admission
Was this page helpful?