· Updated · Engineering · By Simon Chiu

Designing Screenshot API Presets: Sizes and Defaults

Why our presets (og_card, twitter_card, link_preview) use the sizes and defaults they do, how they merge with your parameters, and measured file sizes.

Line illustration of the RenderScreenshot mascot choosing between frames of different sizes pinned on a board

A preset is one parameter that replaces five. preset=og_card sets a 1200x630 viewport, PNG output and a 24-hour cache in a single word, and you can still override any of them. This post explains how we chose the presets in RenderScreenshot, how a preset merges with the parameters you send, one merge rule that surprises people, and what the default format costs in bytes. It is part of how we built RenderScreenshot.

Last checked: 2026-10-15. Preset values come from our presets reference and the Preset model in the API. File sizes were measured with local Chromium (Chrome headless shell 148.0.7778.97, Puppeteer 24.43.1, Node 22.18.0) on an Apple M1 Pro, not with the API. Facebook's image guidance was read on its developer site on the same date.

What a preset is, and what it is not

The API has a dozen parameter groups: viewport, capture, output, wait, cache and more. Most requests for a given job use the same handful of values. A social card is always the same size. A full-page capture always wants full-page mode. A preset is a named bundle of those values.

We set three rules for presets early:

  1. A preset only sets defaults. Anything you send explicitly wins. A preset never blocks a parameter.
  2. A preset names an output, not a customer. og_card is "an image for og:image", not "the settings for blogs". That keeps the list short and stable.
  3. The values are public and machine-readable. GET /v1/presets returns each preset's config, and the presets reference lists the same values. Nothing about a preset is hidden.

The full list today:

Preset Canvas Ratio Format Other defaults Cache TTL
og_card 1200x630 1.91:1 PNG 24 h
twitter_card 1200x600 2:1 PNG 24 h
twitter_card_large 1200x628 1.91:1 PNG 24 h
link_preview 800x600 4:3 PNG 24 h
full_page 1280x800 PNG capture.mode: full_page with scroll settings 1 h
mobile 390x844 PNG scale 3, mobile and touch emulation 24 h
mobile_landscape 844x390 PNG scale 3, mobile, touch, landscape 24 h
desktop_hd 1920x1080 16:9 PNG 24 h
desktop_4k 3840x2160 16:9 PNG 24 h
pdf_document Letter paper PDF background graphics, 1 cm margins 24 h

Why these sizes

og_card: 1200x630

This is the one preset we did not have to think hard about. Facebook's image guidance for shared links asks for images of at least 1200x630 for high-resolution displays and an aspect ratio as close to 1.91:1 as possible, with a minimum of 200x200 and an 8 MB file limit (checked 2026-10-15). LinkedIn, Slack, Discord and most link unfurlers accept the same Open Graph image, so one 1200x630 card covers most platforms. That is why og_card is the default recommendation in our social card guide.

We render at exactly 1200x630 rather than at a larger size. A crawler downloads the image on first share, often on a short timeout, and a bigger canvas only adds bytes that the platform scales away.

twittercard and twittercard_large

These two are close relatives of og_card. twitter_card is a 2:1 canvas (1200x600). twitter_card_large is 1200x628, a 1.91:1 canvas two pixels shorter than og_card. X reads twitter:image and falls back to og:image, and it crops to its own layout, so the question is which ratio you design for. If you design one card for every platform, use og_card and keep important content away from the edges. If you design a card only for X, check X's current card documentation for the ratio of the card type you use. We could not load X's developer documentation on 2026-10-15, so we do not quote its limits here.

link_preview: 800x600

Chat apps and dashboards often show a small thumbnail of a page, not a wide banner. A 4:3 window at 800 px wide shows the top of a real page at a readable layout width, and the image is small enough to store many of them. Use it for "preview of this link" thumbnails, not for og:image.

full_page: a shorter cache

full_page is the only preset with a 1-hour cache instead of 24 hours. A full-page capture is the whole document, so any change anywhere on the page makes it stale, and full-page captures are often used for monitoring and archiving, where a fresh copy matters more than a cache hit. A social card shows the top of a page or a template, which changes less often.

Device presets

mobile and mobile_landscape match the iPhone 14 Pro viewport (390x844) at a scale factor of 3, with mobile and touch emulation. desktop_hd and desktop_4k are the two common desktop resolutions. For other phones and tablets, use viewport.device with an id from GET /v1/devices instead of a preset.

How a preset merges with your parameters

The merge is the part of preset design that matters most in practice, so here is the actual code from the API:

# app/controllers/api/v1/screenshots_controller.rb (excerpt)
def deep_merge_params(preset_config, explicit_params)
  result = explicit_params.dup

  preset_config.each do |key, preset_value|
    if explicit_params[key].is_a?(Hash) && preset_value.is_a?(Hash)
      # For nested hashes, merge recursively with explicit values taking precedence
      result[key] = preset_value.merge(explicit_params[key])
    elsif explicit_params[key].nil?
      # Use preset value if no explicit value provided
      result[key] = preset_value
    end
    # Otherwise keep explicit value (already in result from dup)
  end

  result
end

Three consequences:

  • Explicit values win at the group level. preset: og_card with output: { format: "webp" } keeps the 1200x630 viewport and the 24-hour cache, and changes only the format.
  • Groups you do not send come from the preset. If you send no cache group, you get the preset's TTL.
  • The merge is one level deep. Inside a group, your keys replace the preset's keys with the same name. A nested object inside a group is replaced as a whole, not merged. For example, with preset: full_page, sending capture: { scroll: { delay: 800 } } replaces the preset's whole scroll object, so the preset's simulate: true is gone. If you override a nested object, send all of it.

The batch endpoint applies presets the same way, per item, so a batch can mix og_card and full_page items. An unknown name fails fast with invalid_preset before any render, so a typo never costs a credit.

We recommend not combining a preset with viewport.device in one request. Both set viewport values, and a request is easier to read when one of them owns the viewport.

Overriding a preset

The same override in a GET request (flat parameters) and a POST request (nested JSON):

curl -sS --fail -o card.webp \
  -H "Authorization: Bearer $RS_API_KEY" \
  "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&preset=og_card&format=webp&quality=85"
curl -sS --fail -o card.webp -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "preset": "og_card", "output": {"format": "webp", "quality": 85}}'

With the Node.js SDK:

import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const client = new Client(process.env.RS_API_KEY);

const image = await client.take(
  TakeOptions.url('https://example.com').preset('og_card').format('webp').quality(85),
);
await writeFile('card.webp', image);

The SDK methods (preset, format, quality) are listed in the Node.js SDK reference. The other SDKs use the same TakeOptions builder pattern; see the SDK overview.

What the PNG default costs

We kept PNG as the default for every image preset. PNG is lossless, supports transparency, and is accepted by every crawler and chat app we know of. For an image in a meta tag that a third party fetches, compatibility is the right default.

But PNG is expensive for the images people usually put on cards: gradients, photos and large color fields. We rendered one card design (a gradient background with a title and a URL line, laid out in viewport units so it fills each canvas) at each social preset size, in three formats:

Preset Canvas PNG JPEG (q80) WebP (q80)
og_card 1200x630 335 KB 31 KB 18 KB
twitter_card 1200x600 305 KB 31 KB 17 KB
twitter_card_large 1200x628 335 KB 31 KB 18 KB
link_preview 800x600 200 KB 20 KB 12 KB

The PNG is about 18 times the size of the WebP for this design. A text-heavy page shows a smaller gap: in the same session, a plain documentation page at 1200x630 was 92 KB as PNG and 44 KB as WebP. So:

  • Keep PNG when the image has transparency, sharp UI text you will zoom into, or a consumer whose format support you do not know.
  • Use WebP for cards with gradients or photos when the consumers accept it. All current browsers do; for social platforms, test the ones you care about with their own preview tools before you switch.
  • Use JPEG as the safe smaller option for photo-heavy cards.

This is the measurement script. It renders locally with Puppeteer, not through the API, so it isolates the format cost from network time:

// preset_sizes.mjs: render one responsive card at each social preset size; report bytes.
// Node 22, [email protected]. Local Chromium, not the RenderScreenshot API.
import puppeteer from 'puppeteer';

const PRESETS = {
  og_card: [1200, 630], twitter_card: [1200, 600], twitter_card_large: [1200, 628], link_preview: [800, 600],
};
const html = `<!doctype html><meta charset="utf-8"><style>
html,body{margin:0;height:100%}
body{display:flex;flex-direction:column;justify-content:center;padding:0 6vw;box-sizing:border-box;
background:linear-gradient(135deg,#0f172a,#1e3a8a 60%,#0ea5e9);color:#f8fafc;font-family:Helvetica,Arial,sans-serif}
h1{font-size:5.6vw;line-height:1.1;margin:0 0 3vh}p{font-size:2.5vw;margin:0;opacity:.85}
</style><h1>Designing screenshot presets</h1><p>renderscreenshot.com/blog</p>`;

const browser = await puppeteer.launch({ headless: 'shell' });
try {
  const rows = [];
  for (const [name, [width, height]] of Object.entries(PRESETS)) {
    const page = await browser.newPage();
    await page.setViewport({ width, height });
    await page.setContent(html);
    const row = { preset: name, size: `${width}x${height}`, ratio: (width / height).toFixed(3) };
    for (const [type, quality] of [['png'], ['jpeg', 80], ['webp', 80]]) {
      const buf = await page.screenshot({ type, ...(quality ? { quality } : {}) });
      row[`${type}_kb`] = Math.round(buf.length / 1024);
    }
    rows.push(row);
    await page.close();
  }
  console.table(rows);
} catch (err) {
  console.error(err);
  process.exitCode = 1;
} finally {
  await browser.close();
}

The bytes also matter for storage and delivery cost, which we model in R2 vs S3 for screenshot storage.

Design your template for the preset

A preset fixes the canvas. Your HTML template has to fill it:

  • Set the body to the canvas size (width:1200px;height:630px) or use viewport units, as the script above does, so one template can serve several presets.
  • Set a background on body. Without one, the page canvas follows the browser's color scheme.
  • Embed or wait for your fonts. A card rendered before its web font arrives is a card in a fallback font. We measured how large that difference is in consistent font rendering.
  • Escape user input that goes into the template, such as a post title. A title like <script> must become text, not markup.

How we decide on a new preset

We get requests for more presets (Pinterest pins, Instagram squares, YouTube thumbnails). The test we apply:

  1. Is it a distinct output that many requests share, with a size documented by the platform that consumes it?
  2. Does it need more than a viewport size? If it is only a size, viewport.width and viewport.height already do the job, and a new preset adds a name to learn and nothing else.
  3. Can we keep it stable? A preset's values are part of the API contract. Changing og_card from PNG to WebP would break consumers that assume PNG, so a change like that would be a new preset, not an edit.

If a preset you want passes those tests, tell us.

Try it

Create a free account and request https://example.com with preset=og_card, then the same with format=webp, and compare the Content-Length of the two responses. To put the card in a meta tag without exposing your key, use a signed URL.

Related in this topic