Mockup API
Generate product mockups from PSD templates at scale. Send a template and your artwork, get back a finished mockup in seconds.
Authentication
All API requests require an API key sent in the Authorization header.
Authorization: Bearer your_api_key
Your API key is provided when you sign up. Keep it secret — it grants full access to your account.
Do not expose your API key in client-side code or public repositories.
Generate mockup
Composite one or more images into a PSD template. The API detects smart object placeholder layers, perspective-transforms your images into them, and returns a download URL for the finished PNG.
Request body
| Parameter | Type | Description |
|---|---|---|
|
psdUrl required |
string | HTTPS URL to your PSD template. Must be publicly accessible or a presigned URL. |
|
imageUrls required |
string[] | Array of HTTPS URLs to the images to place into the template. One per placeholder slot, ordered left-to-right. Max 10. |
|
psdIdentifier optional |
string | Template name for slot count lookup. Defaults to the filename from psdUrl. Useful when your URL contains a hashed filename rather than the original. |
# Single-image mockup (poster in a frame) curl -X POST https://api.printed.app/api/v1/process \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "psdUrl": "https://your-cdn.com/templates/frame-mockup.psd", "imageUrls": ["https://your-cdn.com/images/poster.png"] }'
Response
| Field | Type | Description |
|---|---|---|
| success | boolean | Whether processing succeeded |
| resultUrl | string | Presigned download URL for the rendered mockup PNG. Expires in 1 hour. |
| expiresIn | number | Seconds until the download URL expires (3600) |
| processingTimeMs | number | Server processing time in milliseconds |
{ "success": true, "resultUrl": "https://storage.printed.app/outputs/2025-01-15T12-30-abc.png?sig=...", "expiresIn": 3600, "processingTimeMs": 4523 }
{ "success": false, "error": "PSD \"template.psd\" expects 2 poster(s) but only 1 image(s) provided.", "processingTimeMs": 1234 }
Health check
Returns API status. No authentication required.
{ "status": "ok", "version": "1.0.0" }
Code examples
Full working examples in popular languages. Replace your_api_key with your actual key.
async function generateMockup(psdUrl, imageUrls) { const response = await fetch('https://api.printed.app/api/v1/process', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your_api_key', }, body: JSON.stringify({ psdUrl, imageUrls }), }); const data = await response.json(); if (!data.success) { throw new Error(`Mockup failed: ${data.error}`); } // Download the result to your own storage const image = await fetch(data.resultUrl); const buffer = await image.arrayBuffer(); // Save or upload to your CDN fs.writeFileSync('mockup.png', Buffer.from(buffer)); return data.resultUrl; }
import requests def generate_mockup(psd_url: str, image_urls: list[str]) -> str: response = requests.post( 'https://api.printed.app/api/v1/process', headers={ 'Authorization': 'Bearer your_api_key', 'Content-Type': 'application/json', }, json={ 'psdUrl': psd_url, 'imageUrls': image_urls, }, timeout=120, ) data = response.json() if not data.get('success'): raise Exception(f"Mockup failed: {data.get('error')}") # Download the result img = requests.get(data['resultUrl']) with open('mockup.png', 'wb') as f: f.write(img.content) return data['resultUrl']
$ch = curl_init('https://api.printed.app/api/v1/process'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer your_api_key', 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'psdUrl' => 'https://your-cdn.com/templates/frame.psd', 'imageUrls' => ['https://your-cdn.com/images/poster.png'], ]), ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if ($response['success']) { // Download result file_put_contents('mockup.png', file_get_contents($response['resultUrl'])); }
# Generate a mockup curl -X POST https://api.printed.app/api/v1/process \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "psdUrl": "https://your-cdn.com/templates/frame-mockup.psd", "imageUrls": ["https://your-cdn.com/images/poster.png"] }' # Download the result curl -o mockup.png "PASTE_RESULT_URL_HERE"
Multi-image mockup
For PSD templates with multiple placeholder slots (e.g., a gallery wall with 3 frames), provide one image URL per slot:
curl -X POST https://api.printed.app/api/v1/process \ -H "Authorization: Bearer your_api_key" \ -H "Content-Type: application/json" \ -d '{ "psdUrl": "https://your-cdn.com/templates/gallery-3-frame.psd", "imageUrls": [ "https://your-cdn.com/images/left-poster.png", "https://your-cdn.com/images/center-poster.png", "https://your-cdn.com/images/right-poster.png" ], "psdIdentifier": "Gallery Wall 3-Frame.psd" }'
PSD templates
Your PSD templates must use smart object layers as image placeholders. The API automatically detects these and composites your artwork into them with perspective correction.
Requirements
| Requirement | Details |
|---|---|
| Placeholder type | Smart object layers |
| Ordering | Placeholders are filled left-to-right by their position in the template |
| Image count | Provide exactly one image URL per placeholder slot |
| Supported effects | Perspective transforms, layer masks, drop shadows, blend modes |
| Max file size | No hard limit, though larger PSDs take longer to process |
Tips
- Name placeholder layers clearly — layers named "INPUT" or "Change Poster" are auto-detected
- Test with one template first before processing in bulk
- Match image count to placeholder slots exactly
- Use
psdIdentifierif your URL uses a hashed or obfuscated filename - PNG input images are recommended for best quality
Hosting your files
You host your own PSD templates and input images. They just need to be accessible via HTTPS URL.
| Option | Notes |
|---|---|
| Cloudflare R2 | Free egress, fast globally. Use presigned URLs for private files. |
| AWS S3 | Widely supported. Use presigned URLs with at least 10 min expiry. |
| Any CDN | Cloudflare, Bunny, Fastly — anything that serves files over HTTPS. |
| Your own server | As long as files are downloadable via HTTPS. |
URLs must be HTTPS. HTTP URLs are rejected. If using presigned URLs, ensure they have at least 10 minutes of validity.
The result mockup is hosted on our storage for 1 hour. Download it or re-upload to your own storage within that window.
Performance
Benchmarked with real PSD templates (17–18 MB) on production infrastructure.
| Metric | Value |
|---|---|
| Average | ~7 seconds per mockup |
| P50 | 6.7 seconds |
| P95 | 8.3 seconds |
| Fastest | 5.3 seconds |
Throughput
| Approach | Throughput |
|---|---|
| Sequential | ~8–10 mockups/minute |
| 5 concurrent | ~20 mockups/minute |
| 10 concurrent | ~22 mockups/minute |
For best batch throughput, send 5–10 requests concurrently. Higher concurrency is fine but won't increase throughput — requests queue automatically.
First request after idle has a ~2–3 second cold start while a server spins up. Subsequent requests are immediate.
Rate limits
| Limit | Value |
|---|---|
| Requests per minute | 60 |
| Max images per request | 10 |
| Request timeout | 120 seconds |
| Max concurrent | Auto-scaled |
If you hit the rate limit, you'll receive a 429 response. Wait a moment and retry.
Error handling
Always check the success field in the response body, even on 200 status codes.
HTTP status codes
| Code | Meaning | What to do |
|---|---|---|
| 200 | Success (check success field) |
Download the resultUrl |
| 400 | Bad request | Check request body — missing fields or invalid URLs |
| 401 | Unauthorized | Check your API key |
| 429 | Rate limited | Wait, then retry |
| 500 | Server error | Retry after a few seconds. If persistent, contact us. |
Retry strategy
Most errors are deterministic (bad PSD, wrong image count) and won't resolve on retry. Only retry on 500 or timeout.
Attempt 1: immediate Attempt 2: wait 2 seconds Attempt 3: wait 5 seconds