Documentation

Veedeo API V3 Implementation Guide

Getting Started with V3 Last Updated: September 17, 2025

Table of Contents

  1. Quick Start
  2. API Structure
  3. Authentication
  4. Request Format
  5. Response Handling
  6. Webhook Implementation
  7. Code Examples
  8. Best Practices
  9. Testing Your Implementation

Quick Start

1. Get Your API Key

Visit your Veedeo Dashboard to generate an API key.

2. Basic Request

curl -X POST https://api.veedeo.dev/v3/tasks \
  -H "Authorization: Bearer your_api_key" \
  -H "Content-Type: application/json" \
  -H "X-Veedeo-Version: 2025-01-17" \
  -d '{
    "version": "3.0",
    "request_id": "unique_request_123",
    "input": {
      "timeline": {
        "duration_ms": 10000,
        "tracks": [
          {
            "id": "video_track",
            "type": "video",
            "clips": [
              {
                "id": "clip_1",
                "media_url": "https://example.com/image.jpg",
                "start_time_ms": 0,
                "end_time_ms": 10000,
                "properties": {
                  "scale": {"x": 1.0, "y": 1.0},
                  "opacity": 1.0,
                  "position": {"x": 0, "y": 0},
                  "fit": "contain"
                }
              }
            ]
          }
        ]
      },
      "output": {
        "resolution": {"width": 1920, "height": 1080},
        "framerate": 30,
        "format": "mp4",
        "quality": "high"
      }
    },
    "webhook": {
      "url": "https://your-app.com/webhook",
      "events": ["task.completed"]
    }
  }'

API Structure

Base URL and Versioning

Base URL: https://api.veedeo.dev/v3
Version Header: X-Veedeo-Version: 2025-01-17

Core Endpoints

POST /v3/tasks           # Create render task
GET  /v3/tasks/{task_id} # Get task status
GET  /v3/tasks           # List tasks
DELETE /v3/tasks/{task_id} # Cancel task
POST /v3/tasks/{task_id}/retry # Retry task

Authentication

All requests require Bearer token authentication:

const headers = {
  'Authorization': 'Bearer your_api_key',
  'Content-Type': 'application/json',
  'X-Veedeo-Version': '2025-01-17'
};

Request Format

Required Structure

{
  "version": "3.0",
  "request_id": "unique_request_id",
  "input": {
    "timeline": { /* timeline definition */ },
    "output": { /* output configuration */ }
  },
  "webhook": { /* webhook configuration */ },
  "metadata": { /* optional metadata */ }
}

Timeline Structure

{
  "timeline": {
    "duration_ms": 15000,
    "tracks": [
      {
        "id": "track_id",
        "type": "video|audio|subtitle",
        "clips": [
          {
            "id": "clip_id",
            "media_url": "https://example.com/media.jpg",
            "start_time_ms": 0,
            "end_time_ms": 5000,
            "properties": { /* clip-specific properties */ }
          }
        ]
      }
    ]
  }
}

Output Configuration

{
  "output": {
    "resolution": {
      "width": 1920,
      "height": 1080
    },
    "framerate": 30,
    "format": "mp4",
    "quality": "high",
    "codec": "h264"
  }
}

Response Handling

Success Response

{
  "task_id": "tsk_1234567890abcdef",
  "status": "queued",
  "created_at": "2025-01-17T10:00:00Z",
  "estimated_duration_seconds": 120,
  "links": {
    "self": "/v3/tasks/tsk_1234567890abcdef"
  }
}

Error Response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid timeline configuration",
    "details": [
      {
        "field": "input.timeline.duration_ms",
        "issue": "Must be positive integer"
      }
    ],
    "suggested_action": "Fix validation errors and retry"
  }
}

Webhook Implementation

Configuration

{
  "webhook": {
    "url": "https://your-app.com/webhook",
    "events": ["task.started", "task.completed", "task.failed"],
    "secret": "your_webhook_secret"
  }
}

Event Handler

app.post('/webhook', (req, res) => {
  const { event, task_id, data } = req.body;

  // Verify signature
  if (!verifySignature(req.body, req.headers['x-veedeo-signature'], secret)) {
    return res.status(401).send('Invalid signature');
  }

  switch (event) {
    case 'task.completed':
      const { video_url, thumbnail_url } = data.output;
      // Handle completion
      break;
    case 'task.failed':
      const { error } = data;
      // Handle failure
      break;
  }

  res.status(200).json({ received: true });
});

Code Examples

JavaScript Client

class VeedeoClient {
  constructor(apiKey) {
    this.apiKey = apiKey;
    this.baseUrl = 'https://api.veedeo.dev/v3';
  }

  async createRender(renderRequest) {
    const response = await fetch(`${this.baseUrl}/renders`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        'X-Veedeo-Version': '2025-01-17'
      },
      body: JSON.stringify(renderRequest)
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.error.message);
    }

    return response.json();
  }

  async getTask(taskId) {
    const response = await fetch(`${this.baseUrl}/renders/${taskId}`, {
      headers: {
        'Authorization': `Bearer ${this.apiKey}`,
        'X-Veedeo-Version': '2025-01-17'
      }
    });

    return response.json();
  }
}

Python Client

import requests

class VeedeoClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = 'https://api.veedeo.dev/v3'
        self.headers = {
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json',
            'X-Veedeo-Version': '2025-01-17'
        }

    def create_render(self, render_request):
        response = requests.post(
            f'{self.base_url}/renders',
            headers=self.headers,
            json=render_request
        )
        response.raise_for_status()
        return response.json()

    def get_task(self, task_id):
        response = requests.get(
            f'{self.base_url}/renders/{task_id}',
            headers=self.headers
        )
        return response.json()

Media Fit Implementation

Understanding Media Fit Modes

The fit parameter is essential for handling different aspect ratios between your media assets and target canvas. Here's how to implement it effectively.

Automatic Fit Mode Selection

function selectOptimalFitMode(mediaAspectRatio, canvasAspectRatio) {
  const aspectDifference = Math.abs(mediaAspectRatio - canvasAspectRatio);

  // If aspect ratios are very similar (within 10%), use contain to avoid cropping
  if (aspectDifference < 0.1) {
    return 'contain';
  }

  // For social media content, prefer cover to avoid black bars
  if (canvasAspectRatio < 1) { // Portrait canvas
    return mediaAspectRatio > 1 ? 'cover' : 'contain';
  } else { // Landscape canvas
    return mediaAspectRatio < 1 ? 'cover' : 'contain';
  }
}

// Usage example for your specific case
const mediaWidth = 1440, mediaHeight = 1440;  // Square image
const canvasWidth = 1080, canvasHeight = 1920; // Vertical canvas

const mediaAspect = mediaWidth / mediaHeight;   // 1.0
const canvasAspect = canvasWidth / canvasHeight; // 0.5625

const recommendedFit = selectOptimalFitMode(mediaAspect, canvasAspect);
console.log(recommendedFit); // "cover" - fills canvas without black bars

Building a Fit Mode Preview System

interface FitPreviewConfig {
  mediaUrl: string;
  canvasSize: { width: number; height: number };
  fitMode: 'contain' | 'cover' | 'fill' | 'stretch';
}

class FitModePreviewGenerator {
  async generatePreview(config: FitPreviewConfig): Promise<string> {
    const previewRequest = {
      version: '3.0',
      request_id: `preview_${Date.now()}_${config.fitMode}`,
      input: {
        timeline: {
          duration_ms: 1000, // 1-second preview
          tracks: [{
            id: 'preview_track',
            type: 'video',
            clips: [{
              id: 'preview_clip',
              media_url: config.mediaUrl,
              start_time_ms: 0,
              end_time_ms: 1000,
              properties: {
                fit: config.fitMode,
                scale: { x: 1.0, y: 1.0 },
                position: { x: 0, y: 0 },
                opacity: 1.0
              }
            }]
          }]
        },
        metadata: {
          resolution: config.canvasSize,
          framerate: 1
        }
      },
      output: {
        format: 'mp4',
        quality: 'draft' // Fast preview generation
      },
      webhook: {
        url: 'https://your-app.com/webhooks/preview'
      }
    };

    const response = await fetch('https://api.veedeo.dev/v3/tasks', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer your_api_key',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(previewRequest)
    });

    const task = await response.json();
    return task.task_id;
  }
}

React Component for Fit Mode Selection

import React, { useState, useEffect } from 'react';

const FitModeSelector = ({ onFitModeChange, mediaInfo, canvasSize }) => {
  const [selectedFit, setSelectedFit] = useState('contain');
  const [previewUrls, setPreviewUrls] = useState({});

  const fitModes = [
    {
      value: 'contain',
      label: 'Contain',
      description: 'Show complete media, may add black bars',
      icon: '⬜'
    },
    {
      value: 'cover',
      label: 'Cover',
      description: 'Fill canvas completely, may crop media',
      icon: '⬛'
    },
    {
      value: 'fill',
      label: 'Fill',
      description: 'Stretch to fit exactly, may distort',
      icon: '🔄'
    }
  ];

  const handleFitModeChange = (fitMode) => {
    setSelectedFit(fitMode);
    onFitModeChange(fitMode);
  };

  // Auto-suggest optimal fit mode
  useEffect(() => {
    if (mediaInfo && canvasSize) {
      const mediaAspect = mediaInfo.width / mediaInfo.height;
      const canvasAspect = canvasSize.width / canvasSize.height;
      const optimal = selectOptimalFitMode(mediaAspect, canvasAspect);
      setSelectedFit(optimal);
      onFitModeChange(optimal);
    }
  }, [mediaInfo, canvasSize]);

  return (
    <div className="fit-mode-selector">
      <h3>Media Fit Mode</h3>
      <div className="fit-options">
        {fitModes.map((mode) => (
          <label key={mode.value} className="fit-option">
            <input
              type="radio"
              name="fitMode"
              value={mode.value}
              checked={selectedFit === mode.value}
              onChange={() => handleFitModeChange(mode.value)}
            />
            <div className="option-content">
              <span className="icon">{mode.icon}</span>
              <div className="text">
                <strong>{mode.label}</strong>
                <small>{mode.description}</small>
              </div>
            </div>
          </label>
        ))}
      </div>

      {/* Preview section */}
      <div className="fit-preview">
        <p>
          <strong>Your case:</strong> 1440×1440 image → 1080×1920 canvas
        </p>
        <div className="preview-grid">
          <div className={`preview-option ${selectedFit === 'contain' ? 'active' : ''}`}>
            <strong>contain:</strong> Black bars top/bottom
          </div>
          <div className={`preview-option ${selectedFit === 'cover' ? 'active' : ''}`}>
            <strong>cover:</strong> Fills completely, crops sides ✅
          </div>
          <div className={`preview-option ${selectedFit === 'fill' ? 'active' : ''}`}>
            <strong>fill:</strong> May stretch square to rectangle
          </div>
        </div>
      </div>
    </div>
  );
};

export default FitModeSelector;

Validation and Error Prevention

function validateFitConfiguration(clip, canvasSize) {
  const { fit, scale, position } = clip.properties;

  // Validate fit mode
  const validFitModes = ['contain', 'cover', 'fill', 'stretch'];
  if (fit && !validFitModes.includes(fit)) {
    throw new Error(`Invalid fit mode: ${fit}. Must be one of: ${validFitModes.join(', ')}`);
  }

  // Warn about potential quality issues
  if (fit === 'fill' || fit === 'stretch') {
    console.warn(
      `Using ${fit} mode may cause distortion. ` +
      `Consider 'cover' for social media or 'contain' for presentations.`
    );
  }

  // Check for common mistakes
  if (fit === 'cover' && scale && (scale.x !== 1.0 || scale.y !== 1.0)) {
    console.warn(
      `Using 'cover' fit with custom scale may cause unexpected results. ` +
      `Consider using 'contain' with scale for precise control.`
    );
  }

  return true;
}

Best Practices

1. Use Request IDs for Idempotency

const requestId = 'req_1735776000000_abc123def';

const renderRequest = {
  version: '3.0',
  request_id: requestId,
  // ... rest of request
};

2. Implement Proper Error Handling

try {
  const result = await client.createRender(renderRequest);
  console.log('Render created:', result.task_id);
} catch (error) {
  if (error.code === 'VALIDATION_ERROR') {
    // Fix validation errors
  } else if (error.code === 'RATE_LIMIT_EXCEEDED') {
    // Implement backoff
  }
}

3. Handle Webhooks Securely

function verifySignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return signature === `sha256=${expectedSignature}`;
}

4. Download Files Before Expiry

async function handleCompletion(taskId, output) {
  // Files expire after 24 hours
  const { video_url, thumbnail_url, expires_at } = output;

  // Download immediately
  await downloadFile(video_url, `renders/${taskId}.mp4`);
  await downloadFile(thumbnail_url, `thumbnails/${taskId}.jpg`);
}

Testing Your Implementation

1. Test with Simple Request

const testRequest = {
  version: '3.0',
  request_id: 'test_001',
  input: {
    timeline: {
      duration_ms: 5000,
      tracks: [
        {
          id: 'test_track',
          type: 'video',
          clips: [
            {
              id: 'test_clip',
              media_url: 'https://storage.googleapis.com/veedeo-test-assets/sample.jpg',
              start_time_ms: 0,
              end_time_ms: 5000,
              properties: {
                scale: { x: 1.0, y: 1.0 },
                opacity: 1.0,
                position: { x: 0, y: 0 }
              }
            }
          ]
        }
      ]
    },
    output: {
      resolution: { width: 1280, height: 720 },
      framerate: 30,
      format: 'mp4',
      quality: 'standard'
    }
  }
};

const result = await client.createRender(testRequest);
console.log('Test render created:', result.task_id);

2. Monitor Task Progress

async function monitorTask(taskId) {
  let status = 'queued';

  while (status !== 'completed' && status !== 'failed') {
    await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds

    const task = await client.getTask(taskId);
    status = task.status;

    console.log(`Task ${taskId} status: ${status}`);

    if (task.progress) {
      console.log(`Progress: ${task.progress.percentage}%`);
    }
  }

  return status;
}

3. Test Webhook Endpoint

Use tools like webhook.site or ngrok to test webhook delivery:

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Expose it publicly
ngrok http 3000

# Use the HTTPS URL in webhook configuration

Validation Checklist

  • API key is valid and has proper permissions
  • Request includes required headers
  • Timeline structure is valid
  • Media URLs are publicly accessible
  • Webhook endpoint returns 2xx status codes
  • Signature verification is implemented
  • Error handling covers all error codes
  • File download is implemented before expiry

Ready to start building? Check out our Complete API Reference for detailed documentation.