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:
- Load the page and wait for
wait.until. - Wait
wait.delaymilliseconds. - Hide ad containers, then click the cookie button.
- Click
page.click. - Run
page.scripts, injectpage.styles. - Apply
page.hide, thenpage.remove. - 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: 2gives 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.clickcloses the region dialog, if it is there. If it is not, the request continues. Setpage.click_required: trueto fail instead.capture.mode: "element"withcapture.selectorcaptures 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
- Block ads and cookie banners goes deeper on consent banners.
- Element screenshots cover selector strategies for any page.
- Wait strategies for prices that load late.
- Create a free account and run the request on one of your product pages.