Skip to content
All Posts
Blog·March 5, 2026

How the Screenshots Live Render API Works — A Practical Guide for Store Listing Automation

A technical walkthrough of how the Screenshots Live render API processes templates, applies YAML overrides, and outputs store-ready screenshots — with real code examples and CI/CD patterns for whitelabel setups.

What This Post Covers

If you're shipping whitelabel apps — or even just one app with multiple store listings — you've probably hit the screenshot wall. Apple wants screenshots for every device size. Google Play wants its own set. Support three languages and you've tripled your workload. Now multiply that by however many branded variants you maintain.

The previous post covered why Screenshots Live exists. This one is the hands-on follow-up: how the render API is designed, what it expects, and how to wire it into your build pipeline so screenshots stop being a manual chore.

How the Render Pipeline Is Designed

The render system isn't a synchronous "send request, get image" endpoint. It's a job-based pipeline, which matters once you're batch-rendering dozens or hundreds of screenshots.

Here's the flow:

  1. You submit a render request with a template ID and optional YAML overrides (text, screenshots, device frames — anything marked as swappable).
  2. Validation — the system checks that your template exists, you own it, and every field you're overriding is actually allowed. URLs get validated against private IP ranges and restricted protocols.
  3. Queuing — the job enters a Redis-backed BullMQ queue. Your API call returns immediately with a jobId and Pending status. This is the key: you don't wait for the render to finish.
  4. Rendering — a Rust-based worker picks up the job, fetches the template and images (with LRU caching for repeated renders), loads fonts, renders the canvas using Skia, and generates the output.
  5. Packaging — the rendered images get zipped and uploaded to object storage.
  6. Download — you poll the status endpoint. When it's Completed, you hit the download endpoint for a presigned URL (valid for 1 hour).

The async design is intentional. When you need to render 50 templates for a release, you fire them all off, then collect results. No blocking, no timeouts.

Authentication

You need a Pro tier account to access the render API. Create an API key from your dashboard — it'll look like sa_live_.... Every request uses it as a Bearer token:

Authorization: Bearer sa_live_your_key_here

Templates: How They're Structured

Everything revolves around templates. A template is a canvas you build in the visual editor — you place text blocks, device frames, images, and set backgrounds. Each element on the canvas is an item with a unique UUID.

For automation, the critical concept is swappable fields. Not every property of every item is overridable via the API. An admin configures which fields can be swapped — text content, font family, font size, colors, screenshot URLs, device frame IDs, and so on. This is a deliberate constraint: it keeps API consumers from accidentally breaking the layout by changing positions or sizes that the designer locked in.

Think of it as a contract between your designer and your pipeline: the designer owns the layout, the pipeline owns the content.

The YAML Override System

This is the core of the API. Instead of rendering a template exactly as saved, you POST YAML that overrides specific fields on specific items.

Step 1: Get the YAML Scaffold

Every template has a scaffold endpoint that returns all swappable fields as commented YAML:

curl -H "Authorization: Bearer sa_live_your_key_here" \
  https://api.screenshots.live/templates/YOUR_TEMPLATE_ID/yaml

You'll get something like:

templateId: "550e8400-e29b-41d4-a716-446655440000"
items:
  - itemId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
    type: Text
    # text: "Your headline here"
    # fontFamily: "Inter"
    # fontSize: 48
    # color: "#FFFFFF"
  - itemId: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
    type: DeviceFrame
    # screenshotUrl: "https://..."
    # frameId: "..."

Uncomment what you want to override. Leave the rest alone — those fields keep their template defaults.

Step 2: Submit the Render

curl -X POST https://api.screenshots.live/render/api \
  -H "Authorization: Bearer sa_live_your_key_here" \
  -H "Content-Type: text/yaml" \
  -d '
templateId: "550e8400-e29b-41d4-a716-446655440000"
items:
  - itemId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
    type: Text
    text: "Track your deliveries"
    color: "#1A73E8"
  - itemId: "a1b2c3d4-5678-90ab-cdef-1234567890ab"
    type: DeviceFrame
    screenshotUrl: "https://your-cdn.com/screenshots/home-screen.png"
'

Response:

{
  "data": {
    "jobId": "d4e5f6a7-8901-2345-6789-0abcdef12345",
    "templateId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "Pending",
    "createdAt": "2026-03-05T10:30:00.000Z"
  }
}

Step 3: Poll for Status

curl -H "Authorization: Bearer sa_live_your_key_here" \
  https://api.screenshots.live/render/api/JOB_ID

The status progresses through PendingActiveCompleted (or Failed if something went wrong). For most templates, rendering takes a few seconds.

Step 4: Download

curl -H "Authorization: Bearer sa_live_your_key_here" \
  https://api.screenshots.live/render/JOB_ID/download

Returns a presigned URL. The download is a ZIP containing your rendered images.

Uploading Screenshots Directly

You don't need to host your screenshots somewhere just to pass a URL. The /render/api/with-pictures endpoint accepts multipart uploads:

curl -X POST https://api.screenshots.live/render/api/with-pictures \
  -H "Authorization: Bearer sa_live_your_key_here" \
  -F 'yaml=templateId: "YOUR_TEMPLATE_ID"
items:
  - itemId: "DEVICE_FRAME_ITEM_ID"
    type: DeviceFrame
    screenshotUrl: "picture://home-screen.png"' \
  -F 'pictures=@./screenshots/home-screen.png'

The picture:// protocol references uploaded files by filename. You can attach up to 200 files, each up to 10MB. This is what makes the API practical for CI/CD — your pipeline captures screenshots, and you send them straight to the render without needing intermediate storage.

Real-World Example: Whitelabel Pipeline

Here's a concrete scenario. You maintain 5 whitelabel brands, each needing App Store screenshots in 3 languages across 5 key screens. That's 75 rendered images per release.

Your designer has created one template per screen in the editor, with the headline text and device frame marked as swappable. Your CI pipeline (GitLab, GitHub Actions, whatever) captures raw screenshots per brand and locale using Fastlane's snapshot/screengrab.

Here's the script that ties it together:

#!/bin/bash

API_KEY="sa_live_your_key_here"
API_BASE="https://api.screenshots.live"

BRANDS=("acme" "globex" "initech" "umbrella" "stark")
LOCALES=("en" "de" "es")
SCREENS=("home" "profile" "search" "settings" "checkout")

# Template IDs from the editor
declare -A TEMPLATES
TEMPLATES[home]="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
TEMPLATES[profile]="aaaaaaaa-bbbb-cccc-dddd-ffffffffffff"
# ... etc

# Localized headlines (loaded from your i18n files)
declare -A HEADLINES
HEADLINES[home:en]="Track your deliveries"
HEADLINES[home:de]="Verfolge deine Lieferungen"
HEADLINES[home:es]="Rastrea tus entregas"
# ... etc

JOB_IDS=()

# Submit all render jobs
for brand in "${BRANDS[@]}"; do
  for locale in "${LOCALES[@]}"; do
    for screen in "${SCREENS[@]}"; do
      HEADLINE="${HEADLINES[$screen:$locale]}"
      BRAND_COLOR=$(cat "brands/$brand/primary-color.txt")

      RESPONSE=$(curl -s -X POST "$API_BASE/render/api/with-pictures" \
        -H "Authorization: Bearer $API_KEY" \
        -F "yaml=templateId: \"${TEMPLATES[$screen]}\"
items:
  - itemId: \"TEXT_ITEM_UUID\"
    type: Text
    text: \"$HEADLINE\"
    color: \"$BRAND_COLOR\"
  - itemId: \"FRAME_ITEM_UUID\"
    type: DeviceFrame
    screenshotUrl: \"picture://${screen}.png\"" \
        -F "pictures=@./artifacts/$brand/$locale/${screen}.png")

      JOB_ID=$(echo "$RESPONSE" | jq -r '.data.jobId')
      JOB_IDS+=("$JOB_ID|$brand|$locale|$screen")
      echo "Dispatched: $brand/$locale/$screen → $JOB_ID"
    done
  done
done

echo "Submitted ${#JOB_IDS[@]} jobs. Polling for results..."

# Poll and download results
for entry in "${JOB_IDS[@]}"; do
  IFS='|' read -r job_id brand locale screen <<< "$entry"

  while true; do
    STATUS=$(curl -s -H "Authorization: Bearer $API_KEY" \
      "$API_BASE/render/api/$job_id" | jq -r '.status')

    if [ "$STATUS" = "Completed" ]; then
      URL=$(curl -s -H "Authorization: Bearer $API_KEY" \
        "$API_BASE/render/$job_id/download" | jq -r '.downloadUrl')
      mkdir -p "./output/$brand/$locale"
      curl -s -o "./output/$brand/$locale/${screen}.zip" "$URL"
      echo "Done: $brand/$locale/$screen"
      break
    elif [ "$STATUS" = "Failed" ]; then
      echo "FAILED: $brand/$locale/$screen"
      break
    fi
    sleep 2
  done
done

75 render jobs, all dispatched in parallel, then polled and downloaded. In a real pipeline you'd add retry logic and maybe parallelize the polling, but this shows the pattern. Plug this into your GitLab CI .gitlab-ci.yml or GitHub Actions workflow and the whole flow — from code push to store-ready screenshots — runs without human intervention.

Swapping Device Frames

Device frames are the mockup overlays — iPhone 16 Pro, Pixel 9, iPad Air, Galaxy S24. They're preloaded in your account and referenced by UUID. The same template can render with different device frames by overriding the frameId:

items:
  - itemId: "FRAME_ITEM_UUID"
    type: DeviceFrame
    screenshotUrl: "picture://home.png"
    frameId: "IPHONE_16_PRO_FRAME_UUID"

This is how you handle the App Store vs Google Play split. Same template layout, same screenshot, different frame. Submit two render jobs — one with the iPhone frame, one with the Pixel frame — and you've covered both stores from one template.

Rate Limits and Quotas

Some numbers to plan around:

  • Render API: 5 requests per 60 seconds
  • Status polling: 60 requests per 60 seconds
  • Daily renders: 100 per day on Pro tier
  • Picture uploads: max 200 files per request, 10MB each

For big whitelabel setups, the rate limit on render submissions is the bottleneck. Pace your requests with a small delay between batches. The async design helps — submissions are fast, the actual rendering happens in the background.

YAML Validation

The YAML parser is deliberately strict: no anchors, no aliases, no custom tags, max 1MB. If something's wrong, the error message tells you exactly what — wrong field name, non-swappable field, invalid UUID format, URL pointing to a private IP range. It's designed to fail fast and fail clearly, which matters when you're debugging a CI pipeline at 11 PM.

What About the OpenAPI Spec?

If you prefer generating a typed client instead of writing curl commands, the full OpenAPI spec is available at:

https://api.screenshots.live/render/openapi.json

And there's an interactive Swagger UI at:

https://api.screenshots.live/render/docs

Both require Pro tier authentication. The spec covers all render endpoints, DTOs, and error responses — feed it into openapi-generator or orval and you'll have a typed client in TypeScript, Python, Go, or whatever you're working with.

Wrapping Up

The render API is built for one specific workflow: your designer creates templates in the visual editor, and your code fills them with the right content at build time. For whitelabel setups where the same template needs to work across dozens of brands and languages, the YAML override system means you maintain one template per screen layout instead of one per brand-language-device combination.

If you've got the raw screenshots covered (Fastlane, Maestro, manual captures — doesn't matter) and you need the processing step automated, this is what Screenshots Live is for. The pipeline captures screenshots, the API renders them into store-ready images, and Fastlane uploads them. No designer in the loop for routine releases.