Veedeo API V3 Implementation Guide
Getting Started with V3 Last Updated: September 17, 2025
Table of Contents
- Quick Start
- API Structure
- Authentication
- Request Format
- Response Handling
- Webhook Implementation
- Code Examples
- Best Practices
- 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.