Localized website screenshots
To capture a site the way a visitor in another country sees it, you set up to three browser signals: the language (browser.locale), the clock (browser.timezone) and the position (browser.geolocation). Each one reaches a different part of the site. This guide shows which part, so you can send only what the target site reads. More guides are on the guides hub.
RenderScreenshot does not translate pages. These options ask the site for the version it already has.
Last verified on 2026-10-15 against the renderer source and a local Puppeteer 24.43.1 (Chrome 148) replay of the same browser calls.
What each option changes
We replayed the renderer's calls against a local test page and read the results from the server and from page scripts:
| Signal the site might read | locale: "fr-FR" |
timezone: "Asia/Tokyo" |
geolocation |
|---|---|---|---|
Accept-Language request header |
fr-FR (exactly this value) |
unchanged | unchanged |
navigator.language / navigator.languages |
fr-FR / ["fr-FR"] |
unchanged | unchanged |
Intl.DateTimeFormat().resolvedOptions().locale |
unchanged (en-US in our replay) |
unchanged | unchanged |
(1234567.891).toLocaleString() |
unchanged (1,234,567.891) |
unchanged | unchanged |
Time zone of Date and Intl |
unchanged | Asia/Tokyo |
unchanged |
navigator.geolocation.getCurrentPosition() |
unchanged | unchanged | Your coordinates, no permission prompt |
| Client IP address | unchanged | unchanged | unchanged |
Two rows need attention:
- Number and date formats.
localechanges the header andnavigator.language, but not the default locale ofIntl. A page that callstoLocaleString()with no argument still formats numbers the US way. A page that passesnavigator.languagetoIntlgets French formats. Server-rendered pages that format with theAccept-Languageheader also get French formats. - IP address. The browser runs on Cloudflare's network, and Cloudflare documents that Browser Run does not rotate IP addresses (Browser Run FAQ, checked 2026-10-15). A site that redirects by IP sends the version for wherever the request leaves Cloudflare.
Choose the signal the site uses
| The site picks its version by | Send |
|---|---|
URL path or subdomain (/de/, de.example.com) |
That URL. This is the most reliable method. |
Accept-Language header |
browser.locale |
navigator.language in JavaScript |
browser.locale |
| Visitor's clock (opening hours, "today" labels, event times) | browser.timezone |
| Browser location API (store finders, maps) | browser.geolocation |
| IP address | The site's country URL or country selector. No option changes the IP. |
The minimal request
GET supports locale and timezone:
curl -sS --fail -G \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "locale=de-DE" \ --data-urlencode "timezone=Europe/Berlin" \ https://api.renderscreenshot.com/v1/screenshot \ -o de-DE.png
geolocation is POST only. A full German request with a Berlin position:
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", "browser": { "locale": "de-DE", "timezone": "Europe/Berlin", "geolocation": { "latitude": 52.52, "longitude": 13.405, "accuracy": 100 } } }' \ -o de-DE.png
Node.js
import { writeFile } from 'node:fs/promises'; const body = { "url": "https://example.com", "browser": { "locale": "de-DE", "timezone": "Europe/Berlin", "geolocation": { "latitude": 52.52, "longitude": 13.405, "accuracy": 100 } } }; 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('de-DE.png', Buffer.from(await res.arrayBuffer()));
Python
import json import os import sys import urllib.error import urllib.request body = { 'url': 'https://example.com', 'browser': { 'locale': 'de-DE', 'timezone': 'Europe/Berlin', 'geolocation': { 'latitude': 52.52, 'longitude': 13.405, 'accuracy': 100, }, }, } 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('de-DE.png', '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', browser: { locale: 'de-DE', timezone: 'Europe/Berlin', geolocation: { latitude: 52.52, longitude: 13.405, accuracy: 100 } } } 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('de-DE.png', response.body)
PHP
<?php $body = [ 'url' => 'https://example.com', 'browser' => [ 'locale' => 'de-DE', 'timezone' => 'Europe/Berlin', 'geolocation' => [ 'latitude' => 52.52, 'longitude' => 13.405, 'accuracy' => 100, ], ], ]; $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('de-DE.png', $image);
Go
package main import ( "bytes" "io" "log" "net/http" "os" ) const body = `{ "url": "https://example.com", "browser": { "locale": "de-DE", "timezone": "Europe/Berlin", "geolocation": { "latitude": 52.52, "longitude": 13.405, "accuracy": 100 } } }` 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("de-DE.png", 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", "browser": { "locale": "de-DE", "timezone": "Europe/Berlin", "geolocation": { "latitude": 52.52, "longitude": 13.405, "accuracy": 100 } } } """; 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("de-DE.png"), response.body()); } }
The renderer grants the location permission for the target page before it loads, so the site gets the position without a prompt. In our replay, getCurrentPosition() returned latitude 52.52, longitude 13.405 and accuracy 100.
One page in several locales
Each locale is a separate render and a separate cache entry. A shell loop over the GET form:
set -euo pipefail for pair in en-US:America/New_York de-DE:Europe/Berlin ja-JP:Asia/Tokyo; do locale="${pair%%:*}" tz="${pair#*:}" curl -sS --fail -G \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "locale=$locale" \ --data-urlencode "timezone=$tz" \ https://api.renderscreenshot.com/v1/screenshot \ -o "home-$locale.png" done
Non-Latin scripts render without extra fonts
Browser Run ships fonts for Chinese, Japanese and Korean (Noto CJK, IPAfont Gothic, WenQuanYi Zen Hei), Arabic (KACST), Thai (TLWG) and several Indic scripts, according to Cloudflare's supported fonts list (checked 2026-10-15). A Japanese page that declares a web font still uses that font. Right-to-left layout comes from the page's own dir attribute. There is nothing to set in the request.
Parameters that matter
| Parameter | Format | GET name | Validation |
|---|---|---|---|
browser.locale |
BCP 47, for example de-DE |
locale |
Rejected with invalid_locale if not BCP 47 |
browser.timezone |
IANA, for example Europe/Berlin |
timezone |
Rejected with invalid_timezone if unknown |
browser.geolocation.latitude |
number | POST only | -90 to 90 |
browser.geolocation.longitude |
number | POST only | -180 to 180 |
browser.geolocation.accuracy |
meters | POST only | Default 100 |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Page stays in English with locale: "de-DE" |
The site picks the language by URL or IP | Request the German URL. |
| German text, US number formats | The page formats with the default Intl locale |
Expected. Use the site's localized URL if it formats on the server. |
| Wrong country version, right language | Geo-IP redirect | Use the country URL. The IP cannot be changed with these options. |
| Times shown in UTC | No timezone sent |
Add browser.timezone. |
| Store finder shows "location unavailable" | geolocation was sent with GET |
Send it in a POST body. |
| Empty boxes instead of characters | The page asks for a font that failed to load, with no fallback | Check the page's font URLs. The renderer's built-in CJK fonts cover the fallback. |
Related
- Custom User-Agent changes another header that sites read.
- Emoji rendering covers the emoji font in the renderer.
- E-commerce screenshots handle region pop-ups on shop pages.
- Create a free account to capture your own localized pages.