Custom User-Agent for screenshots
browser.user_agent replaces the browser's User-Agent string for one capture. Use it when a server picks the HTML it sends by looking at the User-Agent, or when you want your captures to be easy to find in your own access logs. This guide shows the request and what the option does and does not change. More guides are on the guides hub.
Last verified on 2026-10-15 against the renderer source and a local Puppeteer 24.43.1 (Chrome 148) replay of the same browser call.
What changes and what does not
The renderer sets the override before navigation. We replayed that call with an iPhone Safari string against a local test server and recorded what the server and the page could see:
| Signal | Without user_agent |
With an iPhone user_agent |
|---|---|---|
User-Agent request header |
... HeadlessChrome/148.0.0.0 ... |
The iPhone string |
navigator.userAgent |
Desktop Chrome string | The iPhone string |
Sec-CH-UA and Sec-CH-UA-Mobile headers |
Sent (?0) |
Not sent |
navigator.userAgentData.mobile |
false |
false |
window.innerWidth |
1280 | 1280 |
(pointer: coarse) media query |
No match | No match |
Two results follow:
- A server that reads the
User-Agentheader gets your string and can send its mobile HTML. - The browser still lays the page out at the viewport width, with a mouse pointer. Responsive CSS sees a desktop window.
Get the mobile layout, not only the mobile HTML
Send the User-Agent together with a mobile viewport:
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", "viewport": { "width": 390, "height": 844, "scale": 3, "mobile": true }, "browser": { "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" } }' \ -o mobile-ua.png
Node.js
import { writeFile } from 'node:fs/promises'; const body = { "url": "https://example.com", "viewport": { "width": 390, "height": 844, "scale": 3, "mobile": true }, "browser": { "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" } }; 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('mobile-ua.png', Buffer.from(await res.arrayBuffer()));
Python
import json import os import sys import urllib.error import urllib.request body = { 'url': 'https://example.com', 'viewport': { 'width': 390, 'height': 844, 'scale': 3, 'mobile': True, }, 'browser': { 'user_agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', }, } 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('mobile-ua.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', viewport: { width: 390, height: 844, scale: 3, mobile: true }, browser: { user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1' } } 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('mobile-ua.png', response.body)
PHP
<?php $body = [ 'url' => 'https://example.com', 'viewport' => [ 'width' => 390, 'height' => 844, 'scale' => 3, 'mobile' => true, ], 'browser' => [ 'user_agent' => 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1', ], ]; $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('mobile-ua.png', $image);
Go
package main import ( "bytes" "io" "log" "net/http" "os" ) const body = `{ "url": "https://example.com", "viewport": { "width": 390, "height": 844, "scale": 3, "mobile": true }, "browser": { "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" } }` 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("mobile-ua.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", "viewport": { "width": 390, "height": 844, "scale": 3, "mobile": true }, "browser": { "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" } } """; 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("mobile-ua.png"), response.body()); } }
viewport.mobile: true turns on mobile emulation, and scale: 3 matches the iPhone's pixel density. The output is 1170 px wide, which is 390 CSS pixels at 3x.
A shorter route is a device preset. With GET, device=iphone_14_pro sets the viewport, the scale and an iPhone User-Agent in one parameter:
curl -sS --fail \ -H "Authorization: Bearer $RS_API_KEY" \ "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&device=iphone_14_pro" \ -o iphone.png
If you send both a device and user_agent, your user_agent wins and the device keeps its viewport. The device reference lists the presets.
Only the User-Agent, with GET
The GET endpoint accepts user_agent as a query parameter. URL-encode it, because User-Agent strings contain spaces, slashes and semicolons:
UA='Mozilla/5.0 (compatible; AcmeStatusCheck/1.0; +https://acme.example/bot)' curl -sS --fail -G \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "user_agent=$UA" \ https://api.renderscreenshot.com/v1/screenshot \ -o status.png
A descriptive string like this one helps when you capture your own sites. You can filter the captures out of your analytics, or let them through a firewall rule, by matching the name.
What a User-Agent cannot do
The renderer runs on Cloudflare Browser Run. Cloudflare adds headers to every Browser Run request, such as cf-brapi-devtools and a signed Signature header, and these cannot be removed or overridden (automatic request headers, checked 2026-10-15). Cloudflare's documentation also says that Browser Run requests are always identified as a bot and that changing the user agent does not bypass bot protection (PDF endpoint notes, checked 2026-10-15). A site that blocks automated browsers still blocks the capture, whatever string you send.
Parameters that matter
| Parameter | Type | Default | Notes |
|---|---|---|---|
browser.user_agent |
string | Chrome default | GET: user_agent |
viewport.width / height |
integer | 1280 / 800 | 1 to 7,680 and 1 to 4,320 |
viewport.scale |
number | 1 | 1 to 3 |
viewport.mobile |
boolean | false |
Mobile emulation |
device (GET) |
string | none | Sets viewport, scale and User-Agent |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Mobile HTML but desktop layout | Viewport is still 1280 px | Add a mobile viewport or use a device preset. |
| Desktop HTML with an iPhone User-Agent | The server reads client hints or the viewport | Client hints are not sent with an override. Use a device preset or the site's mobile URL. |
| Bot challenge page in the screenshot | The site blocks automated browsers | The User-Agent cannot fix this. Capture a URL you control, or ask the site owner to allow the capture. |
| Garbled or truncated User-Agent | The GET value was not URL-encoded | Use --data-urlencode, or send it in a POST body. |
Related
- Localized screenshots cover the other headers that change what a server sends, such as
Accept-Language. - Mobile screenshots cover device presets in depth.
- Bot protection and CAPTCHAs explain what to do when a site blocks the capture.
- Create a free account to test the capture against your own server logs.