Skip to main content

Overview

The NanoGPT API provides advanced video generation capabilities using state-of-the-art models. This guide covers how to use our video generation endpoints.

API Authentication

1. API Key Authentication

# Using x-api-key header
curl -H "x-api-key: YOUR_API_KEY"

# Using Bearer token
curl -H "Authorization: Bearer YOUR_API_KEY"

Making a Video Generation Request

Endpoint

POST /api/generate-video

Request Headers

Content-Type: application/json
x-api-key: YOUR_API_KEY  # Optional, for API key auth

Request Body

Basic Text-to-Video Request

{
  "model": "veo2-video",
  "prompt": "A majestic eagle soaring through mountain peaks at sunset",
  "duration": "5s",
  "aspect_ratio": "16:9"
}

Image-to-Video Request

Image-conditioned models accept either imageDataUrl (base64) or imageUrl (a public HTTPS link). The platform always uses the explicit field you send before falling back to any library attachments.
Uploads sent via the API must be 4 MB or smaller. For larger assets, host them externally and provide an imageUrl.
Base64 input
{
  "model": "kling-v21-standard",
  "prompt": "Make the person in the image wave hello",
  "imageDataUrl": "data:image/jpeg;base64,/9j/4AAQ...",
  "duration": "5",
  "aspect_ratio": "16:9"
}
Public URL input
{
  "model": "kling-v21-standard",
  "prompt": "Make the person in the image wave hello",
  "imageUrl": "https://assets.example.com/reference/wave-hello.jpg",
  "duration": "5",
  "aspect_ratio": "16:9"
}

Model-Specific Parameters

Veo Models

{
  "model": "veo2-video",
  "prompt": "Your prompt",
  "duration": "5s",  // 5s-30s for Veo2, fixed 8s for Veo3
  "aspect_ratio": "16:9"  // 16:9, 9:16, 1:1, 4:3, 3:4
}

Kling Models

{
  "model": "kling-video-v2",
  "prompt": "Your prompt",
  "duration": "5",  // "5" or "10"
  "aspect_ratio": "16:9",
  "negative_prompt": "blur, distortion",  // Optional
  "cfg_scale": 0.5  // 0-1, default 0.5
}

Hunyuan Models

{
  "model": "hunyuan-video",
  "prompt": "Your prompt",
  "pro_mode": false,  // true for higher quality (2x cost)
  "aspect_ratio": "16:9",
  "resolution": "720p",  // 480p, 720p, 1080p
  "num_frames": 129,  // 65, 97, 129
  "num_inference_steps": 20,  // 10-50
  "showExplicitContent": false  // Safety filter
}

Wan Image-to-Video

Accepts base64 via imageDataUrl or a public URL via imageUrl.
{
  "model": "wan-video-image-to-video",
  "prompt": "Your prompt",
  "imageDataUrl": "data:image/...",
  "num_frames": 81,  // 81-100
  "frames_per_second": 16,  // 5-24
  "resolution": "720p",  // 480p or 720p
  "num_inference_steps": 30,  // 1-40
  "negative_prompt": "blur, distortion",
  "seed": 42  // Optional
}

Seedance Models

Accepts base64 via imageDataUrl or a public URL via imageUrl. Ensure URLs are directly fetchable by the provider.
{
  "model": "seedance-video",
  "prompt": "Your prompt",
  "resolution": "1080p",  // 480p or 1080p (standard), 480p or 720p (lite)
  "duration": "5",  // "5" or "10"
  "aspect_ratio": "16:9",  // T2V only
  "camera_fixed": false,  // Static camera
  "seed": 42  // Optional
}

Response Format

Initial Response (202 Accepted)

{
  "runId": "fal-request-abc123xyz",
  "status": "pending",
  "model": "veo2-video",
  "cost": 2.5,
  "paymentSource": "USD",
  "remainingBalance": 47.5
}

Response Fields

  • runId: Unique identifier for polling status
  • status: Always “pending” for initial response
  • model: The model used for generation
  • cost: Cost in USD or XNO
  • paymentSource: “USD” or “XNO”
  • remainingBalance: Account balance after deduction

Polling for Status

After receiving a runId, poll the status endpoint until completion.

Status Endpoint

GET /api/generate-video/status?runId={runId}&modelSlug={model}

Polling Example

async function pollVideoStatus(runId, model) {
  const maxAttempts = 120; // ~10 minutes total
  const delayMs = 5000; // 5 seconds (max ~10 minutes)

  for (let i = 0; i < maxAttempts; i++) {
    const response = await fetch(
      `/api/generate-video/status?runId=${runId}&modelSlug=${model}`
    );
    const result = await response.json();

    if (result.data.status === 'COMPLETED') {
      return result.data.output.video.url;
    } else if (result.data.status === 'FAILED') {
      throw new Error(result.data.error || 'Video generation failed');
    }

    // Wait before next poll
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }

  throw new Error('Video generation timed out');
}

Status Response States

In Progress

{
  "data": {
    "status": "IN_PROGRESS",
    "request_id": "fal-request-abc123xyz",
    "details": "Video is being generated"
  }
}

Completed

{
  "data": {
    "status": "COMPLETED",
    "request_id": "fal-request-abc123xyz",
    "output": {
      "video": {
        "url": "https://storage.example.com/video.mp4"
      }
    }
  }
}

Failed

{
  "data": {
    "status": "FAILED",
    "request_id": "fal-request-abc123xyz",
    "error": "Content policy violation",
    "isNSFWError": true,
    "userFriendlyError": "Content flagged as inappropriate. Please modify your prompt and try again."
  }
}

Status Values

  • IN_QUEUE: Request is queued
  • IN_PROGRESS: Video is being generated
  • COMPLETED: Video ready for download
  • FAILED: Generation failed
  • CANCELLED: Request was cancelled

Complete Examples

The submit + poll flow works the same regardless of how you supply the image: image-conditioned models accept either imageDataUrl (base64) or a public imageUrl, and the platform prefers whichever field you send before checking library attachments.

Example 1: Text-to-Video with cURL

# 1) Submit
RUN_ID=$(curl -s -X POST https://nano-gpt.com/api/generate-video \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "veo2-video",
    "prompt": "A beautiful sunset over the ocean with waves",
    "duration": "5s",
    "aspect_ratio": "16:9"
  }' | jq -r '.runId')

echo "Run ID: $RUN_ID"

# 2) Poll status (max ~10 minutes)
for i in {1..120}; do
  RESP=$(curl -s "https://nano-gpt.com/api/generate-video/status?runId=$RUN_ID&modelSlug=veo2-video" \
    -H "x-api-key: YOUR_API_KEY")
  STATUS=$(echo "$RESP" | jq -r '.data.status // empty')
  echo "Attempt $i: status=$STATUS"
  if [ "$STATUS" = "COMPLETED" ]; then
    echo "Completed response:"
    echo "$RESP" | jq .
    VIDEO_URL=$(echo "$RESP" | jq -r '.data.output.video.url')
    echo "Video URL: $VIDEO_URL"
    break
  fi
  sleep 5
done

# Download when ready
# curl -L "$VIDEO_URL" -o output.mp4

Example 2: Image-to-Video with cURL

Base64 input

RUN_ID=$(curl -s -X POST https://nano-gpt.com/api/generate-video \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kling-v21-pro",
    "prompt": "Animate this scene with a slow dolly zoom",
    "imageDataUrl": "data:image/jpeg;base64,/9j/4AAQ...",
    "duration": "5",
    "aspect_ratio": "16:9"
  }' | jq -r '.runId')

Public URL input

RUN_ID=$(curl -s -X POST https://nano-gpt.com/api/generate-video \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kling-v21-pro",
    "prompt": "Animate this scene with a slow dolly zoom",
    "imageUrl": "https://images.unsplash.com/photo-1504196606672-aef5c9cefc92?w=1024",
    "duration": "5",
    "aspect_ratio": "16:9"
  }' | jq -r '.runId')
Use the same polling loop from Example 1 to monitor either request.

Example 3: Image-to-Video with JavaScript

// 1. Convert image to base64
async function imageToBase64(imageFile) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(imageFile);
  });
}

// 2. Submit video generation
async function generateVideo(imageFile) {
  const imageDataUrl = await imageToBase64(imageFile);
  
  const response = await fetch('/api/generate-video', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
      model: 'kling-v21-pro',
      prompt: 'Add gentle camera movement to this scene',
      imageDataUrl: imageDataUrl,
      duration: '5',
      aspect_ratio: '16:9'
    })
  });
  
  const result = await response.json();
  console.log('Video generation started:', result.runId);
  
  // 3. Poll for completion
  const videoUrl = await pollVideoStatus(result.runId, result.model);
  console.log('Video ready:', videoUrl);
  
  return videoUrl;
}

Using a public image URL directly

async function generateVideoFromUrl(imageUrl) {
  const response = await fetch('/api/generate-video', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': 'YOUR_API_KEY'
    },
    body: JSON.stringify({
      model: 'kling-v21-pro',
      prompt: 'Add gentle camera movement to this scene',
      imageUrl,
      duration: '5',
      aspect_ratio: '16:9'
    })
  });

  const result = await response.json();
  const videoUrl = await pollVideoStatus(result.runId, result.model);
  return videoUrl;
}

Example 4: Image-to-Video with Python

import base64
import json
import requests

API_URL = "https://nano-gpt.com/api/generate-video"
API_KEY = "YOUR_API_KEY"

def submit_image_to_video(image_path: str) -> str:
    with open(image_path, "rb") as image_file:
        encoded = base64.b64encode(image_file.read()).decode("utf-8")

    payload = {
        "model": "kling-v21-pro",
        "prompt": "Animate this scene with a slow dolly zoom",
        "imageDataUrl": f"data:image/jpeg;base64,{encoded}",
        "duration": "5",
        "aspect_ratio": "16:9",
    }
    response = requests.post(
        API_URL,
        headers={
            "x-api-key": API_KEY,
            "Content-Type": "application/json",
        },
        data=json.dumps(payload),
        timeout=60,
    )
    response.raise_for_status()
    return response.json()["runId"]
def submit_image_url(image_url: str) -> str:
    payload = {
        "model": "kling-v21-pro",
        "prompt": "Animate this scene with a slow dolly zoom",
        "imageUrl": image_url,
        "duration": "5",
        "aspect_ratio": "16:9",
    }
    response = requests.post(
        API_URL,
        headers={
            "x-api-key": API_KEY,
            "Content-Type": "application/json",
        },
        data=json.dumps(payload),
        timeout=60,
    )
    response.raise_for_status()
    return response.json()["runId"]
Reuse the polling helper from the JavaScript example (or your own status loop) to watch these run IDs until completion.

Example 5: Batch Processing

async function generateMultipleVideos(prompts) {
  // Submit all requests
  const requests = await Promise.all(
    prompts.map(async (prompt) => {
      const response = await fetch('/api/generate-video', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'x-api-key': 'YOUR_API_KEY'
        },
        body: JSON.stringify({
          model: 'seedance-lite-video',
          prompt: prompt,
          duration: '5',
          resolution: '720p'
        })
      });
      return response.json();
    })
  );
  
  // Poll all statuses concurrently
  const videos = await Promise.all(
    requests.map(({ runId, model }) => 
      pollVideoStatus(runId, model)
    )
  );
  
  return videos;
}

Error Handling

Common Error Responses

Insufficient Balance

{
  "error": "Insufficient balance",
  "status": 402
}

Invalid Session

{
  "error": "Invalid session",
  "status": 401
}

Rate Limit Exceeded

{
  "error": "Rate limit exceeded. Please wait before generating another video.",
  "status": 429
}

Content Policy Violation

{
  "error": {
    "message": "Your prompt was blocked due to safety concerns. Please modify your prompt.",
    "type": "CONTENT_POLICY_VIOLATION"
  },
  "status": 400
}

Model-Specific Errors

{
  "error": "Kling 2.1 Standard requires an input image. Please select an image to generate a video.",
  "status": 400
}

Error Handling Best Practices

async function generateVideoWithErrorHandling(params) {
  try {
    // Submit request
    const response = await fetch('/api/generate-video', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': 'YOUR_API_KEY'
      },
      body: JSON.stringify(params)
    });
    
    if (!response.ok) {
      const error = await response.json();
      
      // Handle specific error types
      if (response.status === 429) {
        console.error('Rate limited, retry after delay');
        // Implement exponential backoff
      } else if (response.status === 402) {
        console.error('Insufficient balance');
        // Prompt user to add credits
      } else if (error.error?.type === 'CONTENT_POLICY_VIOLATION') {
        console.error('Content policy violation');
        // Show user-friendly message
      }
      
      throw new Error(error.error?.message || error.error);
    }
    
    const result = await response.json();
    
    // Poll for status with timeout
    const videoUrl = await pollVideoStatus(result.runId, result.model);
    return videoUrl;
    
  } catch (error) {
    console.error('Video generation failed:', error);
    throw error;
  }
}

Rate Limits

  • Default: 50 requests per minute per IP address
  • API Key: Same as default
  • Internal Auth: No rate limit
Rate limits apply to the submission endpoint (/api/generate-video). Status polling endpoints have no rate limits.

Best Practices

  1. Choose the Right Model
    • Use text-to-video for creative generation
    • Use image-to-video for animating existing content
    • Consider cost vs quality tradeoffs
  2. Optimize Prompts
    • Be specific and descriptive
    • Include motion and camera directions
    • Avoid content policy violations
  3. Handle Async Operations
    • Implement proper polling with delays
    • Set reasonable timeouts (5-10 minutes)
    • Show progress to users
  4. Error Recovery
    • Implement retry logic for transient failures
    • Handle rate limits with exponential backoff
    • Provide clear error messages to users
  5. Cost Management
    • Check balance before submitting
    • Estimate costs before generation
    • Use shorter durations for testing