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:

  1. Add scrollY to top. getBoundingClientRect() is relative to the visible window. If you scrolled before you measured, the raw value is off by the scroll distance.
  2. 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.
  3. Write down the width. The same paragraph starts at x: 432 in a 1280 px window and at x: 192 in 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.width 1 to 7,680 px, viewport.height 1 to 4,320 px, viewport.scale 1 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 in this topic

Was this page helpful?