GrabShot Blog

How to Generate Device Frame Mockups with an API

February 19, 2026 · 8 min read

You need an iPhone screenshot mockup for your landing page. Or a MacBook frame around your app's dashboard for a pitch deck. Maybe you're generating hundreds of app store preview images programmatically.

Whatever the reason, manually opening Figma and placing screenshots into device frames doesn't scale. In this guide, we'll build an automated pipeline that captures a website screenshot and wraps it in a realistic device frame - all through API calls.

Why Automate Device Frame Mockups?

Device mockups are everywhere in SaaS marketing: landing pages, social media posts, app store listings, investor decks. The manual process looks something like this:

  1. Take a screenshot or screen recording
  2. Open a design tool (Figma, Sketch, Photoshop)
  3. Find a device frame template
  4. Resize and position the screenshot
  5. Export the final image

That's fine for one image. But when you need to generate mockups for 50 different pages, update them every time your UI changes, or create them on the fly for user-generated content, you need automation.

Common use cases for automated device mockups:

Step 1: Capture the Screenshot

Before you can frame it, you need a clean screenshot at the right resolution. The key is matching the viewport size to the target device's screen resolution.

Here are the viewport sizes for popular devices:

Using GrabShot's screenshot API, you can capture at any viewport size:

curl

curl "https://grabshot.dev/api/screenshot?url=https://example.com&width=393&height=852&fullPage=false" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -o iphone-screenshot.png

Node.js

const fetch = require('node-fetch');
const fs = require('fs');

async function captureForDevice(url, device) {
  const viewports = {
    'iphone-15-pro':    { width: 393, height: 852 },
    'iphone-15-max':    { width: 430, height: 932 },
    'galaxy-s24':       { width: 360, height: 780 },
    'macbook-pro-14':   { width: 1512, height: 982 },
    'browser-1440':     { width: 1440, height: 900 },
  };

  const vp = viewports[device];
  if (!vp) throw new Error(`Unknown device: ${device}`);

  const params = new URLSearchParams({
    url,
    width: vp.width,
    height: vp.height,
    fullPage: 'false',
  });

  const res = await fetch(
    `https://grabshot.dev/api/screenshot?${params}`,
    { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
  );

  const buffer = Buffer.from(await res.arrayBuffer());
  const filename = `${device}-screenshot.png`;
  fs.writeFileSync(filename, buffer);
  console.log(`Saved ${filename} (${buffer.length} bytes)`);
  return filename;
}

// Capture for all devices at once
const devices = ['iphone-15-pro', 'macbook-pro-14', 'browser-1440'];
for (const device of devices) {
  await captureForDevice('https://your-app.com', device);
}

Python

import requests

VIEWPORTS = {
    "iphone-15-pro":   (393, 852),
    "iphone-15-max":   (430, 932),
    "galaxy-s24":      (360, 780),
    "macbook-pro-14":  (1512, 982),
    "browser-1440":    (1440, 900),
}

def capture_for_device(url: str, device: str, api_key: str) -> str:
    w, h = VIEWPORTS[device]
    resp = requests.get(
        "https://grabshot.dev/api/screenshot",
        params={"url": url, "width": w, "height": h, "fullPage": "false"},
        headers={"X-Api-Key": api_key},
    )
    resp.raise_for_status()
    filename = f"{device}-screenshot.png"
    with open(filename, "wb") as f:
        f.write(resp.content)
    print(f"Saved {filename} ({len(resp.content)} bytes)")
    return filename

# Generate mockups for multiple devices
for device in ["iphone-15-pro", "macbook-pro-14"]:
    capture_for_device("https://your-app.com", device, "YOUR_API_KEY")

Step 2: Add the Device Frame

Once you have a screenshot at the correct resolution, you need to composite it into a device frame. There are two main approaches:

Option A: Sharp (Node.js)

Sharp is the fastest image processing library for Node.js. You'll need a transparent PNG of the device frame with a cutout where the screen goes.

const sharp = require('sharp');

async function addDeviceFrame(screenshotPath, framePath, outputPath, screenPosition) {
  // screenPosition: { left, top, width, height } - where the screen sits in the frame
  const screenshot = await sharp(screenshotPath)
    .resize(screenPosition.width, screenPosition.height, { fit: 'fill' })
    .toBuffer();

  // Composite: frame on top of screenshot positioned correctly
  const frame = sharp(framePath);
  const frameMeta = await frame.metadata();

  await sharp({
    create: {
      width: frameMeta.width,
      height: frameMeta.height,
      channels: 4,
      background: { r: 0, g: 0, b: 0, alpha: 0 },
    },
  })
    .composite([
      { input: screenshot, left: screenPosition.left, top: screenPosition.top },
      { input: framePath, left: 0, top: 0 },
    ])
    .png()
    .toFile(outputPath);

  console.log(`Mockup saved to ${outputPath}`);
}

// Example: iPhone 15 Pro frame
addDeviceFrame('iphone-screenshot.png', 'frames/iphone-15-pro.png', 'mockup.png', {
  left: 18,
  top: 18,
  width: 393,
  height: 852,
});

Option B: Pillow (Python)

from PIL import Image

def add_device_frame(screenshot_path, frame_path, output_path, screen_box):
    """
    screen_box: (left, top, width, height) where the screen sits in the frame
    """
    frame = Image.open(frame_path).convert("RGBA")
    screenshot = Image.open(screenshot_path).convert("RGBA")

    left, top, width, height = screen_box
    screenshot = screenshot.resize((width, height), Image.LANCZOS)

    # Create canvas, paste screenshot, then frame on top
    canvas = Image.new("RGBA", frame.size, (0, 0, 0, 0))
    canvas.paste(screenshot, (left, top))
    canvas = Image.alpha_composite(canvas, frame)
    canvas.save(output_path)
    print(f"Mockup saved to {output_path}")

add_device_frame(
    "iphone-screenshot.png",
    "frames/iphone-15-pro.png",
    "mockup.png",
    (18, 18, 393, 852),
)

Step 3: Where to Get Device Frame Assets

You need high-quality transparent PNGs of device frames. Here are reliable sources:

For browser frames specifically, you don't even need an image. You can generate one with pure code:

const sharp = require('sharp');

async function addBrowserFrame(screenshotPath, outputPath, title = '') {
  const screenshot = sharp(screenshotPath);
  const meta = await screenshot.metadata();

  const barHeight = 40;
  const padding = 2;
  const totalWidth = meta.width + padding * 2;
  const totalHeight = meta.height + barHeight + padding;

  // Create a simple browser chrome with SVG
  const browserChrome = Buffer.from(`
    
      
      
      
      
      
      
      ${title}
    
  `);

  const screenshotBuf = await screenshot.toBuffer();

  await sharp(browserChrome)
    .composite([
      { input: screenshotBuf, left: padding, top: barHeight },
    ])
    .png()
    .toFile(outputPath);

  console.log(`Browser mockup saved to ${outputPath}`);
}

addBrowserFrame('browser-screenshot.png', 'browser-mockup.png', 'your-app.com');

Putting It All Together: Full Pipeline

Here's a complete Node.js script that captures a URL across multiple devices and generates framed mockups:

const fetch = require('node-fetch');
const sharp = require('sharp');
const fs = require('fs');

const API_KEY = process.env.GRABSHOT_API_KEY;
const API_URL = 'https://grabshot.dev/api/screenshot';

const DEVICES = {
  'iphone-15-pro': {
    viewport: { width: 393, height: 852 },
    frame: 'frames/iphone-15-pro.png',
    screen: { left: 18, top: 18, width: 393, height: 852 },
  },
  'browser': {
    viewport: { width: 1440, height: 900 },
    frame: null, // use CSS browser frame
    screen: null,
  },
};

async function generateMockups(url) {
  const results = [];

  for (const [name, config] of Object.entries(DEVICES)) {
    // 1. Capture screenshot
    const params = new URLSearchParams({
      url,
      width: config.viewport.width,
      height: config.viewport.height,
      fullPage: 'false',
    });

    const res = await fetch(`${API_URL}?${params}`, {
      headers: { 'X-Api-Key': API_KEY },
    });

    const screenshotBuf = Buffer.from(await res.arrayBuffer());
    const screenshotPath = `temp-${name}.png`;
    fs.writeFileSync(screenshotPath, screenshotBuf);

    // 2. Add frame
    const outputPath = `mockup-${name}.png`;

    if (config.frame) {
      // Device frame compositing
      const screenshot = await sharp(screenshotPath)
        .resize(config.screen.width, config.screen.height, { fit: 'fill' })
        .toBuffer();

      const frameMeta = await sharp(config.frame).metadata();
      await sharp({
        create: {
          width: frameMeta.width,
          height: frameMeta.height,
          channels: 4,
          background: { r: 0, g: 0, b: 0, alpha: 0 },
        },
      })
        .composite([
          { input: screenshot, left: config.screen.left, top: config.screen.top },
          { input: config.frame, left: 0, top: 0 },
        ])
        .png()
        .toFile(outputPath);
    } else {
      // Browser frame (generated)
      await addBrowserFrame(screenshotPath, outputPath, new URL(url).hostname);
    }

    results.push({ device: name, path: outputPath });
    fs.unlinkSync(screenshotPath); // cleanup temp file
  }

  return results;
}

// Usage
generateMockups('https://your-app.com').then(console.log);

Tips for Better Mockups

Need screenshots at scale?

GrabShot captures pixel-perfect screenshots at any viewport size. 25 free captures per month, no credit card required.

Try It Free

When to Use This vs. a Mockup Tool

This API-driven approach is best when you need automation. If you're generating one mockup for a blog post, Figma or Shots.so is faster. But if you're building any of these, an API pipeline wins:

The code above is a starting point. In production, you'd add caching (don't re-capture if the page hasn't changed), queue management for bulk jobs, and CDN storage for the output images.

Wrapping Up

Device frame mockups don't need to be a manual design task. With a screenshot API and an image processing library, you can build a pipeline that captures any URL and wraps it in realistic device frames - iPhone, Android, MacBook, or a clean browser window.

The full pipeline: capture at the right viewport size, composite onto a device frame asset, and export. Whether you do it in Node.js with Sharp or Python with Pillow, it's the same pattern. Capture, composite, ship.