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:

  1. A server that reads the User-Agent header gets your string and can send its mobile HTML.
  2. 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 in this topic

Was this page helpful?