Skip to main content

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:

  1. Long Cache TTL - Images can be cached for 1 year (max-age=31536000)
  2. Automatic Invalidation - When you update an image, the timestamp changes
  3. New URL = New Cache - Browsers and CDN treat the new version as a different resource
  4. 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 proxies
  • max-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

ComponentDescriptionExample
cdn-domainDigitalOcean Spaces CDN endpointcdn.tradingcardapi.com
user-idOwning user's UUID (prefix only)user-550e8400
card-idCard UUID (prefix only)card-123e4567
image-idImage UUID (prefix only)img-789abcde
filenameImage filename with variant suffiximage_large.jpg
vVersion 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:

  1. Updates the updated_at timestamp
  2. Generates new version parameter
  3. 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 LocationCDN EdgeResponse Time
US East CoastUS East20-50ms
US West CoastUS West20-50ms
London, UKEU West30-60ms
SingaporeAsia Pacific40-80ms
Sydney, AustraliaAsia Pacific50-100ms
São Paulo, BrazilSouth America60-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:

  1. 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
  2. Invalid version parameter: URL may have malformed version

    • Verify ?v= parameter is a valid Unix timestamp
    • Re-fetch image metadata to get corrected URL
  3. 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:

  1. Old URL still in use: Application cached the old versioned URL

    • Fetch fresh image metadata from API
    • Verify version parameter has changed
  2. 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:

  1. Serving original images unnecessarily: Using full-size images for thumbnails

    • Use appropriate size variants (small/medium/large)
    • Implement responsive images properly
  2. 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