Mockup API

Generate product mockups from PSD templates at scale. Send a template and your artwork, get back a finished mockup in seconds.

Base URL https://api.printed.app

Authentication

All API requests require an API key sent in the Authorization header.

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

POST /api/v1/process

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

ParameterTypeDescription
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.
Request
# 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

FieldTypeDescription
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
Response · 200
{
  "success": true,
  "resultUrl": "https://storage.printed.app/outputs/2025-01-15T12-30-abc.png?sig=...",
  "expiresIn": 3600,
  "processingTimeMs": 4523
}
Error response
{
  "success": false,
  "error": "PSD \"template.psd\" expects 2 poster(s) but only 1 image(s) provided.",
  "processingTimeMs": 1234
}

Health check

GET /health

Returns API status. No authentication required.

Response · 200
{ "status": "ok", "version": "1.0.0" }

Code examples

Full working examples in popular languages. Replace your_api_key with your actual key.

Node.js / TypeScript
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;
}
Python
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']
PHP
$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']));
}
cURL
# 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:

Request · 3 images
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

RequirementDetails
Placeholder typeSmart object layers
OrderingPlaceholders are filled left-to-right by their position in the template
Image countProvide exactly one image URL per placeholder slot
Supported effectsPerspective transforms, layer masks, drop shadows, blend modes
Max file sizeNo 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 psdIdentifier if 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.

OptionNotes
Cloudflare R2Free egress, fast globally. Use presigned URLs for private files.
AWS S3Widely supported. Use presigned URLs with at least 10 min expiry.
Any CDNCloudflare, Bunny, Fastly — anything that serves files over HTTPS.
Your own serverAs 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.

MetricValue
Average~7 seconds per mockup
P506.7 seconds
P958.3 seconds
Fastest5.3 seconds

Throughput

ApproachThroughput
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

LimitValue
Requests per minute60
Max images per request10
Request timeout120 seconds
Max concurrentAuto-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

CodeMeaningWhat 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.

Recommended backoff
Attempt 1: immediate
Attempt 2: wait 2 seconds
Attempt 3: wait 5 seconds