CDN Integration
Learn how the Trading Card API uses DigitalOcean Spaces CDN for fast global image delivery with automatic cache invalidation through versioned URLs. This guide covers CDN redirect behavior, cache headers, URL structure, and best practices for optimal performance.
Overview
The Card Images API integrates with DigitalOcean Spaces CDN to provide:
- Global Edge Delivery - Images served from locations closest to users worldwide
- Automatic Cache Invalidation - Versioned URLs eliminate manual cache purging
- Reduced Latency - Faster load times through edge caching
- Lower Server Load - API redirects to CDN instead of proxying files
- Optimized Caching - 1-year browser and CDN cache with immutable directive
All uploaded card images are automatically delivered via CDN with versioned URLs for intelligent cache management.
How It Works
1. Upload Flow
When you upload a card image:
2. Download Flow
When you request an image:
The API does not proxy image files. Instead, it returns a 302 redirect to the CDN URL, allowing clients to fetch directly from the nearest edge location.
Versioned URLs
What Are Versioned URLs?
Every image URL includes a version parameter based on the image's last update timestamp:
https://cdn.tradingcardapi.com/cards/abc123/image.jpg?v=1699564800
The ?v=1699564800 parameter represents the Unix timestamp when the image was last modified.
Why Version URLs?
Versioned URLs solve the cache invalidation problem:
- Long Cache TTL - Images can be cached for 1 year (max-age=31536000)
- Automatic Invalidation - When you update an image, the timestamp changes
- New URL = New Cache - Browsers and CDN treat the new version as a different resource
- No Manual Purging - No need to manually clear CDN or browser caches
Version Parameter Generation
The version parameter is automatically generated from the updated_at timestamp:
// Example: How version parameter is created
$timestamp = strtotime($image->updated_at); // e.g., 1699564800
$versionedUrl = $baseUrl . '?v=' . $timestamp;
When you update an image:
Old: https://cdn.tradingcardapi.com/cards/abc123/image.jpg?v=1699564800
New: https://cdn.tradingcardapi.com/cards/abc123/image.jpg?v=1699651200
Browsers and CDN will fetch the new version because the URL changed.
CDN Redirect Behavior
How Redirects Work
The download endpoint returns a 302 redirect instead of proxying the file:
Request:
GET /v1/card-images/abc123/download?size=medium
Authorization: Bearer YOUR_ACCESS_TOKEN
Response:
HTTP/2 302 Found
Location: https://cdn.tradingcardapi.com/cards/abc123/image_medium.jpg?v=1699564800
Your HTTP client automatically follows the redirect and fetches from the CDN.
Why Not Proxy?
Benefits of redirect over proxying:
- Lower API server load - API doesn't transfer image bytes
- Faster delivery - Client connects directly to nearest CDN edge
- Better scalability - CDN handles bandwidth, not your servers
- Global performance - Edge locations worldwide, not just API region
Handling Redirects in Code
Most HTTP clients follow redirects automatically:
PHP
<?php
$client = new GuzzleHttp\Client([
'allow_redirects' => true // Default behavior
]);
$imageId = 'abc123';
$response = $client->get("https://api.tradingcardapi.com/v1/card-images/{$imageId}/download", [
'query' => ['size' => 'medium'],
'headers' => ['Authorization' => 'Bearer ' . $accessToken]
]);
// $response->getBody() contains the image data from CDN
file_put_contents('card-image.jpg', $response->getBody());
JavaScript
const imageId = 'abc123';
const size = 'medium';
// fetch() follows redirects automatically
const response = await fetch(
`https://api.tradingcardapi.com/v1/card-images/${imageId}/download?size=${size}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
if (response.ok) {
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
// Use the image
document.getElementById('card-img').src = imageUrl;
}
Python
import requests
image_id = 'abc123'
size = 'medium'
# requests follows redirects by default
response = requests.get(
f'https://api.tradingcardapi.com/v1/card-images/{image_id}/download',
params={'size': size},
headers={'Authorization': f'Bearer {access_token}'}
)
if response.status_code == 200:
with open('card-image.jpg', 'wb') as f:
f.write(response.content)
Disabling Redirect Following
If you need the CDN URL without downloading the image:
curl -I "https://api.tradingcardapi.com/v1/card-images/abc123/download?size=medium" \
-H "Authorization: Bearer YOUR_TOKEN"
The Location header contains the versioned CDN URL.
Cache Headers
Response Headers
CDN-delivered images include these cache headers:
HTTP/2 200 OK
Content-Type: image/jpeg
Cache-Control: public, max-age=31536000, immutable
X-Cache: HIT
ETag: "abc123-1699564800"
Cache-Control Breakdown
public- Can be cached by browsers and intermediary proxiesmax-age=31536000- Cache for 1 year (31,536,000 seconds)immutable- Don't revalidate, even on refresh (URL change = new resource)
X-Cache Header
Indicates CDN cache status:
HIT- Served from CDN edge cache (fast!)MISS- Fetched from origin, now cached (first request)
ETag
Used for cache validation, though rarely needed with immutable caching:
ETag: "abc123-1699564800"
If you make a conditional request (If-None-Match), you'll get 304 Not Modified if the version hasn't changed.
URL Structure
Complete URL Format
https://{cdn-domain}/cards/{user-id}/{card-id}/{image-id}/{filename}?v={timestamp}
Example:
https://cdn.tradingcardapi.com/cards/user-550e8400/card-123e4567/img-789abcde/image_large.jpg?v=1699564800
URL Components
| Component | Description | Example |
|---|---|---|
cdn-domain | DigitalOcean Spaces CDN endpoint | cdn.tradingcardapi.com |
user-id | Owning user's UUID (prefix only) | user-550e8400 |
card-id | Card UUID (prefix only) | card-123e4567 |
image-id | Image UUID (prefix only) | img-789abcde |
filename | Image filename with variant suffix | image_large.jpg |
v | Version parameter (Unix timestamp) | 1699564800 |
Size Variant Filenames
Different thumbnail sizes use different filename suffixes:
- Original:
image.jpg - Large (600px):
image_large.jpg - Medium (300px):
image_medium.jpg - Small (150px):
image_small.jpg
All variants include the same version parameter for consistency.
API Response Format
Card Image Resource
When you fetch a card image, the response includes versioned CDN URLs:
{
"data": {
"type": "card_images",
"id": "789abcde-f012-3456-7890-abcdef123456",
"attributes": {
"card_id": "123e4567-e89b-12d3-a456-426614174000",
"image_type": "front",
"width": 1200,
"height": 1680,
"file_size": 245678,
"mime_type": "image/jpeg",
"download_url": "https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699564800",
"variants": {
"small": {
"url": "https://cdn.tradingcardapi.com/cards/.../image_small.jpg?v=1699564800",
"width": 150,
"height": 210,
"file_size": 8934,
"status": "completed"
},
"medium": {
"url": "https://cdn.tradingcardapi.com/cards/.../image_medium.jpg?v=1699564800",
"width": 300,
"height": 420,
"file_size": 24567,
"status": "completed"
},
"large": {
"url": "https://cdn.tradingcardapi.com/cards/.../image_large.jpg?v=1699564800",
"width": 600,
"height": 840,
"file_size": 78901,
"status": "completed"
}
},
"created_at": "2024-11-09T10:00:00Z",
"updated_at": "2024-11-09T10:00:00Z"
}
}
}
All URLs include the version parameter matching the updated_at timestamp.
Using Versioned URLs
Direct Image Display
Use the versioned CDN URLs directly in HTML:
<img
src="https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699564800"
alt="1989 Upper Deck Ken Griffey Jr. #1"
/>
Responsive Images with srcset
Combine versioned URLs with responsive image techniques:
<img
src="https://cdn.tradingcardapi.com/cards/.../image_small.jpg?v=1699564800"
srcset="
https://cdn.tradingcardapi.com/cards/.../image_small.jpg?v=1699564800 150w,
https://cdn.tradingcardapi.com/cards/.../image_medium.jpg?v=1699564800 300w,
https://cdn.tradingcardapi.com/cards/.../image_large.jpg?v=1699564800 600w
"
sizes="(max-width: 640px) 150px, 300px"
alt="Card image"
loading="lazy"
/>
See the Responsive Card Images guide for detailed examples.
Storing URLs in Your Database
You can store the full versioned URL:
-- Option 1: Store complete versioned URL
INSERT INTO card_images (image_id, cdn_url)
VALUES ('abc123', 'https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699564800');
Or store the image ID and fetch fresh URLs from the API when needed:
-- Option 2: Store image ID, fetch URL from API
INSERT INTO card_images (image_id, external_image_id)
VALUES ('local-123', 'abc123');
Recommendation: Store the image ID and fetch URLs from the API. This ensures you always have the latest version parameter if images are updated.
Cache Invalidation
Automatic Invalidation
When you update or replace an image, the API automatically:
- Updates the
updated_attimestamp - Generates new version parameter
- Returns new URLs with updated version
Example flow:
// Upload new image to replace existing
$response = $client->cardImages()->update($imageId, [
'file' => new \CURLFile('updated-card.jpg')
]);
// New URL with updated version
$newUrl = $response['data']['attributes']['download_url'];
// https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699651200
// ^ new timestamp
Old cached version remains in CDN/browsers but is effectively invalidated because your app now references the new URL.
No Manual Purging Needed
You do not need to:
- Manually purge CDN cache
- Clear browser caches
- Wait for cache expiration
The version parameter change creates a new cache entry automatically.
Cache Warm-up
After updating an image, the first request to the new versioned URL will be a cache MISS:
X-Cache: MISS
Subsequent requests will be cache HIT:
X-Cache: HIT
For critical images, you can "warm" the cache by making a request immediately after upload:
// After uploading/updating an image
$newUrl = $response['data']['attributes']['download_url'];
// Warm the cache
file_get_contents($newUrl);
// Now user requests will hit warm cache
Performance Optimization
1. Use Appropriate Size Variants
Always request the smallest variant that meets your needs:
// Card thumbnail in a list - use small
const thumbnailUrl = imageData.attributes.variants.small.url;
// Card detail view on mobile - use large
const detailUrl = imageData.attributes.variants.large.url;
// Full-screen zoom - use original
const fullsizeUrl = imageData.attributes.download_url;
2. Preload Critical Images
For above-the-fold images, use <link rel="preload">:
<head>
<link
rel="preload"
as="image"
href="https://cdn.tradingcardapi.com/cards/.../image_large.jpg?v=1699564800"
/>
</head>
3. Lazy Load Below-the-Fold Images
Use native lazy loading for images not immediately visible:
<img
src="https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699564800"
loading="lazy"
alt="Card image"
/>
4. Monitor Cache Hit Ratio
Track the X-Cache header to monitor CDN performance:
async function trackCachePerformance(imageUrl) {
const response = await fetch(imageUrl);
const cacheStatus = response.headers.get('X-Cache');
// Log to analytics
analytics.track('cdn_cache_status', {
url: imageUrl,
status: cacheStatus // HIT or MISS
});
}
Target: >90% cache hit ratio after initial warm-up period.
5. Leverage HTTP/2
CDN supports HTTP/2 for multiplexed connections:
// Multiple images can load in parallel over single connection
const imagePromises = imageIds.map(id =>
fetch(`https://cdn.tradingcardapi.com/cards/${id}/image.jpg?v=${version}`)
);
const images = await Promise.all(imagePromises);
Global Delivery
Edge Locations
DigitalOcean Spaces CDN includes global edge locations:
- North America: Multiple locations (US East, US West, Canada)
- Europe: Multiple locations (UK, Germany, Netherlands, etc.)
- Asia Pacific: Singapore, India, Australia
- South America: Brazil
Performance Expectations
Typical response times after cache warm-up:
| User Location | CDN Edge | Response Time |
|---|---|---|
| US East Coast | US East | 20-50ms |
| US West Coast | US West | 20-50ms |
| London, UK | EU West | 30-60ms |
| Singapore | Asia Pacific | 40-80ms |
| Sydney, Australia | Asia Pacific | 50-100ms |
| São Paulo, Brazil | South America | 60-120ms |
Compare to origin-only delivery (200-500ms globally).
Bandwidth Savings
CDN reduces origin bandwidth by 90-95%:
- Origin bandwidth: CDN fetching from DigitalOcean Spaces (cache MISS)
- CDN bandwidth: Users fetching from edge locations (cache HIT)
With 90% hit ratio, 10x more data is served from CDN than from origin, significantly reducing origin costs.
Integration Examples
PHP: Complete Implementation
<?php
require_once 'vendor/autoload.php';
use TradingCardAPI\Client;
$client = new Client([
'client_id' => getenv('TCAPI_CLIENT_ID'),
'client_secret' => getenv('TCAPI_CLIENT_SECRET')
]);
class CardImageRenderer {
private $client;
public function __construct($client) {
$this->client = $client;
}
/**
* Generate responsive image HTML with versioned CDN URLs
*/
public function renderResponsiveImage($imageId, $alt) {
// Fetch image metadata with CDN URLs
$response = $this->client->cardImages()->get($imageId);
$image = $response['data']['attributes'];
// All URLs include version parameter automatically
$small = $image['variants']['small']['url'];
$medium = $image['variants']['medium']['url'];
$large = $image['variants']['large']['url'];
return sprintf(
'<img src="%s" srcset="%s 150w, %s 300w, %s 600w" ' .
'sizes="(max-width: 640px) 150px, 300px" alt="%s" loading="lazy" />',
htmlspecialchars($small),
htmlspecialchars($small),
htmlspecialchars($medium),
htmlspecialchars($large),
htmlspecialchars($alt)
);
}
/**
* Warm CDN cache for all variants
*/
public function warmCache($imageId) {
$response = $this->client->cardImages()->get($imageId);
$image = $response['data']['attributes'];
// Request each variant to warm cache
$urls = [
$image['download_url'],
$image['variants']['small']['url'],
$image['variants']['medium']['url'],
$image['variants']['large']['url']
];
foreach ($urls as $url) {
// Make HEAD request to warm cache without downloading
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_exec($ch);
curl_close($ch);
}
}
}
// Usage
$renderer = new CardImageRenderer($client);
// Display card image with automatic versioned URLs
echo $renderer->renderResponsiveImage(
'abc123',
'1989 Upper Deck Ken Griffey Jr. #1'
);
// Warm cache after upload
$renderer->warmCache('abc123');
JavaScript: React Component
import React, { useEffect, useState } from 'react';
import { TradingCardAPI } from 'trading-card-api';
const client = new TradingCardAPI({
clientId: process.env.TCAPI_CLIENT_ID,
clientSecret: process.env.TCAPI_CLIENT_SECRET
});
function CardImage({ imageId, alt }) {
const [imageData, setImageData] = useState(null);
const [cacheStatus, setCacheStatus] = useState(null);
useEffect(() => {
async function loadImage() {
// Fetch image with versioned CDN URLs
const response = await client.cardImages.get(imageId);
const image = response.data.attributes;
setImageData(image);
// Track cache performance
const cdnResponse = await fetch(image.variants.medium.url, {
method: 'HEAD'
});
setCacheStatus(cdnResponse.headers.get('X-Cache'));
}
loadImage();
}, [imageId]);
if (!imageData) return <div>Loading...</div>;
return (
<div>
<img
src={imageData.variants.small.url}
srcSet={`
${imageData.variants.small.url} 150w,
${imageData.variants.medium.url} 300w,
${imageData.variants.large.url} 600w
`}
sizes="(max-width: 640px) 150px, 300px"
alt={alt}
loading="lazy"
/>
{cacheStatus && (
<small>CDN Cache: {cacheStatus}</small>
)}
</div>
);
}
export default CardImage;
Python: Image Manager
from trading_card_api import Client
import requests
class CardImageManager:
def __init__(self, client_id, client_secret):
self.client = Client(
client_id=client_id,
client_secret=client_secret
)
def get_versioned_urls(self, image_id):
"""
Get all versioned CDN URLs for an image
"""
response = self.client.card_images.get(image_id)
image = response['data']['attributes']
return {
'original': image['download_url'],
'large': image['variants']['large']['url'],
'medium': image['variants']['medium']['url'],
'small': image['variants']['small']['url'],
'version': image['updated_at']
}
def warm_cache(self, image_id):
"""
Warm CDN cache for all variants
"""
urls = self.get_versioned_urls(image_id)
for variant, url in urls.items():
if variant != 'version':
# HEAD request to warm cache
requests.head(url)
print(f"Warmed cache for {variant}: {url}")
def track_cache_performance(self, image_id):
"""
Monitor CDN cache hit ratio
"""
urls = self.get_versioned_urls(image_id)
stats = {}
for variant, url in urls.items():
if variant != 'version':
response = requests.head(url)
cache_status = response.headers.get('X-Cache', 'UNKNOWN')
stats[variant] = cache_status
return stats
# Usage
manager = CardImageManager(
client_id=os.getenv('TCAPI_CLIENT_ID'),
client_secret=os.getenv('TCAPI_CLIENT_SECRET')
)
# Get versioned URLs
urls = manager.get_versioned_urls('abc123')
print(f"Medium variant: {urls['medium']}")
# Warm cache after upload
manager.warm_cache('abc123')
# Monitor performance
stats = manager.track_cache_performance('abc123')
print(f"Cache stats: {stats}")
Troubleshooting
Images Not Loading from CDN
Symptoms: Images fail to load, browser shows broken image icon
Possible Causes:
-
CDN not configured: API may be returning local disk URLs
- Check that URLs start with CDN domain (e.g.,
cdn.tradingcardapi.com) - Contact support if CDN appears unavailable
- Check that URLs start with CDN domain (e.g.,
-
Invalid version parameter: URL may have malformed version
- Verify
?v=parameter is a valid Unix timestamp - Re-fetch image metadata to get corrected URL
- Verify
-
Network issues: CDN edge location may be temporarily unavailable
- Check DigitalOcean status page
- Try from different network/location
Solution:
// Implement fallback to download endpoint
async function loadImageWithFallback(imageId, size = 'medium') {
const apiUrl = `https://api.tradingcardapi.com/v1/card-images/${imageId}/download?size=${size}`;
try {
// Try CDN URL first
const response = await fetch(apiUrl);
if (response.ok) {
return await response.blob();
}
} catch (error) {
console.error('CDN load failed:', error);
}
// Fallback: download through API
return fetch(apiUrl).then(r => r.blob());
}
Cache Not Invalidating
Symptoms: Updated images still show old version
Possible Causes:
-
Old URL still in use: Application cached the old versioned URL
- Fetch fresh image metadata from API
- Verify version parameter has changed
-
Browser cache issue: Browser aggressively cached despite headers
- Hard refresh (Ctrl+Shift+R or Cmd+Shift+R)
- Clear browser cache
Solution:
// Always fetch latest URLs, don't cache them locally
function getCurrentImageUrl($imageId) {
// Don't store this URL - fetch fresh each time
$response = $client->cardImages()->get($imageId);
return $response['data']['attributes']['download_url'];
}
Slow First Load (Cache MISS)
Symptoms: First image load is slow, subsequent loads are fast
Explanation: This is normal behavior:
- First request = cache MISS (fetches from origin)
- Subsequent requests = cache HIT (serves from edge)
Solution: Warm cache proactively
# Warm cache immediately after upload
def upload_and_warm(card_id, image_path):
# Upload image
response = client.card_images.create(
card_id=card_id,
file_path=image_path
)
image_id = response['data']['id']
image_url = response['data']['attributes']['download_url']
# Warm cache immediately
requests.head(image_url)
return image_id
High CDN Costs
Symptoms: DigitalOcean CDN bandwidth charges are high
Possible Causes:
-
Serving original images unnecessarily: Using full-size images for thumbnails
- Use appropriate size variants (small/medium/large)
- Implement responsive images properly
-
Cache thrashing: Constantly changing version parameters
- Don't update images unless content actually changed
- Batch updates to reduce version churn
Solution:
// Use smallest variant that meets requirements
function getOptimalVariant(displayWidth) {
if (displayWidth <= 150) return 'small';
if (displayWidth <= 300) return 'medium';
if (displayWidth <= 600) return 'large';
return 'original';
}
const variant = getOptimalVariant(containerWidth);
const imageUrl = imageData.variants[variant].url;
Best Practices
1. Always Use Versioned URLs
Never strip the version parameter:
// ✅ GOOD: Keep version parameter
const url = "https://cdn.tradingcardapi.com/cards/.../image.jpg?v=1699564800";
// ❌ BAD: Removing version parameter
const url = "https://cdn.tradingcardapi.com/cards/.../image.jpg";
2. Fetch URLs Fresh, Don't Cache Them
Store image IDs, not URLs:
-- ✅ GOOD: Store image ID
CREATE TABLE cards (
id UUID PRIMARY KEY,
front_image_id UUID,
back_image_id UUID
);
-- ❌ BAD: Store versioned URL (becomes stale)
CREATE TABLE cards (
id UUID PRIMARY KEY,
front_image_url VARCHAR(512)
);
3. Warm Cache for Critical Images
Proactively warm cache after uploads:
// After uploading, warm cache
$response = $client->cardImages()->create([/*...*/]);
$imageUrl = $response['data']['attributes']['download_url'];
// Warm all variants
foreach (['small', 'medium', 'large'] as $variant) {
$url = $response['data']['attributes']['variants'][$variant]['url'];
file_get_contents($url); // Warms cache
}
4. Monitor Cache Performance
Track cache hit ratios in production:
// Log cache performance metrics
async function trackImageLoad(imageUrl) {
const start = performance.now();
const response = await fetch(imageUrl);
const duration = performance.now() - start;
analytics.track('image_load', {
url: imageUrl,
cache_status: response.headers.get('X-Cache'),
duration_ms: duration,
size_bytes: parseInt(response.headers.get('Content-Length'))
});
}
5. Use Appropriate Variants
Match variant size to display requirements:
// Match variant to use case
const sizes = {
thumbnail: 'small', // Card grid, 150px
detail: 'medium', // Card page, 300px
retina: 'large', // Detail on retina screens, 600px
fullscreen: 'original' // Lightbox/zoom, full size
};
Next Steps
- Responsive Card Images - Implement responsive images with CDN URLs
- Working with Images - Upload and manage card images
- API Reference - Card Images - Complete endpoint documentation
- Code Examples - Implementation examples in your language