Screenshot e-commerce product pages

Shop pages are hard to capture cleanly. Before a visitor sees the product, the page shows a cookie wall, a "you are on the US store" dialog, a newsletter pop-up, a chat bubble and a promotion bar. This guide removes each of them in one request and then captures only the price block, which is the part most price-tracking and catalog jobs need. More guides are on the guides hub.

Last verified on 2026-10-15 against the renderer source. The request was checked in seven languages against a local test server.

Match each obstacle to an option

Obstacle on the page Option What the renderer does
Cookie consent banner block.cookie_banners: true Looks for a known "accept" button, clicks the first one it finds, then waits 300 ms
Chat bubble (Intercom, Drift, Zendesk and similar) block.chat_widgets: true Blocks the widget's network requests
Ads and trackers block.ads, block.trackers Blocks known ad and analytics hosts
"Stay on this store?" region dialog page.click Clicks your selector, then waits 300 ms
Newsletter pop-up, promotion bar page.remove Removes the elements from the page
Lazy product images wait.until: "networkidle" plus wait.delay Waits for the network to go quiet, then a fixed time

The cookie option clicks "accept". On some shops, accepting consent loads more scripts, such as marketing tags. If you do not want that, leave block.cookie_banners off and put the banner's selector in page.remove.

The order of the steps

The renderer applies these options in a fixed order. The order explains most surprises:

  1. Load the page and wait for wait.until.
  2. Wait wait.delay milliseconds.
  3. Hide ad containers, then click the cookie button.
  4. Click page.click.
  5. Run page.scripts, inject page.styles.
  6. Apply page.hide, then page.remove.
  7. Capture.

Removal comes last. A pop-up that a shop opens on a timer is already on the page after the delay, so page.remove catches it. A pop-up that opens after the capture cannot appear in the image.

The request: a product page, price block only

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://shop.example.com/products/blue-mug",
      "viewport": {
        "width": 1280,
        "height": 800,
        "scale": 2
      },
      "wait": {
        "until": "networkidle",
        "delay": 1000
      },
      "block": {
        "cookie_banners": true,
        "chat_widgets": true
      },
      "page": {
        "click": "#region-modal .stay-here",
        "remove": [
          ".promo-bar",
          "#newsletter-popup"
        ]
      },
      "capture": {
        "mode": "element",
        "selector": ".product-price"
      },
      "output": {
        "format": "webp",
        "quality": 85
      }
    }' \
  -o price.webp

Node.js

import { writeFile } from 'node:fs/promises';

const body = {
  "url": "https://shop.example.com/products/blue-mug",
  "viewport": {
    "width": 1280,
    "height": 800,
    "scale": 2
  },
  "wait": {
    "until": "networkidle",
    "delay": 1000
  },
  "block": {
    "cookie_banners": true,
    "chat_widgets": true
  },
  "page": {
    "click": "#region-modal .stay-here",
    "remove": [
      ".promo-bar",
      "#newsletter-popup"
    ]
  },
  "capture": {
    "mode": "element",
    "selector": ".product-price"
  },
  "output": {
    "format": "webp",
    "quality": 85
  }
};

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('price.webp', Buffer.from(await res.arrayBuffer()));

Python

import json
import os
import sys
import urllib.error
import urllib.request

body = {
    'url': 'https://shop.example.com/products/blue-mug',
    'viewport': {
        'width': 1280,
        'height': 800,
        'scale': 2,
    },
    'wait': {
        'until': 'networkidle',
        'delay': 1000,
    },
    'block': {
        'cookie_banners': True,
        'chat_widgets': True,
    },
    'page': {
        'click': '#region-modal .stay-here',
        'remove': ['.promo-bar', '#newsletter-popup'],
    },
    'capture': {
        'mode': 'element',
        'selector': '.product-price',
    },
    'output': {
        'format': 'webp',
        'quality': 85,
    },
}

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('price.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://shop.example.com/products/blue-mug',
  viewport: {
    width: 1280,
    height: 800,
    scale: 2
  },
  wait: {
    until: 'networkidle',
    delay: 1000
  },
  block: {
    cookie_banners: true,
    chat_widgets: true
  },
  page: {
    click: '#region-modal .stay-here',
    remove: ['.promo-bar', '#newsletter-popup']
  },
  capture: {
    mode: 'element',
    selector: '.product-price'
  },
  output: {
    format: 'webp',
    quality: 85
  }
}

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('price.webp', response.body)

PHP

<?php

$body = [
    'url' => 'https://shop.example.com/products/blue-mug',
    'viewport' => [
        'width' => 1280,
        'height' => 800,
        'scale' => 2,
    ],
    'wait' => [
        'until' => 'networkidle',
        'delay' => 1000,
    ],
    'block' => [
        'cookie_banners' => true,
        'chat_widgets' => true,
    ],
    'page' => [
        'click' => '#region-modal .stay-here',
        'remove' => ['.promo-bar', '#newsletter-popup'],
    ],
    'capture' => [
        'mode' => 'element',
        'selector' => '.product-price',
    ],
    'output' => [
        'format' => 'webp',
        'quality' => 85,
    ],
];

$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('price.webp', $image);

Go

package main

import (
    "bytes"
    "io"
    "log"
    "net/http"
    "os"
)

const body = `{
  "url": "https://shop.example.com/products/blue-mug",
  "viewport": {
    "width": 1280,
    "height": 800,
    "scale": 2
  },
  "wait": {
    "until": "networkidle",
    "delay": 1000
  },
  "block": {
    "cookie_banners": true,
    "chat_widgets": true
  },
  "page": {
    "click": "#region-modal .stay-here",
    "remove": [
      ".promo-bar",
      "#newsletter-popup"
    ]
  },
  "capture": {
    "mode": "element",
    "selector": ".product-price"
  },
  "output": {
    "format": "webp",
    "quality": 85
  }
}`

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("price.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://shop.example.com/products/blue-mug",
              "viewport": {
                "width": 1280,
                "height": 800,
                "scale": 2
              },
              "wait": {
                "until": "networkidle",
                "delay": 1000
              },
              "block": {
                "cookie_banners": true,
                "chat_widgets": true
              },
              "page": {
                "click": "#region-modal .stay-here",
                "remove": [
                  ".promo-bar",
                  "#newsletter-popup"
                ]
              },
              "capture": {
                "mode": "element",
                "selector": ".product-price"
              },
              "output": {
                "format": "webp",
                "quality": 85
              }
            }
            """;

        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("price.webp"), response.body());
    }
}

What each part does:

  • viewport.scale: 2 gives a sharp image of small price text. The 1280 px wide layout is captured at 2560 px.
  • wait.until: "networkidle" waits until no requests run for 500 ms. The extra second covers price widgets that start late.
  • page.click closes the region dialog, if it is there. If it is not, the request continues. Set page.click_required: true to fail instead.
  • capture.mode: "element" with capture.selector captures only that element's box.

Replace the URL and the selectors with the ones from your shop. Open the product page, right-click the price and choose Inspect to find a selector.

For a GET request of the whole page, the blocking options have short names:

curl -sS --fail -G \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "block_cookies=true" \
  --data-urlencode "block_ads=true" \
  --data-urlencode "full_page=true" \
  https://api.renderscreenshot.com/v1/screenshot \
  -o product-full.png

Element capture, page.click and page.remove need the POST body.

Choose a good price selector

  • Pick the wrapper that holds the current price, the old crossed-out price and the discount badge. A selector for the number alone cuts the context off.
  • Prefer stable attributes such as [data-testid="price"] or [itemprop="price"] over generated class names like .css-1x9ab.
  • If more than one element matches, the first one is captured. On pages with "related products", make the selector specific to the main product.
  • The element box has no margin. To add white space around the price, inject a style: "page": {"styles": [".product-price { padding: 16px; background: #fff; }"]}.

Prices, currencies and regions

A shop decides the currency from the URL, a cookie or the visitor's IP address. The browser's IP belongs to Cloudflare's network, so IP-based stores may show a currency you did not expect. The reliable fix is the store's own market URL, for example a country path or a currency parameter. The localized screenshots guide explains what browser.locale and browser.timezone can and cannot change.

Parameters that matter

Parameter Default Limit or note
wait.until load load, domcontentloaded, networkidle
wait.delay 0 At most 10,000 ms
wait.timeout 30 s At most 60 s
capture.mode viewport element needs capture.selector
viewport.scale 1 1 to 3
output.quality 80 1 to 100, JPEG and WebP

The full lists are in blocking, page modifications and capture.

Troubleshooting

Symptom Cause Fix
Error "Selector not found" The price element is not on the page, or loads later Check the selector in DevTools. Raise wait.delay.
Cookie banner still visible The banner's button is not in the known list Put the banner selector in page.remove, or its button in page.click.
Gray boxes where product images should be Lazy images below the fold Capture the element or viewport you need, or raise wait.delay.
Wrong currency Region chosen by IP Use the store's country or currency URL.
Price block captured from a "related products" card The selector matched more than one element Make the selector specific to the main product.
A pop-up covers the product It opened during the wait Add its selector to page.remove. Removal runs just before the capture.

Related in this topic

Was this page helpful?