Configure Google Cloud Storage
curl --request POST \
--url https://api.tornadoapi.io/user/gcs \
--header 'Content-Type: application/json' \
--header 'x-api-key: <x-api-key>' \
--data '
{
"project_id": "<string>",
"bucket": "<string>",
"service_account_json": "<string>",
"folder_prefix": "<string>",
"base_folder": "<string>"
}
'import requests
url = "https://api.tornadoapi.io/user/gcs"
payload = {
"project_id": "<string>",
"bucket": "<string>",
"service_account_json": "<string>",
"folder_prefix": "<string>",
"base_folder": "<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({
project_id: '<string>',
bucket: '<string>',
service_account_json: '<string>',
folder_prefix: '<string>',
base_folder: '<string>'
})
};
fetch('https://api.tornadoapi.io/user/gcs', 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/user/gcs",
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([
'project_id' => '<string>',
'bucket' => '<string>',
'service_account_json' => '<string>',
'folder_prefix' => '<string>',
'base_folder' => '<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/user/gcs"
payload := strings.NewReader("{\n \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<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/user/gcs")
.header("x-api-key", "<x-api-key>")
.header("Content-Type", "application/json")
.body("{\n \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tornadoapi.io/user/gcs")
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 \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"message": "Google Cloud Storage configured successfully",
"provider": "gcs",
"container_or_bucket": "tornado-downloads"
}
User
Configure Google Cloud Storage
Configure Google Cloud Storage for your downloads
POST
/
user
/
gcs
Configure Google Cloud Storage
curl --request POST \
--url https://api.tornadoapi.io/user/gcs \
--header 'Content-Type: application/json' \
--header 'x-api-key: <x-api-key>' \
--data '
{
"project_id": "<string>",
"bucket": "<string>",
"service_account_json": "<string>",
"folder_prefix": "<string>",
"base_folder": "<string>"
}
'import requests
url = "https://api.tornadoapi.io/user/gcs"
payload = {
"project_id": "<string>",
"bucket": "<string>",
"service_account_json": "<string>",
"folder_prefix": "<string>",
"base_folder": "<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({
project_id: '<string>',
bucket: '<string>',
service_account_json: '<string>',
folder_prefix: '<string>',
base_folder: '<string>'
})
};
fetch('https://api.tornadoapi.io/user/gcs', 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/user/gcs",
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([
'project_id' => '<string>',
'bucket' => '<string>',
'service_account_json' => '<string>',
'folder_prefix' => '<string>',
'base_folder' => '<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/user/gcs"
payload := strings.NewReader("{\n \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<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/user/gcs")
.header("x-api-key", "<x-api-key>")
.header("Content-Type", "application/json")
.body("{\n \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tornadoapi.io/user/gcs")
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 \"project_id\": \"<string>\",\n \"bucket\": \"<string>\",\n \"service_account_json\": \"<string>\",\n \"folder_prefix\": \"<string>\",\n \"base_folder\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"message": "Google Cloud Storage configured successfully",
"provider": "gcs",
"container_or_bucket": "tornado-downloads"
}
Overview
Set up Google Cloud Storage for uploaded videos. Authenticate with a service account JSON key. Credentials are verified before saving.Header Parameters
string
required
Your API key for authentication
Request Body
string
required
Google Cloud project ID
string
required
GCS bucket name
string
required
Service account JSON credentials as a JSON string. Base64 is not decoded by this endpoint
string
Optional folder prefix for organizing uploads (e.g.,
downloads/2024/)string
default:"videos"
Base folder name for uploaded files. Defaults to
videos if not specified. Set to a custom value to change the top-level folder where files are stored (e.g., downloads, media).Response
string
Success confirmation message
string
Storage provider type:
gcsstring
The configured bucket name
string
The configured folder prefix (if provided)
Examples
curl -X POST "https://api.tornadoapi.io/user/gcs" \
-H "x-api-key: sk_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"project_id": "my-gcp-project",
"bucket": "tornado-downloads",
"service_account_json": "{\"type\":\"service_account\",\"project_id\":\"my-gcp-project\",\"private_key\":\"-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n\",\"client_email\":\"tornado@my-gcp-project.iam.gserviceaccount.com\"}"
}'
import requests
import json
# Load service account JSON file (recommended)
with open("service-account.json") as f:
sa_json = json.dumps(json.load(f))
response = requests.post(
"https://api.tornadoapi.io/user/gcs",
headers={"x-api-key": "sk_your_api_key"},
json={
"project_id": "my-gcp-project",
"bucket": "tornado-downloads",
"service_account_json": sa_json
}
)
print(response.json())
import requests
import json
# Build as a dict and serialize with json.dumps()
service_account = {
"type": "service_account",
"project_id": "my-gcp-project",
"private_key_id": "key-id",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "tornado@my-gcp-project.iam.gserviceaccount.com",
"client_id": "123456789",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
response = requests.post(
"https://api.tornadoapi.io/user/gcs",
headers={"x-api-key": "sk_your_api_key"},
json={
"project_id": "my-gcp-project",
"bucket": "tornado-downloads",
"service_account_json": json.dumps(service_account)
}
)
print(response.json())
{
"message": "Google Cloud Storage configured successfully",
"provider": "gcs",
"container_or_bucket": "tornado-downloads"
}
Success Response
{
"message": "Google Cloud Storage configured successfully",
"provider": "gcs",
"container_or_bucket": "tornado-downloads"
}
Error Responses
{
"error": "Invalid configuration: Service account JSON missing 'private_key' field"
}
{
"error": "Credential validation failed: AuthenticationFailed"
}
{
"error": "Invalid API Key"
}
{
"error": "GCS storage configuration is temporarily unavailable"
}
Verification Process
When you submit storage configuration, Tornado:- Validates the request format and the service account JSON structure
- Creates a storage client with your credentials
- Uploads a small uniquely named credential-probe object
- Attempts best-effort deletion of the probe
- Saves credentials in Key Vault and a configuration reference on the API key
Required GCS Permissions
The Storage Object Admin role is one broad option. A custom role can instead grant the operations needed for the chosen upload/delivery path, including:storage.objects.createstorage.objects.deletestorage.objects.getstorage.objects.list
Pass the complete service-account JSON as a string, preferably using
json.dumps() as above. Do not Base64-encode this field.Python users: Do not manually construct the
service_account_json as a raw string with escaped quotes.
The \n characters in the private key will be interpreted as literal newlines by Python, which produces
invalid JSON and causes a "Invalid JSON in service account credentials" error.Always use json.dumps() on a dict or a loaded JSON file to ensure proper escaping:# Correct: json.dumps() handles escaping
sa_json = json.dumps({"type": "service_account", "private_key": "-----BEGIN...\n...", ...})
# Wrong: manual string with \n becomes literal newlines
sa_json = '{"type": "service_account", "private_key": "-----BEGIN...\n..."}'
Was this page helpful?