Resize screenshots
output.resize scales the captured image to the size you ask for, in the same request. The page is still laid out at the viewport size, so a desktop page stays a desktop page, only smaller. This guide shows the request, compares the five fit modes with real numbers and explains when a smaller viewport or a different scale is the better tool. More guides are on the guides hub.
The request
Capture a 1280x800 page and return a 640x400 WebP thumbnail:
curl
curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "output": { "format": "webp", "quality": 85, "resize": { "width": 640, "height": 400, "fit": "cover" } } }' \ -o thumb.webp
Node.js
import { writeFile } from 'node:fs/promises'; const body = { "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "output": { "format": "webp", "quality": 85, "resize": { "width": 640, "height": 400, "fit": "cover" } } }; const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!res.ok) { console.error(res.status, await res.text()); process.exit(1); } await writeFile('thumb.webp', Buffer.from(await res.arrayBuffer()));
Python
import json import os import sys import urllib.error import urllib.request body = { 'url': 'https://example.com', 'viewport': { 'width': 1280, 'height': 800, }, 'output': { 'format': 'webp', 'quality': 85, 'resize': { 'width': 640, 'height': 400, 'fit': 'cover', }, }, } req = urllib.request.Request( 'https://api.renderscreenshot.com/v1/screenshot', data=json.dumps(body).encode(), headers={ 'Authorization': f"Bearer {os.environ['RS_API_KEY']}", 'Content-Type': 'application/json', }, method='POST', ) try: with urllib.request.urlopen(req, timeout=90) as res: with open('thumb.webp', 'wb') as f: f.write(res.read()) except urllib.error.HTTPError as err: sys.exit(f'{err.code} {err.read().decode()}')
Ruby
require 'json' require 'net/http' body = { url: 'https://example.com', viewport: { width: 1280, height: 800 }, output: { format: 'webp', quality: 85, resize: { width: 640, height: 400, fit: 'cover' } } } uri = URI('https://api.renderscreenshot.com/v1/screenshot') request = Net::HTTP::Post.new(uri) request['Authorization'] = "Bearer #{ENV.fetch('RS_API_KEY')}" request['Content-Type'] = 'application/json' request.body = body.to_json response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 90) do |http| http.request(request) end abort("#{response.code} #{response.body}") unless response.is_a?(Net::HTTPSuccess) File.binwrite('thumb.webp', response.body)
PHP
<?php $body = [ 'url' => 'https://example.com', 'viewport' => [ 'width' => 1280, 'height' => 800, ], 'output' => [ 'format' => 'webp', 'quality' => 85, 'resize' => [ 'width' => 640, 'height' => 400, 'fit' => 'cover', ], ], ]; $ch = curl_init('https://api.renderscreenshot.com/v1/screenshot'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('RS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 90, ]); $image = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($image === false || $status !== 200) { fwrite(STDERR, $status . ' ' . ($image === false ? curl_error($ch) : $image) . "\n"); exit(1); } file_put_contents('thumb.webp', $image);
Go
package main import ( "bytes" "io" "log" "net/http" "os" ) const body = `{ "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "output": { "format": "webp", "quality": 85, "resize": { "width": 640, "height": 400, "fit": "cover" } } }` func main() { req, err := http.NewRequest(http.MethodPost, "https://api.renderscreenshot.com/v1/screenshot", bytes.NewBufferString(body)) if err != nil { log.Fatal(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RS_API_KEY")) req.Header.Set("Content-Type", "application/json") res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() data, err := io.ReadAll(res.Body) if err != nil { log.Fatal(err) } if res.StatusCode != http.StatusOK { log.Fatalf("%d %s", res.StatusCode, data) } if err := os.WriteFile("thumb.webp", data, 0o644); err != nil { log.Fatal(err) } }
Java
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Files; import java.nio.file.Path; public class Capture { public static void main(String[] args) throws Exception { String body = """ { "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "output": { "format": "webp", "quality": 85, "resize": { "width": 640, "height": 400, "fit": "cover" } } } """; HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.renderscreenshot.com/v1/screenshot")) .header("Authorization", "Bearer " + System.getenv("RS_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<byte[]> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofByteArray()); if (response.statusCode() != 200) { System.err.println(response.statusCode() + " " + new String(response.body())); System.exit(1); } Files.write(Path.of("thumb.webp"), response.body()); } }
The capture and the target have the same 16:10 shape, so every fit mode returns the same image here. The modes differ when the shapes differ.
The five fit modes
fit |
Keeps the aspect ratio | Output size | What happens to the extra space |
|---|---|---|---|
contain (default) |
Yes | Exactly the box | The whole image fits inside; the rest of the box is padding |
cover |
Yes | Exactly the box | The image fills the box; the overflow is cropped |
fill |
No | Exactly the box | The image is stretched to the box |
inside |
Yes | At most the box | The whole image fits; no padding, so one side is shorter |
outside |
Yes | At least the box | The image covers the box; nothing is cropped, so one side is longer |
These are the same five names that image libraries such as sharp use. The numbers below come from sharp 0.35.5, run on 2026-10-15, resizing a 1280x800 image into a 600x600 box:
fit |
Result | Notes |
|---|---|---|
cover |
600x600 | Scaled to 960x600, then 180 px cut from each side |
contain |
600x600 | Scaled to 600x375, then padded top and bottom |
fill |
600x600 | Text and logos look squeezed |
inside |
600x375 | No padding, no crop |
outside |
960x600 | No crop; wider than the box |
For screenshots, cover and inside are the usual choices. cover gives a fixed-size tile for a grid and cuts the edges of the page. inside keeps the whole page and lets the size vary. fill distorts text, so it is rarely what you want for a web page.
Resize, viewport or scale?
The three settings change different things:
| Setting | Changes the page layout | Changes pixel density | Use it for |
|---|---|---|---|
viewport.width / height |
Yes. A 390 px viewport gets the mobile layout | No | Mobile captures, layout at a given breakpoint |
viewport.scale |
No | Yes. 2 gives twice the pixels per CSS pixel | Sharp images for high-density screens |
output.resize |
No | No. It scales the finished image | Thumbnails and fixed-size tiles of the desktop layout |
Two patterns use them together:
- A sharp thumbnail for high-density screens. Capture at
scale: 2, then resize to twice the display size. A tile shown at 320x200 CSS pixels getsresize640x400. - A fixed social card from any page. Use the
og_cardpreset for the 1200x630 viewport. You do not needresize, because the capture already has the right size.
Several sizes from one capture
Each request is one render and uses one credit, so three sizes through output.resize cost three credits. If you need several sizes of the same capture, request the largest one and resize on your side. Tested on 2026-10-15 with Node.js 22.18 and sharp 0.35.5:
npm install [email protected]
// Make several sizes from one capture, so you pay for one render. // Usage: node sizes.mjs capture.png import sharp from 'sharp'; const input = process.argv[2] ?? 'capture.png'; const sizes = [ { name: 'card', width: 1200, height: 630, fit: 'cover' }, { name: 'thumb', width: 400, height: 250, fit: 'cover' }, { name: 'square', width: 600, height: 600, fit: 'inside' }, ]; for (const { name, width, height, fit } of sizes) { const info = await sharp(input) .resize({ width, height, fit, withoutEnlargement: true }) .webp({ quality: 85 }) .toFile(`${name}.webp`); console.log(name, `${info.width}x${info.height}`); }
On a 1280x800 capture this printed card 1200x630, thumb 400x250 and square 600x375.
Parameters that matter
| Parameter | Type | Default | Notes |
|---|---|---|---|
output.resize.width |
integer | none | Target width in pixels |
output.resize.height |
integer | none | Target height in pixels |
output.resize.fit |
string | contain |
contain, cover, fill, inside, outside |
output.format |
string | png |
WebP or JPEG make smaller thumbnails |
output.quality |
integer | 80 | 1 to 100, JPEG and WebP only |
viewport.scale |
number | 1 | 1 to 3 |
Width and height each accept 1 to 7680. Send at least one of them: with only one, the other follows the aspect ratio and fit does not apply. The padding that contain adds is white, or transparent when output.transparent is true. An unknown fit value or an out-of-range size returns invalid_parameter.
output.resize is a POST option. The output options reference and the viewport reference list the rest.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Thumbnail shows the desktop layout, you wanted mobile | Resize does not change the layout | Set a mobile viewport.width instead. |
| Edges of the page are missing | fit: "cover" crops |
Use inside or contain. |
| Text looks squeezed | fit: "fill" ignores the aspect ratio |
Use cover or inside. |
| Blurry thumbnail on a phone | Resized below the display's pixel density | Capture at scale: 2 and resize to twice the display size. |
| Bars around the image | fit: "contain", the default, pads |
Use cover for a full tile or inside for no padding. |
Related
- Presets for capture sizes that need no resize.
- Retina screenshots explain
viewport.scalein detail. - Render Markdown to an image
- Create a free account to try the fit modes on your own pages.