Skip to main content

cURL

Simple Text-to-Video

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "A cat walking in a garden"
  }'

Text-to-Video with Custom Settings

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Futuristic city with flying cars, neon lights, rain",
    "duration": "10",
    "aspect_ratio": "16:9",
    "model": "kling_o3_pro"
  }'

Image-to-Video

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Make the character wave and smile",
    "images_url": [
      "https://example.com/character.jpg"
    ],
    "duration": "5",
    "aspect_ratio": "9:16",
    "model": "kling_o3"
  }'

Portrait Video for Stories

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Person dancing energetically",
    "duration": "5",
    "aspect_ratio": "9:16",
    "model": "kling_o3_pro"
  }'

Multi-Shots Product Showcase

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Product showcase from different angles",
    "duration": "15",
    "model": "kling_o3_pro"
  }'

With Webhook

curl -X POST https://platform.runblob.io/v1/kling/o3-video/generate \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Dragon flying over mountains",
    "duration": "10",
    "model": "kling_o3_pro",
    "callback_url": "https://your-app.com/webhook/o3-video"
  }'

Check Status

curl -X GET https://platform.runblob.io/v1/kling/o3-video/generations/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer YOUR_API_KEY"

Python

Simple Client

import requests
import time
from typing import Optional, Dict

class KlingO3VideoClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://platform.runblob.io"
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
    
    def generate(self, prompt: str, **kwargs) -> Dict:
        """Generate video with O3 model"""
        response = requests.post(
            f"{self.base_url}/v1/kling/o3-video/generate",
            headers=self.headers,
            json={"prompt": prompt, **kwargs}
        )
        response.raise_for_status()
        return response.json()
    
    def get_status(self, generation_id: str) -> Dict:
        """Get generation status"""
        response = requests.get(
            f"{self.base_url}/v1/kling/o3-video/generations/{generation_id}",
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()
    
    def wait_for_completion(
        self, 
        generation_id: str, 
        max_wait: int = 900  # 15 minutes
    ) -> str:
        """Poll until video is ready"""
        intervals = [5, 5, 10, 15, 15]  # Progressive backoff
        start_time = time.time()
        attempt = 0
        
        while time.time() - start_time < max_wait:
            result = self.get_status(generation_id)
            
            if result["status"] == "completed":
                return result["video_url"]
            elif result["status"] == "failed":
                raise Exception("Generation failed")
            
            delay = intervals[min(attempt, len(intervals) - 1)]
            time.sleep(delay)
            attempt += 1
        
        raise TimeoutError("Generation timed out")

# Usage
client = KlingO3VideoClient("YOUR_API_KEY")

# Simple generation
result = client.generate(
    "Dragon flying over mountains at sunset",
    duration="10",
    model="kling_o3_pro"
)
print(f"Generation ID: {result['generation_id']}")

# Wait for completion
video_url = client.wait_for_completion(result["generation_id"])
print(f"Video ready: {video_url}")

Image-to-Video with Base64

import base64

def encode_image(image_path: str) -> str:
    """Encode image to base64"""
    with open(image_path, "rb") as f:
        return f"data:image/jpeg;base64,{base64.b64encode(f.read()).decode()}"

# Upload local image
client = KlingO3VideoClient("YOUR_API_KEY")
result = client.generate(
    "Animate this photo with smooth motion",
    images_base64=[encode_image("photo.jpg")],
    duration="5",
    mode="std"
)

video_url = client.wait_for_completion(result["generation_id"])
print(f"Video ready: {video_url}")

Advanced: With Error Handling

import requests
from typing import Optional
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class O3VideoClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.base_url = "https://platform.runblob.io"
    
    def generate(self, prompt: str, **kwargs) -> dict:
        """Generate video with comprehensive error handling"""
        try:
            # Validate inputs
            self._validate_request(prompt, **kwargs)
            
            response = requests.post(
                f"{self.base_url}/v1/kling/o3-video/generate",
                headers={
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                },
                json={"prompt": prompt, **kwargs},
                timeout=30
            )
            
            if response.status_code == 402:
                raise ValueError("INSUFFICIENT_CREDITS: Please top up your balance")
            elif response.status_code == 400:
                error_detail = response.json().get("detail", "Unknown error")
                raise ValueError(f"Invalid request: {error_detail}")
            
            response.raise_for_status()
            result = response.json()
            
            logger.info(f"✅ Generation started: {result['generation_id']}")
            return result
            
        except requests.exceptions.HTTPError as e:
            logger.error(f"HTTP error: {e}")
            raise
        except Exception as e:
            logger.error(f"Error: {e}")
            raise
    
    def _validate_request(self, prompt: str, **kwargs):
        """Validate request parameters"""
        if len(prompt) < 1 or len(prompt) > 2500:
            raise ValueError("Prompt must be 1-2500 characters")
        
        images_url = kwargs.get("images_url", [])
        images_base64 = kwargs.get("images_base64", [])
        total_images = len(images_url) + len(images_base64)
        
        if total_images > 5:
            raise ValueError("Maximum 5 images allowed")
        
        if images_url and images_base64:
            raise ValueError("Cannot provide both images_url and images_base64")
        
        duration = kwargs.get("duration")
        if duration and duration not in ["5", "10", "15"]:
            raise ValueError("Duration must be 5, 10, or 15 seconds")
        
        mode = kwargs.get("mode")
        if mode and mode not in ["std", "pro"]:
            raise ValueError("Mode must be 'std' or 'pro'")

# Usage with error handling
try:
    client = O3VideoClient("YOUR_API_KEY")
    result = client.generate(
        "Epic fantasy landscape with castles",
        duration="15",
        mode="pro"
    )
    print(f"✅ Generation started: {result['generation_id']}")
except ValueError as e:
    print(f"❌ Validation error: {e}")
except Exception as e:
    print(f"❌ Unexpected error: {e}")

JavaScript / Node.js

Simple Client

const fetch = require('node-fetch');

class KlingO3VideoClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://platform.runblob.io';
  }

  async generate(prompt, options = {}) {
    const response = await fetch(`${this.baseUrl}/v1/kling/o3-video/generate`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ prompt, ...options })
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`Generation failed: ${error.detail || 'Unknown error'}`);
    }

    return await response.json();
  }

  async getStatus(generationId) {
    const response = await fetch(
      `${this.baseUrl}/v1/kling/o3-video/generations/${generationId}`,
      {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      }
    );
    return await response.json();
  }

  async waitForCompletion(generationId, maxWait = 900000) { // 15 minutes
    const intervals = [5000, 5000, 10000, 15000, 15000]; // Progressive backoff
    const startTime = Date.now();
    let attempt = 0;
    
    while (Date.now() - startTime < maxWait) {
      const result = await this.getStatus(generationId);
      
      if (result.status === 'completed') {
        return result.video_url;
      } else if (result.status === 'failed') {
        throw new Error('Generation failed');
      }
      
      const delay = intervals[Math.min(attempt, intervals.length - 1)];
      await new Promise(resolve => setTimeout(resolve, delay));
      attempt++;
    }
    throw new Error('Generation timed out');
  }
}

// Usage
(async () => {
  const client = new KlingO3VideoClient('YOUR_API_KEY');
  
  // Simple generation
  const result = await client.generate(
    'Dragon flying over mountains at sunset',
    {
      duration: '10',
      model: 'kling_o3_pro'
    }
  );
  
  console.log('Generation ID:', result.generation_id);
  
  // Wait for completion
  const videoUrl = await client.waitForCompletion(result.generation_id);
  console.log('Video ready:', videoUrl);
})();

Image-to-Video with URLs

const client = new KlingO3VideoClient('YOUR_API_KEY');

const result = await client.generate(
  'Make the character wave and smile',
  {
    images_url: [
      'https://example.com/character.jpg'
    ],
    duration: '5',
    aspect_ratio: '9:16',
    model: 'kling_o3'
  }
);

const videoUrl = await client.waitForCompletion(result.generation_id);
console.log('✅ Video ready:', videoUrl);

With Base64 Upload

const fs = require('fs').promises;

async function encodeImage(imagePath) {
  const buffer = await fs.readFile(imagePath);
  return `data:image/jpeg;base64,${buffer.toString('base64')}`;
}

// Upload local images
const result = await client.generate(
  'Animate these photos with smooth transitions',
  {
    images_base64: [
      await encodeImage('photo1.jpg'),
      await encodeImage('photo2.jpg')
    ],
    duration: '10',
    model: 'kling_o3_pro'
  }
);

TypeScript with Types

type VideoDuration = '5' | '10' | '15';
type VideoModel = 'kling_o3' | 'kling_o3_pro';
type VideoAspectRatio = '16:9' | '9:16' | '1:1';
type VideoStatus = 'pending' | 'processing' | 'completed' | 'failed';

interface GenerateO3VideoRequest {
  prompt: string;
  images_base64?: string[];
  images_url?: string[];
  duration?: VideoDuration;
  aspect_ratio?: VideoAspectRatio;
  model?: VideoModel;
  keep_original_sound?: boolean;
  callback_url?: string;
}

interface GenerateO3VideoResponse {
  generation_id: string;
  status: VideoStatus;
  price: string;
}

interface O3VideoStatusResponse {
  generation_id: string;
  status: VideoStatus;
  prompt: string;
  video_url: string | null;
  model: string;
}

class TypedO3VideoClient {
  constructor(private apiKey: string) {}
  
  async generate(request: GenerateO3VideoRequest): Promise<GenerateO3VideoResponse> {
    const response = await fetch('https://platform.runblob.io/v1/kling/o3-video/generate', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(request)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${await response.text()}`);
    }
    
    return await response.json();
  }
  
  async getStatus(generationId: string): Promise<O3VideoStatusResponse> {
    const response = await fetch(
      `https://platform.runblob.io/v1/kling/o3-video/generations/${generationId}`,
      {
        headers: { 'Authorization': `Bearer ${this.apiKey}` }
      }
    );
    return await response.json();
  }
}

// Usage
const client = new TypedO3VideoClient('YOUR_API_KEY');

const result = await client.generate({
  prompt: 'Dragon flying over mountains',
  duration: '10',
  model: 'kling_o3_pro',
  aspect_ratio: '16:9'
});

console.log('Generation started:', result.generation_id);
const express = require('express');
const app = express();

app.use(express.json());

// Webhook endpoint
app.post('/webhook/o3-video', (req, res) => {
  const { generation_id, status, video_url, model, message } = req.body;
  
  if (status === 'completed') {
    console.log(`✅ Video ready for ${generation_id}: ${video_url} (${model})`);
    // Process the completed video
    processVideo(generation_id, video_url, model);
  } else {
    console.error(`❌ Generation failed for ${generation_id}: ${message}`);
    // Handle the error
    handleError(generation_id, message);
  }
  
  res.status(200).json({ status: 'received' });
});

function processVideo(generationId, videoUrl, model) {
  // Download video to your storage
  // Update database
  // Notify user via WebSocket/SSE
}

function handleError(generationId, errorCode) {
  // Log error for analytics
  // Update database
  // Notify user
}

// Generate with webhook
const client = new KlingO3VideoClient('YOUR_API_KEY');
const result = await client.generate(
  'Epic fantasy landscape',
  {
    duration: '15',
    model: 'kling_o3_pro',
    callback_url: 'https://your-app.com/webhook/o3-video'
  }
);

console.log('Generation started:', result.generation_id);
// Webhook will notify when complete

app.listen(3000);

Best Practices

Polling is inefficient and can hit rate limits. Use webhooks to receive instant notifications:
result = client.generate(
    prompt,
    duration="10",
    callback_url="https://your-app.com/webhook/o3-video"
)
Check parameters before sending to catch errors early:
function validateRequest(data) {
  if (data.prompt.length < 1 || data.prompt.length > 2500) {
    throw new Error("Invalid prompt length");
  }
  
  const totalImages = (data.images_url?.length || 0) + 
                     (data.images_base64?.length || 0);
  if (totalImages > 5) {
    throw new Error("Maximum 5 images allowed");
  }
  
  if (data.images_url && data.images_base64) {
    throw new Error("Cannot mix images_url and images_base64");
  }
}
Start with short intervals, increase gradually:
intervals = [5, 5, 10, 15, 15]  # seconds
for i, delay in enumerate(intervals):
    result = check_status(generation_id)
    if result['status'] in ['completed', 'failed']:
        break
    time.sleep(delay)
CDN URLs may expire after 24 hours. Download and save to your storage:
import requests

def download_video(url: str, save_path: str):
    response = requests.get(url, stream=True)
    with open(save_path, 'wb') as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)

# After generation completes
download_video(video_url, f'videos/{generation_id}.mp4')

UI Component Examples

Duration Selector

const durations = [
  { value: '5', label: '5 seconds', time: '~2-3 min' },
  { value: '10', label: '10 seconds', time: '~3-5 min' },
  { value: '15', label: '15 seconds', time: '~5-7 min' },
];

function DurationSelector({ value, onChange }) {
  return (
    <select value={value} onChange={(e) => onChange(e.target.value)}>
      {durations.map(d => (
        <option key={d.value} value={d.value}>
          {d.label} ({d.time})
        </option>
      ))}
    </select>
  );
}

Model/Quality Selector

const models = [
  { 
    value: 'kling_o3', 
    label: 'Standard (720p)', 
    description: 'Faster generation, lower cost',
    icon: '⚡'
  },
  { 
    value: 'kling_o3_pro', 
    label: 'Pro (1080p)', 
    description: 'Higher quality, ~2x price',
    icon: '⭐'
  },
];

function ModelSelector({ value, onChange }) {
  return (
    <div>
      {models.map(model => (
        <button
          key={mode.value}
          onClick={() => onChange(mode.value)}
          className={value === mode.value ? 'active' : ''}
        >
          <span>{mode.icon}</span>
          <div>
            <strong>{mode.label}</strong>
            <p>{mode.description}</p>
          </div>
        </button>
      ))}
    </div>
  );
}

Aspect Ratio Selector

const aspectRatios = [
  { value: '16:9', label: 'Landscape', icon: '🖼️', use: 'YouTube, Desktop' },
  { value: '9:16', label: 'Portrait', icon: '📱', use: 'Stories, TikTok' },
  { value: '1:1', label: 'Square', icon: '⬜', use: 'Instagram' },
];

function AspectRatioSelector({ value, onChange }) {
  return (
    <div className="aspect-ratio-grid">
      {aspectRatios.map(ratio => (
        <div
          key={ratio.value}
          onClick={() => onChange(ratio.value)}
          className={value === ratio.value ? 'selected' : ''}
        >
          <span className="icon">{ratio.icon}</span>
          <strong>{ratio.label}</strong>
          <small>{ratio.use}</small>
        </div>
      ))}
    </div>
  );
}