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

This is the private-pilot contract. The branded endpoint uses the managed Vercel Sandbox deployment. General rollout still requires compatibility, output-privacy, and production-capacity verification.

Authentication

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

Header
Authorization: Bearer your_api_key

Keys are provisioned manually for compatibility-approved private pilots. Self-service signup is not available. Keep the assigned key secret.

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. Provide a PSD URL. Managed revisions are a preview contract and will require an assigned, activated revision. 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
URL source
string HTTPS URL to your PSD template. Must be publicly accessible or a presigned URL.
templateId
managed source
string Assigned managed template ID. Send with templateRevision instead of psdUrl.
templateRevision
managed source
string Assigned immutable revision. Mutable latest aliases are not accepted.
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 URL-source template name for slot count lookup. Defaults to the filename from psdUrl. Useful when your URL contains a hashed filename rather than the original.
imageFit
optional
enum cover (default), contain, or legacy stretch. Cover and contain preserve artwork aspect ratio.
Request
# Single-image 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"],
    "imageFit": "cover"
  }'

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 effectsCompatibility-tested perspective transforms, masks, shadows, and common blend modes
Max file size200 MiB per PSD and 50 MiB per artwork image

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 signed result URL expires after 1 hour. URL expiry does not delete the object; each pilot agreement defines storage retention and deletion.

Pilot performance

Local saved-composite checks and repeatability runs do not establish hosted latency or throughput. Historical Fly figures are not a service-level promise for the Vercel Sandbox deployment.

MetricValue
ExecutionOne isolated Vercel Sandbox per render
Local reconstruction33/33 admitted cases pass saved-composite checks
Local repeatability429/429 renders across 33 cases, 13 successful runs each
Hosted latency and throughputNot yet qualified
Pilot concurrencyAssigned after hosted validation
Representative gate500 successful renders required; not completed
Burst gate10,000 renders with less than 1% terminal failure required; permission-gated and not completed

Use asynchronous batches and respect the rate and concurrency limits assigned to your pilot.

Rate limits

LimitValue
Requests per minuteAssigned in the pilot agreement
Max images per request10
Request timeout120 seconds
Max concurrentAssigned after hosted validation

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
422 Input or compatibility class rejected Submit an admitted 8-bit RGB PSD and supported artwork.
429 Rate limited Wait, then retry
500 Server error Retry after a few seconds. If persistent, contact us.
503 Isolated renderer unavailable Retry with backoff.

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