Thumbnails
Thumbnails API
AntCDN supports two thumbnail approaches:
- Dynamic thumbnails (fastest): request a thumbnail directly from the worker URL.
- Generated thumbnails (API job): request a thumbnail to be generated and cached.
Overview
In most cases, dynamic thumbnails are enough. Use the job API if you want deterministic caching / tracking.
Dynamic thumbnails (worker URL)
Request a thumbnail directly from the worker:
https://worker.antcdn.net/v1/thumbnail/{orgID}/{envID}/{edgeKey}.{ext}?width=640&timeStamp=30Common query params:
width: output width in pixelstimeStamp: seconds into the video
Generated thumbnails (API job)
1) Request generation
/{orgID}/edge-ids/{edgeId}/thumbnails
Body:
{ "width": 320, "height": 180, "timestampMs": 12000}Response:
{ "body": { "jobId": "thumb_job_123456789abcdef", "url": "https://worker.antcdn.net/v1/thumbnail/{orgID}/{envID}/{edgeKey}.webp?width=320&timeStamp=12", "status": "pending" }}2) Poll job status
/{orgID}/thumbnails/{jobID}
Monitor the progress of thumbnail generation jobs.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
jobID | string | Yes | The job ID returned from thumbnail generation |
Response
{ "body": { "status": "completed", "url": "https://worker.antcdn.net/v1/thumbnail/{orgID}/{envID}/{edgeKey}.webp?width=320&timeStamp=12" }}Job Status Values
| Status | Description |
|---|---|
pending | Thumbnail generation is queued |
processing | Thumbnail is being generated |
completed | Thumbnail is ready and URL is available |
failed | Generation failed (check error logs) |
Example Request
curl -X GET "https://api.antcdn.net/v1/api/thumbnails/thumb_job_123456789abcdef" \ -H "X-Api-Key: hv_live_123456789abcdefghijklmnopqrstuvwxyz"const checkThumbnailStatus = async (jobId) => { const response = await fetch(`https://api.antcdn.net/v1/api/thumbnails/${jobId}`, { headers: { 'X-Api-Key': 'hv_live_123456789abcdefghijklmnopqrstuvwxyz' } }); return response.json();};Tip
If you already have a thumbnailUrl from video details, you can use that directly.
Private Video Thumbnails
For private videos, the thumbnailUrl is returned clean (without embedded tokens). To access:
Using signing keys (recommended for storage):
const signedUrl = `${video.thumbnailUrl}&token=${generateJWT(signingKey, keyId)}`;Using previewToken (quick access, expires ~1 hour):
const signedUrl = `${video.thumbnailUrl}&token=${video.previewToken}`;See Private Videos for signing key setup and JWT examples.
Best Practices
Thumbnail Dimensions
- Video Galleries: 320x180 (16:9) or 240x135 (16:9)
- Social Media: 1200x630 (Facebook) or 1200x675 (Twitter)
- Mobile Apps: 640x360 or 480x270
- High Quality: 1920x1080 for detailed previews
Timestamp Selection
- Video Start: Use
0for opening frame - Key Moments: Extract frames at important scenes
- Midpoint: Use
videoDurationMs / 2for middle frame - Custom: Choose specific timestamps for branded content
Error Handling
const generateThumbnailWithRetry = async (videoId, options, maxRetries = 3) => { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await generateThumbnail(videoId, options);
// Poll for completion let status = 'pending'; while (status === 'pending' || status === 'processing') { await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds const statusResponse = await checkThumbnailStatus(response.jobId); status = statusResponse.status;
if (status === 'completed') { return statusResponse.url; } else if (status === 'failed') { throw new Error('Thumbnail generation failed'); } } } catch (error) { if (attempt === maxRetries) throw error; await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff } }};Common Use Cases
Video Gallery Thumbnails
// Generate thumbnails for video galleryconst generateGalleryThumbnails = async (videos) => { const thumbnails = await Promise.all( videos.map(async (video) => { const response = await generateThumbnail(video.publicId, { width: 320, height: 180, timestampMs: 5000 // 5 seconds into video }); return { videoId: video.assetId, thumbnailUrl: response.url, jobId: response.jobId }; }) ); return thumbnails;};Social Media Previews
// Generate social media optimized thumbnailsconst generateSocialThumbnail = async (videoId) => { const response = await generateThumbnail(videoId, { width: 1200, height: 630, // Facebook optimal size timestampMs: 0 // Use opening frame }); return response.url;};Mobile App Thumbnails
// Generate mobile-optimized thumbnailsconst generateMobileThumbnail = async (videoId) => { const response = await generateThumbnail(videoId, { width: 640, height: 360, timestampMs: 10000 // 10 seconds into video }); return response.url;};Error Handling
Common Errors
| Status Code | Error | Description | Solution |
|---|---|---|---|
| 400 | Bad Request | Invalid dimensions or timestamp | Check width/height (1-4096) and timestamp (0 to video duration) |
| 401 | Unauthorized | Invalid API key | Verify API key is correct and active |
| 403 | Forbidden | Video doesn’t belong to organization | Ensure video belongs to your organization |
| 404 | Not Found | Video not found | Verify video public ID is correct |
| 422 | Unprocessable Entity | Timestamp exceeds video duration | Use timestamp within video length |
| 429 | Too Many Requests | Rate limit exceeded | Wait before making more requests |
Error Response Example
{ "title": "Bad Request", "status": 400, "detail": "Invalid thumbnail dimensions", "errors": [ { "message": "Width must be between 1 and 4096 pixels", "location": "body.width", "value": 5000 } ]}Rate Limits
| Operation | Limit | Window |
|---|---|---|
| Thumbnail generation | 50 requests | 1 hour |
| Status checks | 100 requests | 1 hour |
| Thumbnail retrieval | 1000 requests | 1 hour |
Tip
Thumbnail generation is processed asynchronously. Use the job ID to poll for completion status rather than making repeated generation requests.