Clip screenshots
capture.clip crops the screenshot to a rectangle you give in pixels: x, y, width and height. Use it when you know where the area is, for example the top 600 pixels of a landing page or a fixed banner slot. When you know what the area is (a pricing table, a chart), use element screenshots instead: a selector follows the content when the layout moves, and a rectangle does not. Both are covered in the capture guides.
The minimal request
clip is POST-only. It is not in the GET parameter list, so it cannot go in a query string or a signed URL.
curl -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 }, "capture": { "clip": { "x": 416, "y": 200, "width": 448, "height": 112 } } }' \ -o clip.png
On https://example.com at a 1280 px viewport, that rectangle is the paragraph of text plus 16 px of margin on each side. The section below shows how those numbers were measured.
If the area is an element and you need a URL (an <img> tag, a signed URL), use element mode through GET:
curl -G "https://api.renderscreenshot.com/v1/screenshot" \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "selector=body > p" \ -o paragraph.png
Parameters that matter
| Parameter | Default | Notes |
|---|---|---|
capture.clip.x |
none | Left edge of the rectangle, in pixels from the left of the page |
capture.clip.y |
none | Top edge, in pixels from the top of the page |
capture.clip.width |
none | Width of the rectangle |
capture.clip.height |
none | Height of the rectangle |
viewport.width |
1280 |
Decides the layout, so it decides where things are. Always send it with a clip. |
viewport.scale |
1 |
Device scale factor, 1 to 3. Check the output size (below) when you change it. |
output.resize |
none | Resize after the crop, for example to make a fixed-size thumbnail of the area |
Send all four clip fields as integers. The response headers X-Screenshot-Width and X-Screenshot-Height give the size of the image you received. Check them once when you set up a new clip, and again whenever you change viewport.scale.
Measure the rectangle
Measure in a browser at the same width you will send as viewport.width. Open DevTools on the target page, set the window or Device Emulation to that width, and run:
// Paste into the DevTools console. Prints a clip with 16 px of padding, rounded outward. const el = document.querySelector('body > p'); // the area you want const r = el.getBoundingClientRect(); const pad = 16; const clip = { x: Math.floor(r.left - pad), y: Math.floor(r.top + window.scrollY - pad), // page coordinates, not viewport width: Math.ceil(r.width + 2 * pad), height: Math.ceil(r.height + 2 * pad), }; console.log(window.innerWidth, JSON.stringify(clip));
Three habits prevent most wrong crops:
- Add
scrollYtotop.getBoundingClientRect()is relative to the visible window. If you scrolled before you measured, the raw value is off by the scroll distance. - Round outward. Layout boxes have fractional sizes (the example.com paragraph is 76.78 px tall). Floor the left and top, ceil the width and height, or you lose a pixel row of text.
- Write down the width. The same paragraph starts at
x: 432in a 1280 px window and atx: 192in an 800 px window, because the page centers it. I measured both on 2026-10-15 with Chromium 156. A clip measured at one width is wrong at the other.
Clip or element mode?
capture.clip |
capture.mode: "element" |
|
|---|---|---|
| You provide | Four numbers | A CSS selector |
| Content moves (new banner, longer headline) | The crop stays put and shows something else | The crop follows the element |
| Area is not one element (half of a section, two cards side by side) | Works | Needs a wrapper you can select |
| Works in GET and signed URLs | No | Yes (selector) |
| Missing target | Nothing to check: the rectangle is cropped whatever is in it | Set selector_required: true to fail with a target_error |
| Best for | Fixed slots, above-the-fold crops, visual checks of a known region | Components, cards, charts, embeds |
A pattern that combines both: when the region is "the element plus a margin", measure the element once, then decide. If the page is yours and stable, clip is fine. If the page belongs to someone else, element mode is the one that survives the next redesign.
Above the fold: clip vs viewport height
"The top 600 px of the page" does not need a clip. Set the viewport to that height and capture the viewport, which is the default mode:
{ "url": "https://example.com", "viewport": { "width": 1280, "height": 600 } }
The difference: a page that sizes its hero with vh units lays out differently in a 600 px viewport than in an 800 px one. If you want the hero as a visitor on an 800 px screen sees it, keep the viewport at 800 and clip to 600.
Clip, then resize
To make a fixed-size thumbnail of an area, combine the crop with image resizing. The crop runs on the page; the resize runs on the result:
{ "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "capture": { "clip": { "x": 0, "y": 0, "width": 1280, "height": 640 } }, "output": { "format": "webp", "resize": { "width": 600, "height": 300, "fit": "cover" } } }
SDK and language examples
The six official SDKs have builder methods for element mode (element(selector)) but no method for clip, and their option objects send only the options that have builder methods. Send clip requests with the HTTP client of your language. Each listing reads the key from RS_API_KEY, saves clip.png, and exits with an error on a 4xx or 5xx response. For signing, retries and the full response handling, see using the API without an SDK.
Node.js
// clip.mjs - Node 20+, no dependencies import { writeFile } from 'node:fs/promises'; 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({ url: 'https://example.com', viewport: { width: 1280, height: 800 }, capture: { clip: { x: 416, y: 200, width: 448, height: 112 } }, }), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); await writeFile('clip.png', Buffer.from(await res.arrayBuffer())); console.log(res.headers.get('x-screenshot-width'), 'x', res.headers.get('x-screenshot-height'));
Python
# clip.py - Python 3.9+, standard library only import json, os, urllib.request req = urllib.request.Request( "https://api.renderscreenshot.com/v1/screenshot", data=json.dumps({ "url": "https://example.com", "viewport": {"width": 1280, "height": 800}, "capture": {"clip": {"x": 416, "y": 200, "width": 448, "height": 112}}, }).encode(), headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}", "Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=60) as res: # raises HTTPError on 4xx/5xx open("clip.png", "wb").write(res.read()) print(res.headers["X-Screenshot-Width"], "x", res.headers["X-Screenshot-Height"])
Ruby
# clip.rb - Ruby 3.0+, standard library only require 'json' require 'net/http' uri = URI('https://api.renderscreenshot.com/v1/screenshot') body = { url: 'https://example.com', viewport: { width: 1280, height: 800 }, capture: { clip: { x: 416, y: 200, width: 448, height: 112 } } } res = Net::HTTP.post(uri, body.to_json, 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}", 'Content-Type' => 'application/json') abort "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) File.binwrite('clip.png', res.body) puts "#{res['X-Screenshot-Width']} x #{res['X-Screenshot-Height']}"
PHP
<?php // clip.php - PHP 8.0+ with the curl extension $ch = curl_init('https://api.renderscreenshot.com/v1/screenshot'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('RS_API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'url' => 'https://example.com', 'viewport' => ['width' => 1280, 'height' => 800], 'capture' => ['clip' => ['x' => 416, 'y' => 200, 'width' => 448, 'height' => 112]], ]), ]); $body = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if ($body === false || $status >= 400) { fwrite(STDERR, "{$status}: " . ($body ?: curl_error($ch)) . PHP_EOL); exit(1); } file_put_contents('clip.png', $body);
Go
// clip.go - Go 1.21+, standard library only package main import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http" "os" "time" ) func main() { payload, _ := json.Marshal(map[string]any{ "url": "https://example.com", "viewport": map[string]int{"width": 1280, "height": 800}, "capture": map[string]any{"clip": map[string]int{"x": 416, "y": 200, "width": 448, "height": 112}}, }) req, err := http.NewRequest("POST", "https://api.renderscreenshot.com/v1/screenshot", bytes.NewReader(payload)) 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.Client{Timeout: 60 * time.Second}).Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { log.Fatal(err) } if res.StatusCode >= 400 { log.Fatalf("%d: %s", res.StatusCode, body) } if err := os.WriteFile("clip.png", body, 0o644); err != nil { log.Fatal(err) } fmt.Println(res.Header.Get("X-Screenshot-Width"), "x", res.Header.Get("X-Screenshot-Height")) }
Java
// Clip.java - Java 15+ (text block), java.net.http only 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; import java.time.Duration; public class Clip { public static void main(String[] args) throws Exception { String json = """ {"url": "https://example.com", "viewport": {"width": 1280, "height": 800}, "capture": {"clip": {"x": 416, "y": 200, "width": 448, "height": 112}}}"""; HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.renderscreenshot.com/v1/screenshot")) .timeout(Duration.ofSeconds(60)) .header("Authorization", "Bearer " + System.getenv("RS_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<byte[]> res = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofByteArray()); if (res.statusCode() >= 400) { System.err.println(res.statusCode() + ": " + new String(res.body())); System.exit(1); } Files.write(Path.of("clip.png"), res.body()); } }
The Node, Python, Ruby, PHP and Go listings were run on 2026-10-15 (Node 22.18, Python 3.14, Ruby 4.0, PHP 8.5, Go 1.27) against a local stub of the endpoint, to check the request body, the headers and the error exit. The Java listing was not run.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The crop shows a different part of the page than in your browser | Measured at another width | Measure at the exact viewport.width you send, and send it explicitly |
| The crop is shifted down by a fixed amount | top measured after scrolling, without adding scrollY |
Use page coordinates: rect.top + window.scrollY |
| Last line of text is cut by a pixel | Fractional box sizes rounded down | Floor x/y, ceil width/height, or add padding |
| The crop was right last week, now it is wrong | The page layout changed | Switch to element mode with a stable selector |
| A cookie banner or sticky header covers the area | Fixed elements are painted over the page | block.cookie_banners: true, or page.hide with the header selector; see customizing the page before capture |
| The clip is ignored in a URL | clip sent as a query parameter |
Use POST; GET supports selector but not clip |
| The clip has no effect in a batch | capture.clip is not available in batch requests |
Send individual POST requests |
The output is larger than width x height |
viewport.scale above 1 |
Check X-Screenshot-Width; resize with output.resize if you need exact pixels |
| Area below the first screen is blank | The content is lazy-loaded and never scrolled into view | Use element mode, or a full-page capture with scroll simulation and crop afterwards |
Limits
- One rectangle per request. Each request is one render and uses one credit.
viewport.width1 to 7,680 px,viewport.height1 to 4,320 px,viewport.scale1 to 3.- POST only; not in GET parameters, signed URLs or batch requests.
- Render time is bounded by
wait.timeout: 30 seconds by default, 60 seconds at most.
Related parameters
- Capture parameters:
mode,selector,selector_required,clip,beyond_viewport,scroll. - Element screenshots: the selector-based alternative.
- Splitting long screenshots: the opposite job, cutting one tall image into many.