Skip to content

Shorts & Clips V2 API

Transform videos into viral short-form content, generate AI videos from scratch, convert articles into video recaps, or create talking-head avatar videos — all through a single unified API.

ModesClip (extract from video), Create (AI-generate), Recap (article→video), Avatar (talking head)
PipelineAutomated multi-step processing with real-time WebSocket progress
AI FeaturesB-Roll suggestions, Series AI split, virality scoring, auto-captions
Languages23 languages for transcription and captions
OutputMP4, up to 1080p, configurable aspect ratio (9:16, 16:9, 1:1, 4:5)
PublishingDirect publishing to TikTok, YouTube, Instagram, Facebook, LinkedIn

Two ways to use Shorts

FOTOhub exposes Shorts through two distinct interfaces. They are separate systems with different base URLs, authentication, and request shapes — pick the one that matches your use case.

1. Pipeline REST API (programmatic / SDK)

A stateless, step-based REST API for building your own pipelines or calling from the SDK.

Base URLhttps://apis.fotohub.app
PathsPOST /v1/shorts/<step>ingest, transcribe, detect-scenes, generate-clips, captions, reframe, render, agent
AuthAPI key — Authorization: Bearer fh_live_...
ModelStateless. Each call takes a video_url and returns the step result plus billing info. No projects or clips are stored.

See the full reference in Pipeline REST API.

2. Console RPC (shorts-process)

The stateful project/clip system that powers the web dashboard: server-side projects, clips, series, templates, publishing, and analytics.

Base URLhttps://s1.fotohub.app/functions/v1/shorts-process
MethodAlways POST, with {"action": "...", ...params} in the body
AuthSupabase session tokenAuthorization: Bearer <session_token> (the JWT from your logged-in dashboard session). Not an fh_live_* API key.
ModelStateful. Create a project, process it, then read back clips.

Copy-paste examples for every action live in the Shorts Console.

Two different transports — don't mix them

The console operations documented below (projects, clips, series, templates, publishing, analytics) are not reachable as dedicated RESTful resource paths. They are all dispatched through the single shorts-process endpoint via the action field, authenticated with a session token — not an API key. For programmatic access with an fh_live_* API key, use the Pipeline REST API instead.


Projects

Projects are the top-level container for all shorts workflows. Each project has a mode that determines its processing pipeline.

Transport

Project operations are Console RPC calls. Every request is a POST to https://s1.fotohub.app/functions/v1/shorts-process with an action field in the body, authenticated with a Supabase session token (not an fh_live_* API key). The signatures below show the action and its parameters.

Create Project

Create a new shorts project.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_create_project", ... }

Billing: No credits charged for creation

ParameterTypeRequiredDefaultDescription
modestringYesPipeline mode: clip, create, recap, or avatar
titlestringNoAuto-generatedProject title
source_urlstringNoVideo URL (YouTube, TikTok, Instagram, Vimeo) or uploaded file path. Required for clip mode.
source_typestringNoAuto-detectedyoutube, tiktok, instagram, vimeo, upload, article
settingsobjectNo{}Mode-specific configuration (language, caption_style, aspect_ratio, etc.)
avatar_configobjectNoAvatar mode configuration (face image, voice, script)
brand_kit_idstringNoApply brand kit (logo, colors, fonts) to all generated clips
briefstringNoCreative brief for create mode

Response:

json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "user_id": "user-uuid",
  "title": "My Short",
  "mode": "clip",
  "status": "draft",
  "source_url": "https://youtube.com/watch?v=...",
  "settings": {},
  "created_at": "2026-07-22T10:00:00Z",
  "steps_completed": []
}
python
from fotohub import FotoHub

client = FotoHub(api_key="YOUR_API_KEY")

project = client.shorts.create_project(
    mode="clip",
    source_url="https://youtube.com/watch?v=dQw4w9WgXcQ",
    title="Best Moments",
    settings={
        "language": "en",
        "caption_style": "karaoke",
        "aspect_ratio": "9:16",
        "max_clips": 5
    }
)
print(project.id)
typescript
import { FotoHub } from "fotohub";

const client = new FotoHub({ apiKey: "YOUR_API_KEY" });

const project = await client.shorts.createProject({
  mode: "clip",
  sourceUrl: "https://youtube.com/watch?v=dQw4w9WgXcQ",
  title: "Best Moments",
  settings: {
    language: "en",
    captionStyle: "karaoke",
    aspectRatio: "9:16",
    maxClips: 5
  }
});
console.log(project.id);
go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	payload := map[string]interface{}{
		"action":     "v2_create_project",
		"mode":       "clip",
		"source_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
		"title":      "Best Moments",
		"settings": map[string]interface{}{
			"language":      "en",
			"caption_style": "karaoke",
			"aspect_ratio":  "9:16",
			"max_clips":     5,
		},
	}
	body, _ := json.Marshal(payload)

	req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
	req.Header.Set("Content-Type", "application/json")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)

	var result map[string]interface{}
	json.Unmarshal(respBody, &result)
	fmt.Printf("Project ID: %s\n", result["id"])
}
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_create_project",
    "mode": "clip",
    "source_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
    "title": "Best Moments",
    "settings": {
      "language": "en",
      "caption_style": "karaoke",
      "aspect_ratio": "9:16",
      "max_clips": 5
    }
  }'

List Projects

Retrieve all projects for the authenticated user.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_list_projects", ... }
ParameterTypeRequiredDefaultDescription
limitintegerNo20Max results (1-100)
offsetintegerNo0Pagination offset

Response:

json
{
  "projects": [
    {
      "id": "uuid",
      "title": "My Short",
      "mode": "clip",
      "status": "completed",
      "source_url": "...",
      "input_duration_seconds": 324,
      "current_step": "done",
      "steps_completed": ["ingest", "transcribe", "detect", "clip", "caption", "render"],
      "processing_time_ms": 45000,
      "created_at": "2026-07-22T10:00:00Z",
      "completed_at": "2026-07-22T10:01:15Z"
    }
  ],
  "total": 1
}
python
projects = client.shorts.list_projects(limit=10)
for p in projects:
    print(f"{p.title}{p.status}")
typescript
const { projects } = await client.shorts.listProjects({ limit: 10 });
projects.forEach(p => console.log(`${p.title} — ${p.status}`));
go
payload := map[string]interface{}{
    "action": "v2_list_projects",
    "limit":  10,
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_list_projects", "limit": 10}'

Get Project

Get full project details including all clips.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_get_project", "project_id": "..." }
ParameterTypeRequiredDescription
idstringYesProject UUID

Response:

json
{
  "project": {
    "id": "uuid",
    "title": "My Short",
    "mode": "clip",
    "status": "completed",
    "settings": { "language": "en", "caption_style": "karaoke" },
    "source_url": "...",
    "steps_completed": ["ingest", "transcribe", "detect", "clip", "caption", "render"]
  },
  "clips": [
    {
      "id": "clip-uuid",
      "user_title": "Epic Hook",
      "hook_text": "You won't believe what happens next...",
      "virality_score": 87,
      "duration": 28.5,
      "start_time": 45.2,
      "end_time": 73.7,
      "status": "rendered",
      "renders": {
        "9:16": { "url": "https://...", "status": "rendered" }
      }
    }
  ]
}
python
result = client.shorts.get_project("project-uuid")
print(f"Clips: {len(result.clips)}")
typescript
const { project, clips } = await client.shorts.getProject("project-uuid");
console.log(`Clips: ${clips.length}`);
go
payload := map[string]interface{}{
    "action":     "v2_get_project",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_get_project", "project_id": "project-uuid"}'

Delete Project

Delete a project and all associated clips. Cancels any running pipeline.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_delete_project", "project_id": "..." }
ParameterTypeRequiredDescription
idstringYesProject UUID
python
client.shorts.delete_project("project-uuid")
typescript
await client.shorts.deleteProject("project-uuid");
go
payload := map[string]interface{}{
    "action":     "v2_delete_project",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_delete_project", "project_id": "project-uuid"}'

Start Pipeline

Start or resume processing for a project. This triggers the full automated pipeline for the project's mode.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_process_project", "project_id": "...", "settings": {} }

Billing: Credits charged per step (see Pricing & Credits)

ParameterTypeRequiredDefaultDescription
idstringYesProject UUID
settingsobjectNo{}Override or add pipeline settings

Pipeline steps by mode:

ModeSteps
clipingest → transcribe → detect → clip → caption → reframe → render
createscript → storyboard → generate → render
avatarvoice → lip-sync → render
recapscrape → script → storyboard → generate → render

Response:

json
{
  "status": "started",
  "project_id": "uuid",
  "mode": "clip"
}
python
result = client.shorts.process_project("project-uuid", settings={
    "caption_style": "bold",
    "max_clips": 3,
    "min_duration": 15,
    "max_duration": 60
})
typescript
const result = await client.shorts.processProject("project-uuid", {
  settings: {
    captionStyle: "bold",
    maxClips: 3,
    minDuration: 15,
    maxDuration: 60
  }
});
go
payload := map[string]interface{}{
    "action":     "v2_process_project",
    "project_id": "project-uuid",
    "settings": map[string]interface{}{
        "caption_style": "bold",
        "max_clips":     3,
        "min_duration":  15,
        "max_duration":  60,
    },
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_process_project",
    "project_id": "project-uuid",
    "settings": {
      "caption_style": "bold",
      "max_clips": 3,
      "min_duration": 15,
      "max_duration": 60
    }
  }'

WARNING

Pipeline processing is asynchronous. Use WebSocket or polling to track progress.


Cancel Pipeline

Cancel a running pipeline processing.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_cancel_project", "project_id": "..." }
python
client.shorts.cancel_project("project-uuid")
typescript
await client.shorts.cancelProject("project-uuid");
go
payload := map[string]interface{}{
    "action":     "v2_cancel_project",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_cancel_project", "project_id": "project-uuid"}'

Clips

Clips are the output units — individual short-form videos extracted or generated from a project.

Transport

Clip operations are Console RPC calls — POST https://s1.fotohub.app/functions/v1/shorts-process with an action field, authenticated with a Supabase session token.

List Clips

Get all clips for a project, sorted by virality score.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_list_clips", "project_id": "..." }

Response:

json
{
  "clips": [
    {
      "id": "clip-uuid",
      "project_id": "project-uuid",
      "user_title": "The Hook Moment",
      "hook_text": "Here's what nobody tells you about...",
      "virality_score": 92,
      "duration": 34.2,
      "start_time": 120.5,
      "end_time": 154.7,
      "tags": ["hook", "storytelling", "viral"],
      "status": "rendered",
      "renders": {
        "9:16": { "url": "https://...", "status": "rendered" },
        "1:1": { "url": "https://...", "status": "rendered" }
      },
      "thumbnail_url": "https://...",
      "transcript_segment": { "segments": [...] },
      "score_breakdown": {
        "hook_strength": 0.95,
        "pacing": 0.88,
        "completeness": 0.91,
        "topic": "storytelling"
      }
    }
  ]
}
python
clips = client.shorts.list_clips("project-uuid")
for clip in clips:
    print(f"{clip.user_title} — Score: {clip.virality_score}")
typescript
const { clips } = await client.shorts.listClips("project-uuid");
clips.forEach(c => console.log(`${c.userTitle} — Score: ${c.viralityScore}`));
go
payload := map[string]interface{}{
    "action":     "v2_list_clips",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_list_clips", "project_id": "project-uuid"}'

Get Clip

Get full details for a single clip.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_get_clip", "clip_id": "..." }
python
clip = client.shorts.get_clip("clip-uuid")
print(clip.renders)
typescript
const { clip } = await client.shorts.getClip("clip-uuid");
console.log(clip.renders);
go
payload := map[string]interface{}{
    "action":  "v2_get_clip",
    "clip_id": "clip-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_get_clip", "clip_id": "clip-uuid"}'

Update Clip

Update clip metadata (title, notes, favorite status).

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_update_clip", "clip_id": "...", ... }
ParameterTypeRequiredDescription
user_titlestringNoCustom title
user_notesstringNoNotes/annotations
is_favoritebooleanNoMark as favorite
is_discardedbooleanNoSoft-delete (hide from results)
tagsstring[]NoCustom tags
python
client.shorts.update_clip("clip-uuid", user_title="My Best Hook", is_favorite=True)
typescript
await client.shorts.updateClip("clip-uuid", {
  userTitle: "My Best Hook",
  isFavorite: true
});
go
payload := map[string]interface{}{
    "action":      "v2_update_clip",
    "clip_id":     "clip-uuid",
    "user_title":  "My Best Hook",
    "is_favorite": true,
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_update_clip",
    "clip_id": "clip-uuid",
    "user_title": "My Best Hook",
    "is_favorite": true
  }'

Render Clip

Queue a render for a specific clip in a given aspect ratio and quality.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_render_clip", "clip_id": "...", ... }

Billing: 1-3 credits per render (depending on quality)

ParameterTypeRequiredDefaultDescription
aspect_ratiostringNo9:169:16, 16:9, 1:1, 4:5
qualitystringNosocial_1080psocial_720p, social_1080p, pro_1080p

Response:

json
{
  "queued": true,
  "aspect_ratio": "9:16"
}
python
client.shorts.render_clip("clip-uuid", aspect_ratio="9:16", quality="social_1080p")
typescript
await client.shorts.renderClip("clip-uuid", {
  aspectRatio: "9:16",
  quality: "social_1080p"
});
go
payload := map[string]interface{}{
    "action":       "v2_render_clip",
    "clip_id":      "clip-uuid",
    "aspect_ratio": "9:16",
    "quality":      "social_1080p",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_render_clip",
    "clip_id": "clip-uuid",
    "aspect_ratio": "9:16",
    "quality": "social_1080p"
  }'

Delete Clip

Permanently delete a clip.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_delete_clip", "clip_id": "..." }
python
client.shorts.delete_clip("clip-uuid")
typescript
await client.shorts.deleteClip("clip-uuid");
go
payload := map[string]interface{}{
    "action":  "v2_delete_clip",
    "clip_id": "clip-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_delete_clip", "clip_id": "clip-uuid"}'

Create Mode

Generate short-form videos entirely from AI — provide a brief, get a script, storyboard, and final video.

Generate Script

Generate a structured video script from a creative brief using FOTOhub AI.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_generate_script", "project_id": "...", ... }

Billing: 2 credits

ParameterTypeRequiredDefaultDescription
briefstringYesCreative brief describing the video (10-2000 chars)
target_durationintegerNo60Target video duration in seconds (15-180)
stylestringNoVisual style hint (e.g., "cinematic", "energetic", "minimalist")
platformstringNoTarget platform ("tiktok", "youtube_shorts", "instagram_reels")

Response:

json
{
  "status": "generated",
  "script": {
    "title": "5 Things Nobody Tells You About Startups",
    "hook": "I lost $50k in 3 months. Here's what I wish someone told me.",
    "scenes": [
      {
        "scene_number": 1,
        "duration_seconds": 8,
        "narration": "Three years ago, I quit my job...",
        "visual_description": "Person at desk, packing belongings into a box",
        "text_overlay": "Day 1: The Leap"
      }
    ],
    "outro_cta": "Follow for Part 2",
    "total_duration_seconds": 58,
    "mood": "inspirational",
    "tags": ["startup", "entrepreneurship", "lessons"]
  }
}
python
result = client.shorts.generate_script("project-uuid",
    brief="5 surprising facts about deep sea creatures that will blow your mind",
    target_duration=45,
    platform="tiktok"
)
print(f"Script: {result.script.title} ({result.script.total_duration_seconds}s)")
typescript
const result = await client.shorts.generateScript("project-uuid", {
  brief: "5 surprising facts about deep sea creatures that will blow your mind",
  targetDuration: 45,
  platform: "tiktok"
});
console.log(`Script: ${result.script.title}`);
go
payload := map[string]interface{}{
    "action":          "v2_generate_script",
    "project_id":      "project-uuid",
    "brief":           "5 surprising facts about deep sea creatures",
    "target_duration": 45,
    "platform":        "tiktok",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
script := result["script"].(map[string]interface{})
fmt.Printf("Script: %s\n", script["title"])
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_generate_script",
    "project_id": "project-uuid",
    "brief": "5 surprising facts about deep sea creatures",
    "target_duration": 45,
    "platform": "tiktok"
  }'

Generate Storyboard

Convert a script into a visual storyboard with scene-by-scene prompts for video generation.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_generate_storyboard", "project_id": "..." }

Billing: 2 credits

ParameterTypeRequiredDefaultDescription
scriptobjectNoUses project's scriptOverride script object

Response:

json
{
  "status": "storyboard_created",
  "storyboard": {
    "scenes": [
      {
        "scene_number": 1,
        "video_prompt": "Cinematic shot of person at desk, warm lighting, office environment",
        "duration_seconds": 8,
        "transition": "fade",
        "narration": "Three years ago, I quit my job..."
      }
    ]
  }
}
python
result = client.shorts.generate_storyboard("project-uuid")
print(f"Scenes: {len(result.storyboard.scenes)}")
typescript
const result = await client.shorts.generateStoryboard("project-uuid");
console.log(`Scenes: ${result.storyboard.scenes.length}`);
go
payload := map[string]interface{}{
    "action":     "v2_generate_storyboard",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_generate_storyboard", "project_id": "project-uuid"}'

Generate Video

Generate the final video from storyboard using AI video generation (Seedance 2.0).

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_generate_video", "project_id": "...", ... }

Billing: 15-50 credits per scene (depending on resolution and model)

ParameterTypeRequiredDefaultDescription
modelstringNofastVideo model: fast (5s generation), pro (higher quality), mini (cheaper)
resolutionstringNo720p480p, 720p, 1080p

Credits

Video generation is credit-intensive. A 45-second video with 5 scenes at 720p costs approximately 75-100 credits.

python
result = client.shorts.generate_video("project-uuid", model="pro", resolution="1080p")
typescript
const result = await client.shorts.generateVideo("project-uuid", {
  model: "pro",
  resolution: "1080p"
});
go
payload := map[string]interface{}{
    "action":     "v2_generate_video",
    "project_id": "project-uuid",
    "model":      "pro",
    "resolution": "1080p",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_generate_video",
    "project_id": "project-uuid",
    "model": "pro",
    "resolution": "1080p"
  }'

Avatar Mode

Generate talking-head videos with AI-driven lip sync and voice.

Generate Avatar Video

Create an avatar video from a face image and script/audio.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_generate_avatar", "project_id": "...", ... }

Billing: 10-25 credits (depending on duration)

ParameterTypeRequiredDefaultDescription
face_image_urlstringYesURL to face image (front-facing, clear)
scriptstringNoText to speak (uses TTS)
audio_urlstringNoPre-recorded audio URL (alternative to script)
voice_idstringNoDefault voiceVoice ID from FOTOhub Voice library
languagestringNoenLanguage for TTS
python
result = client.shorts.generate_avatar("project-uuid",
    face_image_url="https://storage.fotohub.app/.../face.jpg",
    script="Welcome to my channel! Today I'm going to show you...",
    voice_id="voice-energetic-female",
    language="en"
)
typescript
const result = await client.shorts.generateAvatar("project-uuid", {
  faceImageUrl: "https://storage.fotohub.app/.../face.jpg",
  script: "Welcome to my channel! Today I'm going to show you...",
  voiceId: "voice-energetic-female",
  language: "en"
});
go
payload := map[string]interface{}{
    "action":         "v2_generate_avatar",
    "project_id":     "project-uuid",
    "face_image_url": "https://storage.fotohub.app/.../face.jpg",
    "script":         "Welcome to my channel! Today I'm going to show you...",
    "voice_id":       "voice-energetic-female",
    "language":       "en",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_generate_avatar",
    "project_id": "project-uuid",
    "face_image_url": "https://storage.fotohub.app/.../face.jpg",
    "script": "Welcome to my channel!",
    "voice_id": "voice-energetic-female"
  }'

Recap Mode

Convert articles and blog posts into engaging video recaps with AI narration and visuals.

Scrape Article

Extract text content from a URL for video recap generation.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_recap_scrape", "project_id": "...", "url": "..." }

Billing: 1 credit

ParameterTypeRequiredDescription
idstringYesProject UUID
urlstringYesArticle URL to scrape

Response:

json
{
  "status": "scraped",
  "title": "The Future of AI in Creative Industries",
  "word_count": 2340,
  "domain": "techcrunch.com",
  "preview": "First 500 characters of the article text..."
}
python
result = client.shorts.scrape_article("project-uuid",
    url="https://techcrunch.com/2026/07/20/ai-creative-tools/"
)
print(f"Scraped: {result.title} ({result.word_count} words)")
typescript
const result = await client.shorts.scrapeArticle("project-uuid", {
  url: "https://techcrunch.com/2026/07/20/ai-creative-tools/"
});
console.log(`Scraped: ${result.title} (${result.wordCount} words)`);
go
payload := map[string]interface{}{
    "action":     "v2_recap_scrape",
    "project_id": "project-uuid",
    "url":        "https://techcrunch.com/2026/07/20/ai-creative-tools/",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Printf("Scraped: %s (%v words)\n", result["title"], result["word_count"])
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_recap_scrape",
    "project_id": "project-uuid",
    "url": "https://techcrunch.com/2026/07/20/ai-creative-tools/"
  }'

Generate Recap Script

Generate a video script from the scraped article content.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_recap_generate_script", "project_id": "...", ... }

Billing: 3 credits

ParameterTypeRequiredDefaultDescription
target_durationintegerNo45Target video duration in seconds
stylestringNoinformativeScript style: informative, dramatic, casual, professional
languagestringNoenOutput language

Response:

json
{
  "status": "script_generated",
  "title": "AI is Changing Creative Work Forever",
  "scenes": 5,
  "total_duration": 48,
  "script": {
    "title": "AI is Changing Creative Work Forever",
    "hook": "In 2026, AI didn't replace creators — it made them 10x faster.",
    "scenes": [
      {
        "scene_number": 1,
        "duration_seconds": 8,
        "narration": "Something incredible happened in the creative industry this year...",
        "visual_description": "Split screen: traditional artist vs AI-assisted creator",
        "text_overlay": "The Creative Revolution"
      }
    ],
    "outro_cta": "Read the full article — link in bio",
    "total_duration_seconds": 48,
    "mood": "informative",
    "tags": ["ai", "creativity", "technology"]
  }
}
python
result = client.shorts.generate_recap_script("project-uuid",
    target_duration=60,
    style="dramatic",
    language="en"
)
print(f"Script: {result.script.title} ({result.total_duration}s, {result.scenes} scenes)")
typescript
const result = await client.shorts.generateRecapScript("project-uuid", {
  targetDuration: 60,
  style: "dramatic",
  language: "en"
});
console.log(`Script: ${result.script.title}`);
go
payload := map[string]interface{}{
    "action":          "v2_recap_generate_script",
    "project_id":      "project-uuid",
    "target_duration": 60,
    "style":           "dramatic",
    "language":        "en",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Printf("Script: %s\n", result["title"])
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_recap_generate_script",
    "project_id": "project-uuid",
    "target_duration": 60,
    "style": "dramatic",
    "language": "en"
  }'

Get Recap Script

Retrieve the current script for a recap project.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_recap_get_script", "project_id": "..." }
python
result = client.shorts.get_recap_script("project-uuid")
print(result.script)
typescript
const { script } = await client.shorts.getRecapScript("project-uuid");
console.log(script);
go
payload := map[string]interface{}{
    "action":     "v2_recap_get_script",
    "project_id": "project-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_recap_get_script", "project_id": "project-uuid"}'

Series

Organize clips into episodic series with auto-numbering and narrative management.

Transport

Series operations are Console RPC calls — POST https://s1.fotohub.app/functions/v1/shorts-process with an action field, authenticated with a Supabase session token.

List Series

Get all series for the authenticated user.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_list_series" }

Response:

json
{
  "series": [
    {
      "id": "series-uuid",
      "title": "Startup Lessons",
      "description": "Weekly lessons from building in public",
      "status": "active",
      "episode_count": 12,
      "settings": {},
      "created_at": "2026-07-01T10:00:00Z",
      "updated_at": "2026-07-22T15:30:00Z"
    }
  ]
}
python
series_list = client.shorts.list_series()
for s in series_list:
    print(f"{s.title}{s.episode_count} episodes")
typescript
const { series } = await client.shorts.listSeries();
series.forEach(s => console.log(`${s.title} — ${s.episodeCount} episodes`));
go
payload := map[string]interface{}{
    "action": "v2_list_series",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_list_series"}'

Create Series

Create a new clip series.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_create_series", ... }
ParameterTypeRequiredDefaultDescription
titlestringYesSeries title
descriptionstringNoSeries description
brand_kit_idstringNoBrand kit to apply to all episodes
settingsobjectNo{}Series-level settings
python
series = client.shorts.create_series(
    title="Startup Lessons",
    description="Weekly lessons from building in public"
)
print(series.id)
typescript
const series = await client.shorts.createSeries({
  title: "Startup Lessons",
  description: "Weekly lessons from building in public"
});
console.log(series.id);
go
payload := map[string]interface{}{
    "action":      "v2_create_series",
    "title":       "Startup Lessons",
    "description": "Weekly lessons from building in public",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Printf("Series ID: %s\n", result["id"])
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_create_series",
    "title": "Startup Lessons",
    "description": "Weekly lessons from building in public"
  }'

Get Series

Get series details with all episodes.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_get_series", "series_id": "..." }

Response:

json
{
  "series": {
    "id": "series-uuid",
    "title": "Startup Lessons",
    "status": "active",
    "episode_count": 5
  },
  "episodes": [
    {
      "id": "clip-uuid",
      "user_title": "Episode 1: The Idea",
      "episode_number": 1,
      "duration": 32,
      "virality_score": 85,
      "status": "rendered",
      "renders": { "9:16": { "url": "..." } },
      "thumbnail_url": "https://..."
    }
  ]
}
python
result = client.shorts.get_series("series-uuid")
print(f"{result.series.title}{len(result.episodes)} episodes")
typescript
const { series, episodes } = await client.shorts.getSeries("series-uuid");
console.log(`${series.title} — ${episodes.length} episodes`);
go
payload := map[string]interface{}{
    "action":    "v2_get_series",
    "series_id": "series-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_get_series", "series_id": "series-uuid"}'

Update Series

Update series metadata or status.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_update_series", "series_id": "...", ... }
ParameterTypeRequiredDescription
titlestringNoNew title
descriptionstringNoNew description
statusstringNoactive, paused, completed, archived
settingsobjectNoUpdated settings
python
client.shorts.update_series("series-uuid", title="Startup Lessons Season 2", status="active")
typescript
await client.shorts.updateSeries("series-uuid", {
  title: "Startup Lessons Season 2",
  status: "active"
});
go
payload := map[string]interface{}{
    "action":    "v2_update_series",
    "series_id": "series-uuid",
    "title":     "Startup Lessons Season 2",
    "status":    "active",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_update_series",
    "series_id": "series-uuid",
    "title": "Startup Lessons Season 2"
  }'

Add Clips to Series

Add clips to a series with auto-incrementing episode numbers.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_add_to_series", "series_id": "...", "clip_ids": [...] }
ParameterTypeRequiredDescription
clip_idsstring[]YesArray of clip UUIDs to add

Response:

json
{
  "status": "added",
  "episodes": [
    { "clip_id": "clip-1", "episode_number": 6 },
    { "clip_id": "clip-2", "episode_number": 7 }
  ]
}
python
result = client.shorts.add_to_series("series-uuid", clip_ids=["clip-1", "clip-2"])
print(f"Added {len(result.episodes)} episodes")
typescript
const result = await client.shorts.addToSeries("series-uuid", {
  clipIds: ["clip-1", "clip-2"]
});
console.log(`Added ${result.episodes.length} episodes`);
go
payload := map[string]interface{}{
    "action":    "v2_add_to_series",
    "series_id": "series-uuid",
    "clip_ids":  []string{"clip-1", "clip-2"},
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_add_to_series",
    "series_id": "series-uuid",
    "clip_ids": ["clip-1", "clip-2"]
  }'

Reorder Series

Reorder episodes within a series.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_reorder_series", "series_id": "...", "order": [...] }
ParameterTypeRequiredDescription
orderstring[]YesClip IDs in desired order
python
client.shorts.reorder_series("series-uuid", order=["clip-3", "clip-1", "clip-2"])
typescript
await client.shorts.reorderSeries("series-uuid", {
  order: ["clip-3", "clip-1", "clip-2"]
});
go
payload := map[string]interface{}{
    "action":    "v2_reorder_series",
    "series_id": "series-uuid",
    "order":     []string{"clip-3", "clip-1", "clip-2"},
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_reorder_series",
    "series_id": "series-uuid",
    "order": ["clip-3", "clip-1", "clip-2"]
  }'

Delete Series

Delete a series (clips are preserved, just unlinked).

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_delete_series", "series_id": "..." }
python
client.shorts.delete_series("series-uuid")
typescript
await client.shorts.deleteSeries("series-uuid");
go
payload := map[string]interface{}{
    "action":    "v2_delete_series",
    "series_id": "series-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_delete_series", "series_id": "series-uuid"}'

AI Split Series

Use AI to analyze clips and intelligently group them into narrative episodes with cliffhangers.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_series_ai_split", "series_id": "...", ... }

Billing: 3 credits

ParameterTypeRequiredDefaultDescription
clip_idsstring[]YesClip UUIDs to split into episodes
target_episodesintegerNoAuto (clips/3)Number of target episodes
stylestringNonarrativeSplit style: narrative, educational, suspense

Styles:

StyleDescription
narrativeStory arcs with beginning, middle, end per episode
educationalProgressive learning — each episode builds on prior
suspenseTension building with dramatic hooks between episodes

Response:

json
{
  "series_id": "series-uuid",
  "episodes_created": 3,
  "plan": {
    "episodes": [
      {
        "episode_number": 1,
        "title": "The Discovery",
        "clip_ids": ["clip-1", "clip-4"],
        "hook_for_next": "But what they found next changed everything...",
        "narrative_note": "Sets up the mystery, introduces the characters"
      },
      {
        "episode_number": 2,
        "title": "The Twist",
        "clip_ids": ["clip-2", "clip-5"],
        "hook_for_next": "And that's when the real challenge began...",
        "narrative_note": "Reveals the complication, raises stakes"
      },
      {
        "episode_number": 3,
        "title": "The Resolution",
        "clip_ids": ["clip-3", "clip-6"],
        "hook_for_next": "",
        "narrative_note": "Climax and resolution, call to action"
      }
    ],
    "series_narrative": "A three-part journey of discovery and resolution"
  }
}
python
result = client.shorts.ai_split_series("series-uuid",
    clip_ids=["clip-1", "clip-2", "clip-3", "clip-4", "clip-5", "clip-6"],
    target_episodes=3,
    style="narrative"
)
for ep in result.plan.episodes:
    print(f"Ep {ep.episode_number}: {ep.title}{ep.hook_for_next}")
typescript
const result = await client.shorts.aiSplitSeries("series-uuid", {
  clipIds: ["clip-1", "clip-2", "clip-3", "clip-4", "clip-5", "clip-6"],
  targetEpisodes: 3,
  style: "narrative"
});
result.plan.episodes.forEach(ep =>
  console.log(`Ep ${ep.episodeNumber}: ${ep.title}`)
);
go
payload := map[string]interface{}{
    "action":          "v2_series_ai_split",
    "series_id":       "series-uuid",
    "clip_ids":        []string{"clip-1", "clip-2", "clip-3", "clip-4", "clip-5", "clip-6"},
    "target_episodes": 3,
    "style":           "narrative",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Printf("Episodes created: %v\n", result["episodes_created"])
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_series_ai_split",
    "series_id": "series-uuid",
    "clip_ids": ["clip-1", "clip-2", "clip-3", "clip-4", "clip-5", "clip-6"],
    "target_episodes": 3,
    "style": "narrative"
  }'

Templates

Save and reuse caption/style configurations as templates.

Transport

Template operations are Console RPC calls — POST https://s1.fotohub.app/functions/v1/shorts-process with an action field, authenticated with a Supabase session token.

List Templates

Get your templates plus popular public templates.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_list_templates" }

Response:

json
{
  "templates": [
    {
      "id": "template-uuid",
      "name": "Viral Captions",
      "description": "Bold captions with emoji emphasis",
      "category": "captions",
      "is_public": false,
      "config": {
        "caption_style": "bold",
        "font_size": 48,
        "position": "center",
        "color": "#FFFFFF",
        "background": "gradient_dark"
      },
      "use_count": 23,
      "created_at": "2026-06-15T10:00:00Z"
    }
  ]
}
python
templates = client.shorts.list_templates()
for t in templates:
    print(f"{t.name} ({t.category}) — used {t.use_count}x")
typescript
const { templates } = await client.shorts.listTemplates();
templates.forEach(t => console.log(`${t.name} — used ${t.useCount}x`));
go
payload := map[string]interface{}{
    "action": "v2_list_templates",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_list_templates"}'

Create Template

Save current configuration as a reusable template.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_create_template", ... }
ParameterTypeRequiredDefaultDescription
namestringYesTemplate name
configobjectYesConfiguration to save
categorystringNocustomCategory: captions, reframe, render, custom
descriptionstringNoTemplate description
python
template = client.shorts.create_template(
    name="My Brand Style",
    config={
        "caption_style": "karaoke",
        "font": "Inter Bold",
        "primary_color": "#7C3AED",
        "aspect_ratio": "9:16"
    },
    category="captions"
)
typescript
const template = await client.shorts.createTemplate({
  name: "My Brand Style",
  config: {
    captionStyle: "karaoke",
    font: "Inter Bold",
    primaryColor: "#7C3AED",
    aspectRatio: "9:16"
  },
  category: "captions"
});
go
payload := map[string]interface{}{
    "action":   "v2_create_template",
    "name":     "My Brand Style",
    "category": "captions",
    "config": map[string]interface{}{
        "caption_style": "karaoke",
        "font":          "Inter Bold",
        "primary_color": "#7C3AED",
        "aspect_ratio":  "9:16",
    },
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_create_template",
    "name": "My Brand Style",
    "config": {
      "caption_style": "karaoke",
      "font": "Inter Bold",
      "primary_color": "#7C3AED"
    },
    "category": "captions"
  }'

Delete Template

Delete a template you own.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_delete_template", "template_id": "..." }
python
client.shorts.delete_template("template-uuid")
typescript
await client.shorts.deleteTemplate("template-uuid");
go
payload := map[string]interface{}{
    "action":      "v2_delete_template",
    "template_id": "template-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_delete_template", "template_id": "template-uuid"}'

Apply Template

Apply a template's configuration to a clip.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_apply_template", "template_id": "...", "clip_id": "..." }
ParameterTypeRequiredDescription
clip_idstringYesClip UUID to apply template to
python
client.shorts.apply_template("template-uuid", clip_id="clip-uuid")
typescript
await client.shorts.applyTemplate("template-uuid", { clipId: "clip-uuid" });
go
payload := map[string]interface{}{
    "action":      "v2_apply_template",
    "template_id": "template-uuid",
    "clip_id":     "clip-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_apply_template",
    "template_id": "template-uuid",
    "clip_id": "clip-uuid"
  }'

Analytics

Track usage, costs, and real-world performance of your shorts.

Transport

Analytics operations are Console RPC calls — POST https://s1.fotohub.app/functions/v1/shorts-process with an action field, authenticated with a Supabase session token.

Dashboard

Aggregated analytics overview for your account.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_analytics_dashboard" }

Response:

json
{
  "total_credits_used": 245.50,
  "total_events": 87,
  "avg_processing_time_ms": 23400,
  "event_breakdown": {
    "ingest": 15,
    "transcribe": 15,
    "clip": 12,
    "render": 20,
    "publish": 10,
    "script_generate": 8,
    "video_generate": 7
  },
  "projects": {
    "total": 15,
    "by_status": {
      "completed": 12,
      "processing": 1,
      "draft": 2
    }
  },
  "recent_events": [
    {
      "event_type": "render",
      "credits_used": 2.5,
      "processing_time_ms": 8500,
      "created_at": "2026-07-22T15:30:00Z"
    }
  ]
}
python
dashboard = client.shorts.get_dashboard()
print(f"Credits used: {dashboard.total_credits_used}")
print(f"Projects: {dashboard.projects.total}")
typescript
const dashboard = await client.shorts.getDashboard();
console.log(`Credits used: ${dashboard.totalCreditsUsed}`);
go
payload := map[string]interface{}{
    "action": "v2_analytics_dashboard",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_analytics_dashboard"}'

Cost Report

Detailed credit breakdown by operation type.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_analytics_cost_report" }

Response:

json
{
  "total": 245.50,
  "by_operation": {
    "video_generate": 120.0,
    "render": 45.5,
    "transcribe": 30.0,
    "ingest": 20.0,
    "script_generate": 16.0,
    "publish": 10.0,
    "broll_suggest": 4.0
  },
  "event_count": 87
}
python
report = client.shorts.get_cost_report()
print(f"Total: {report.total} credits")
for op, cost in report.by_operation.items():
    print(f"  {op}: {cost}")
typescript
const report = await client.shorts.getCostReport();
console.log(`Total: ${report.total} credits`);
go
payload := map[string]interface{}{
    "action": "v2_analytics_cost_report",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_analytics_cost_report"}'

Submit Performance

Submit real-world performance metrics for a clip (views, likes, shares from social platforms).

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_submit_performance", "clip_id": "...", ... }
ParameterTypeRequiredDefaultDescription
clip_idstringYesClip UUID
viewsintegerNoView count
likesintegerNoLike count
sharesintegerNoShare count
platformstringNotiktokPlatform source

TIP

Submitting performance data improves the AI's virality scoring for future clips.

python
client.shorts.submit_performance("clip-uuid",
    views=50000,
    likes=3200,
    shares=450,
    platform="tiktok"
)
typescript
await client.shorts.submitPerformance("clip-uuid", {
  views: 50000,
  likes: 3200,
  shares: 450,
  platform: "tiktok"
});
go
payload := map[string]interface{}{
    "action":   "v2_submit_performance",
    "clip_id":  "clip-uuid",
    "views":    50000,
    "likes":    3200,
    "shares":   450,
    "platform": "tiktok",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_submit_performance",
    "clip_id": "clip-uuid",
    "views": 50000,
    "likes": 3200,
    "shares": 450,
    "platform": "tiktok"
  }'

B-Roll AI

Get AI-powered suggestions for stock footage inserts to enhance your clips.

Suggest B-Roll

Analyze a clip's transcript and suggest where to insert B-Roll footage with stock footage search queries.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_broll_suggest", "clip_id": "..." }

Billing: 2 credits

Response:

json
{
  "clip_id": "clip-uuid",
  "suggestions": [
    {
      "timestamp": 5.2,
      "duration": 3,
      "search_query": "city skyline sunset timelapse",
      "description": "Aerial shot of city at golden hour",
      "rationale": "Visual break during narration about urban growth, creates emotional connection to the topic"
    },
    {
      "timestamp": 18.0,
      "duration": 2.5,
      "search_query": "hands typing code laptop",
      "description": "Close-up of developer working",
      "rationale": "Illustrates the technical concept being discussed in this segment"
    },
    {
      "timestamp": 32.5,
      "duration": 4,
      "search_query": "team celebration office",
      "description": "People high-fiving in modern office",
      "rationale": "Reinforces success story at the emotional peak of the narrative"
    }
  ]
}
python
result = client.shorts.suggest_broll("clip-uuid")
for suggestion in result.suggestions:
    print(f"  @{suggestion.timestamp}s → search: '{suggestion.search_query}'")
    print(f"    {suggestion.rationale}")
typescript
const result = await client.shorts.suggestBRoll("clip-uuid");
result.suggestions.forEach(s =>
  console.log(`@${s.timestamp}s — ${s.searchQuery}: ${s.description}`)
);
go
payload := map[string]interface{}{
    "action":  "v2_broll_suggest",
    "clip_id": "clip-uuid",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)

var result map[string]interface{}
json.Unmarshal(respBody, &result)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_broll_suggest", "clip_id": "clip-uuid"}'

Batch Render

Render multiple clips from a project in one request.

Batch Render

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_batch_render", "project_id": "...", ... }

Billing: 1-3 credits per clip rendered

ParameterTypeRequiredDefaultDescription
project_idstringYesProject UUID
clip_idsstring[]NoAll clipsSpecific clips to render (omit for all)
qualitystringNosocial_1080pRender quality
aspect_ratiostringNo9:16Output aspect ratio

Response:

json
{
  "status": "started",
  "project_id": "project-uuid"
}
python
result = client.shorts.batch_render(
    project_id="project-uuid",
    clip_ids=["clip-1", "clip-2", "clip-3"],
    quality="social_1080p",
    aspect_ratio="9:16"
)
typescript
const result = await client.shorts.batchRender({
  projectId: "project-uuid",
  clipIds: ["clip-1", "clip-2", "clip-3"],
  quality: "social_1080p",
  aspectRatio: "9:16"
});
go
payload := map[string]interface{}{
    "action":       "v2_batch_render",
    "project_id":   "project-uuid",
    "clip_ids":     []string{"clip-1", "clip-2", "clip-3"},
    "quality":      "social_1080p",
    "aspect_ratio": "9:16",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_batch_render",
    "project_id": "project-uuid",
    "clip_ids": ["clip-1", "clip-2", "clip-3"],
    "quality": "social_1080p",
    "aspect_ratio": "9:16"
  }'

WARNING

Batch render runs asynchronously. Poll the project or clips endpoint to check render status.


Publishing

Publish clips directly to social media platforms.

Transport

Publishing operations are Console RPC calls — POST https://s1.fotohub.app/functions/v1/shorts-process with an action field, authenticated with a Supabase session token.

List Social Accounts

Get connected social media accounts.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_list_social_accounts" }

Response:

json
{
  "accounts": [
    {
      "id": "account-uuid",
      "platform": "tiktok",
      "account_name": "@mychannel",
      "account_avatar_url": "https://...",
      "scopes": ["video.upload", "video.publish"],
      "token_expires_at": "2026-08-22T00:00:00Z"
    }
  ]
}
python
accounts = client.shorts.list_social_accounts()
for acc in accounts:
    print(f"{acc.platform}: {acc.account_name}")
typescript
const { accounts } = await client.shorts.listSocialAccounts();
accounts.forEach(a => console.log(`${a.platform}: ${a.accountName}`));
go
payload := map[string]interface{}{
    "action": "v2_list_social_accounts",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_list_social_accounts"}'

Publish Clip

Publish or schedule a clip to a social platform.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_publish", "clip_id": "...", "platform": "...", ... }

Billing: 1 credit per publish

ParameterTypeRequiredDefaultDescription
clip_idstringYesClip UUID (must be rendered)
platformstringYesyoutube, tiktok, instagram, facebook, linkedin
account_idstringNoDefault accountSocial account UUID
titlestringNoClip titlePost title
descriptionstringNoPost description/caption
hashtagsstring[]No[]Hashtags (without #)
scheduled_atstringNoImmediateISO timestamp for scheduled publishing

Response:

json
{
  "queued": true,
  "queue_id": "queue-uuid",
  "scheduled_at": null,
  "platform": "tiktok"
}
python
result = client.shorts.publish("clip-uuid",
    platform="tiktok",
    title="You won't believe this!",
    hashtags=["viral", "shorts", "fyp"],
    description="Part 1 of my startup journey"
)
typescript
const result = await client.shorts.publish({
  clipId: "clip-uuid",
  platform: "tiktok",
  title: "You won't believe this!",
  hashtags: ["viral", "shorts", "fyp"],
  description: "Part 1 of my startup journey"
});
go
payload := map[string]interface{}{
    "action":      "v2_publish",
    "clip_id":     "clip-uuid",
    "platform":    "tiktok",
    "title":       "You won't believe this!",
    "hashtags":    []string{"viral", "shorts", "fyp"},
    "description": "Part 1 of my startup journey",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_publish",
    "clip_id": "clip-uuid",
    "platform": "tiktok",
    "title": "You won'\''t believe this!",
    "hashtags": ["viral", "shorts", "fyp"]
  }'

Batch Publish

Publish multiple clips to multiple platforms at once.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_batch_publish", ... }
ParameterTypeRequiredDescription
clip_idsstring[]Yes*Clip UUIDs
platformsstring[]Yes*Platforms (cross-product with clip_ids)
scheduled_atstringNoSchedule all for this time
clipsobject[]No*Full control: array of individual publish configs

*Either clip_ids + platforms OR clips is required.

Response:

json
{
  "queued": 6,
  "results": [
    { "clip_id": "clip-1", "platform": "tiktok", "status": "queued" },
    { "clip_id": "clip-1", "platform": "youtube", "status": "queued" },
    { "clip_id": "clip-2", "platform": "tiktok", "status": "queued" }
  ]
}
python
result = client.shorts.batch_publish(
    clip_ids=["clip-1", "clip-2", "clip-3"],
    platforms=["tiktok", "youtube"],
    scheduled_at="2026-07-23T09:00:00Z"
)
print(f"Queued: {result.queued}")
typescript
const result = await client.shorts.batchPublish({
  clipIds: ["clip-1", "clip-2", "clip-3"],
  platforms: ["tiktok", "youtube"],
  scheduledAt: "2026-07-23T09:00:00Z"
});
go
payload := map[string]interface{}{
    "action":       "v2_batch_publish",
    "clip_ids":     []string{"clip-1", "clip-2", "clip-3"},
    "platforms":    []string{"tiktok", "youtube"},
    "scheduled_at": "2026-07-23T09:00:00Z",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "v2_batch_publish",
    "clip_ids": ["clip-1", "clip-2", "clip-3"],
    "platforms": ["tiktok", "youtube"],
    "scheduled_at": "2026-07-23T09:00:00Z"
  }'

Get Publish Queue

View scheduled and completed publish jobs.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_get_publish_queue", ... }
ParameterTypeRequiredDefaultDescription
statusstringNoAllFilter: scheduled, published, failed, cancelled
limitintegerNo50Max results (1-200)
python
queue = client.shorts.get_publish_queue(status="scheduled")
for item in queue:
    print(f"{item.clip_id}{item.platform} at {item.scheduled_at}")
typescript
const { queue } = await client.shorts.getPublishQueue({ status: "scheduled" });
queue.forEach(q => console.log(`${q.clipId} → ${q.platform}`));
go
payload := map[string]interface{}{
    "action": "v2_get_publish_queue",
    "status": "scheduled",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_get_publish_queue", "status": "scheduled"}'

Publishing Analytics

Overview of publishing performance.

POST https://s1.fotohub.app/functions/v1/shorts-process
{ "action": "v2_publish_analytics" }

Response:

json
{
  "published": 45,
  "scheduled": 3,
  "failed": 2,
  "by_platform": {
    "tiktok": 20,
    "youtube": 15,
    "instagram": 10
  }
}
python
analytics = client.shorts.get_publish_analytics()
print(f"Published: {analytics.published}, Scheduled: {analytics.scheduled}")
typescript
const analytics = await client.shorts.getPublishAnalytics();
console.log(`Published: ${analytics.published}`);
go
payload := map[string]interface{}{
    "action": "v2_publish_analytics",
}
body, _ := json.Marshal(payload)

req, _ := http.NewRequest("POST", "https://s1.fotohub.app/functions/v1/shorts-process", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer YOUR_SESSION_TOKEN")
req.Header.Set("Content-Type", "application/json")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Println(string(respBody))
bash
curl -X POST https://s1.fotohub.app/functions/v1/shorts-process \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action": "v2_publish_analytics"}'

Pipeline REST API

The Pipeline REST API is a stateless, step-based interface for building your own shorts pipeline or calling from the SDK. It is a completely separate system from the Console RPC documented above.

Base URLhttps://apis.fotohub.app
AuthAPI key — Authorization: Bearer fh_live_...
MethodPOST to /v1/shorts/<step>
StateNone. Each call takes a video_url and returns the step result. There are no server-side projects, clips, or job_ids — you pass the output URL of one step as the video_url of the next.

Every endpoint returns the same envelope: the operation name, credits charged, a billing summary, and the step-specific fields from the processing engine:

json
{
  "operation": "generate-clips",
  "credits_used": 5,
  "billing": { "success": true, "remaining_credits": 995 },
  "...": "step-specific fields"
}

Auth reminder

These endpoints use your fh_live_* API key, not a Supabase session token. Get your API key at fotohub.app/console.

Ingest

Ingest a source video into the pipeline (downloads and normalizes it).

POST /v1/shorts/ingest

Billing: 2 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the source video to process
titlestringNoOptional title for the video
bash
curl -X POST https://apis.fotohub.app/v1/shorts/ingest \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
    "title": "Best Moments"
  }'
python
import requests

resp = requests.post(
    "https://apis.fotohub.app/v1/shorts/ingest",
    headers={"Authorization": "Bearer fh_live_..."},
    json={"video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ", "title": "Best Moments"},
)
data = resp.json()
print(data["operation"], data["credits_used"])
typescript
const resp = await fetch("https://apis.fotohub.app/v1/shorts/ingest", {
  method: "POST",
  headers: {
    Authorization: "Bearer fh_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    video_url: "https://youtube.com/watch?v=dQw4w9WgXcQ",
    title: "Best Moments",
  }),
});
const data = await resp.json();
console.log(data.operation, data.credits_used);

Transcribe

Transcribe video audio with WhisperX (23 languages supported).

POST /v1/shorts/transcribe

Billing: 2 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the video to transcribe
languagestringNoautoSource language (auto or ISO code: en, pl, de, fr, es, ...)
modelstringNowhisperxTranscription model
bash
curl -X POST https://apis.fotohub.app/v1/shorts/transcribe \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://.../video.mp4", "language": "auto"}'

Detect Scenes

Detect scene changes using YOLOv8 and visual analysis.

POST /v1/shorts/detect-scenes

Billing: 3 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the video for scene detection
sensitivityfloatNo0.5Detection sensitivity, 0.11.0 (0.1 = fewer scenes, 1.0 = more)
bash
curl -X POST https://apis.fotohub.app/v1/shorts/detect-scenes \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://.../video.mp4", "sensitivity": 0.5}'

Generate Clips

AI-powered clip selection and ranking.

POST /v1/shorts/generate-clips

Billing: 5 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the source video
transcriptstringNoPre-computed transcript (if available)
num_clipsintegerNo5Number of clips to generate (120)
min_durationintegerNo15Minimum clip duration in seconds (560)
max_durationintegerNo60Maximum clip duration in seconds (15180)
stylestringNoviralClip style: viral, informative, storytelling, highlight
bash
curl -X POST https://apis.fotohub.app/v1/shorts/generate-clips \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://.../video.mp4",
    "num_clips": 5,
    "min_duration": 15,
    "max_duration": 60,
    "style": "viral"
  }'
python
import requests

resp = requests.post(
    "https://apis.fotohub.app/v1/shorts/generate-clips",
    headers={"Authorization": "Bearer fh_live_..."},
    json={
        "video_url": "https://.../video.mp4",
        "num_clips": 5,
        "min_duration": 15,
        "max_duration": 60,
        "style": "viral",
    },
)
print(resp.json())
typescript
const resp = await fetch("https://apis.fotohub.app/v1/shorts/generate-clips", {
  method: "POST",
  headers: {
    Authorization: "Bearer fh_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    video_url: "https://.../video.mp4",
    num_clips: 5,
    min_duration: 15,
    max_duration: 60,
    style: "viral",
  }),
});
console.log(await resp.json());

Captions

Generate animated captions for a video clip.

POST /v1/shorts/captions

Billing: 2 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the video
stylestringNokaraokeCaption style: karaoke, subtitle, bold, minimal, animated
languagestringNoautoCaption language
fontstringNoFont family for captions
positionstringNobottomPosition: top, center, bottom
bash
curl -X POST https://apis.fotohub.app/v1/shorts/captions \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://.../clip.mp4", "style": "karaoke", "position": "bottom"}'

Reframe

Smart reframe — change aspect ratio with intelligent subject tracking.

POST /v1/shorts/reframe

Billing: 3 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the source video
target_ratiostringNo9:16Target aspect ratio: 9:16, 1:1, 4:5, 16:9
focusstringNoautoFocus tracking: auto (face/subject), center, rule-of-thirds
bash
curl -X POST https://apis.fotohub.app/v1/shorts/reframe \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"video_url": "https://.../clip.mp4", "target_ratio": "9:16", "focus": "auto"}'

Render

Render a final short-form video with captions and effects.

POST /v1/shorts/render

Billing: 5 credits

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the source clip
captionsbooleanNotrueInclude captions
caption_stylestringNokaraokeCaption style
aspect_ratiostringNo9:16Output aspect ratio
qualitystringNohighRender quality: draft, medium, high
watermarkbooleanNofalseAdd FOTOhub watermark
bash
curl -X POST https://apis.fotohub.app/v1/shorts/render \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://.../clip.mp4",
    "captions": true,
    "caption_style": "karaoke",
    "aspect_ratio": "9:16",
    "quality": "high",
    "watermark": false
  }'

Agent Mode

Fully automated pipeline — input one video URL and receive multiple finished shorts. The agent runs the whole pipeline internally: ingest → transcribe → detect scenes → generate clips → captions → reframe → render.

POST /v1/shorts/agent

Billing: 15 credits (full pipeline)

ParameterTypeRequiredDefaultDescription
video_urlstringYesURL of the source video
num_shortsintegerNo3Number of shorts to generate (110)
stylestringNoviralContent style: viral, informative, storytelling, highlight
aspect_ratiostringNo9:16Output aspect ratio: 9:16, 1:1, 4:5
captionsbooleanNotrueAuto-generate captions
caption_stylestringNokaraokeCaption style for all clips
languagestringNoautoContent language
bash
curl -X POST https://apis.fotohub.app/v1/shorts/agent \
  -H "Authorization: Bearer fh_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
    "num_shorts": 3,
    "style": "viral",
    "aspect_ratio": "9:16",
    "captions": true,
    "caption_style": "karaoke",
    "language": "auto"
  }'
python
import requests

resp = requests.post(
    "https://apis.fotohub.app/v1/shorts/agent",
    headers={"Authorization": "Bearer fh_live_..."},
    json={
        "video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
        "num_shorts": 3,
        "style": "viral",
        "aspect_ratio": "9:16",
        "captions": True,
        "caption_style": "karaoke",
        "language": "auto",
    },
)
data = resp.json()
print(f"{data['operation']}{data['credits_used']} credits")
typescript
const resp = await fetch("https://apis.fotohub.app/v1/shorts/agent", {
  method: "POST",
  headers: {
    Authorization: "Bearer fh_live_...",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    video_url: "https://youtube.com/watch?v=dQw4w9WgXcQ",
    num_shorts: 3,
    style: "viral",
    aspect_ratio: "9:16",
    captions: true,
    caption_style: "karaoke",
    language: "auto",
  }),
});
const data = await resp.json();
console.log(`${data.operation} — ${data.credits_used} credits`);

Pricing & Credits

Pipeline REST API (/v1/shorts/*, API key):

OperationCreditsNotes
Ingest2Per video
Transcribe2Per video
Detect Scenes3Per video
Generate Clips5Per video (AI analysis)
Captions2Per video
Reframe3Per video
Render5Per clip
Agent Mode (full pipeline)15All steps combined

Console RPC (shorts-process, session token):

OperationCreditsNotes
Create Project0Free
Render Clip1-3Per clip (depends on quality)
Script Generation2Per script
Storyboard2Per storyboard
Video Generation (AI)15-50Per scene (model + resolution)
Article Scrape1Per article
Recap Script3Per script
B-Roll AI Suggest2Per clip
Series AI Split3Per split operation
Publish1Per platform per clip
Batch Render1-3Per clip

Subscription Plans

Higher-tier plans receive discounted credit rates. See fotohub.app/pricing for plan details.


Rate Limits

TierRequests/minConcurrent pipelinesNotes
Free101
Pro303
Business6010
EnterpriseCustomCustomContact sales

Rate limit headers are included in every response:

  • X-RateLimit-Limit — max requests per window
  • X-RateLimit-Remaining — remaining requests
  • X-RateLimit-Reset — window reset time (unix timestamp)

Error Codes

CodeMeaningCommon Cause
400Bad RequestInvalid parameters, missing required fields
401UnauthorizedInvalid or expired API key
403ForbiddenInsufficient credits, feature not enabled
404Not FoundProject/clip/series doesn't exist or belongs to another user
409ConflictPipeline already running for this project
422UnprocessableContent extraction failed (e.g., empty article)
429Rate LimitedToo many requests — wait and retry
500Server ErrorInternal error — retry or contact support
502Bad GatewayUpstream service unavailable (e.g., video download failed)

All errors return:

json
{
  "detail": "Human-readable error message"
}

Webhooks

Set a webhook_url when creating a project to receive status updates.

Webhook Payload:

json
{
  "event": "project.completed",
  "project_id": "uuid",
  "status": "completed",
  "clips_count": 5,
  "processing_time_ms": 45000,
  "timestamp": "2026-07-22T15:30:00Z"
}

Events:

  • project.started — Pipeline started
  • project.step_completed — Individual step finished
  • project.completed — All steps done, clips available
  • project.failed — Pipeline failed
  • publish.completed — Clip published to platform
  • publish.failed — Publishing failed

WebSocket Progress

Connect to WebSocket for real-time pipeline progress.

wss://gpu.fotohub.app/shorts-engine/ws/shorts/{project_id}

Messages received:

json
{"type": "step_start", "step": "transcribe", "progress": 0.2}
{"type": "step_complete", "step": "transcribe", "progress": 0.4}
{"type": "clip_found", "clip_id": "...", "score": 87, "title": "..."}
{"type": "complete", "clips_count": 5}
{"type": "error", "message": "..."}

Messages you can send:

  • "ping" — receive {"type": "pong"}
  • "cancel" — cancel pipeline

Supported Languages

CodeLanguageCodeLanguage
enEnglishfrFrench
esSpanishdeGerman
ptPortugueseitItalian
plPolishnlDutch
ruRussianukUkrainian
jaJapanesekoKorean
zhChinesearArabic
hiHinditrTurkish
svSwedishdaDanish
fiFinnishnoNorwegian
csCzechroRomanian
huHungarian

Caption Styles

StyleDescriptionBest For
karaokeWord-by-word highlight, bold popTikTok, Reels
subtitleClassic bottom subtitlesYouTube Shorts
boldLarge centered bold textMotivational, hooks
minimalSmall, clean, unobtrusiveProfessional content
animatedMotion graphics captionsCreative, trendy