ScreenshotNeo

BlogHow-to

How to Use the Browserless Screenshot API in a PHP Website Project

Capture and save Browserless screenshots from PHP with cURL or Guzzle. Configure full-page shots, formats and waits, and fix common response and capture errors.

By the ScreenshotNeo team4 October 202610 min read

To take a website screenshot from a PHP project with Browserless, send a server-side POST request with JSON to Browserless’s /screenshot endpoint. Include your API token as the token query parameter, put the target in url, and save the returned image bytes. Set options.fullPage to true to capture the full document. Keep the token on the PHP server; do not put it in browser JavaScript. See Browserless’s Screenshot API reference and PHP integration guide.

Prerequisites

  • PHP with the cURL extension enabled, or Composer and Guzzle.
  • A Browserless API token.
  • The correct base URL for your Browserless deployment. The Cloud example below uses the SFO region host; Browserless offers region-specific and self-hosted endpoints, so use the endpoint assigned to your deployment.

Set the token in your server environment, for example as BROWSERLESS_TOKEN. The examples read it with getenv() so it does not need to be committed to source control. Since the token appears in the request URL, avoid logging full request URLs.

Minimal PHP example with cURL

Save this as screenshot.php and run it from the command line or call the function from your application. This requests a full-page PNG and checks both transport errors and HTTP errors before writing the file.

<?php
declare(strict_types=1);

$token = getenv('BROWSERLESS_TOKEN');
if ($token === false || $token === '') {
    throw new RuntimeException('Set the BROWSERLESS_TOKEN environment variable.');
}

// Replace this sample with the base URL for your Cloud region or deployment.
$baseUrl = 'https://production-sfo.browserless.io';
$endpoint = $baseUrl . '/screenshot?' . http_build_query(['token' => $token]);
$payload = [
    'url' => 'https://example.com/',
    'options' => [
        'fullPage' => true,
        'type' => 'png',
    ],
];
$json = json_encode($payload, JSON_THROW_ON_ERROR);

$curl = curl_init($endpoint);
curl_setopt_array($curl, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $json,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 90,
]);

$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException('Browserless request failed: ' . $message);
}
$status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
$contentType = (string) curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
curl_close($curl);

if ($status < 200 || $status >= 300) {
    throw new RuntimeException("Browserless returned HTTP {$status}: " . substr($response, 0, 1000));
}
if (!str_starts_with($contentType, 'image/')) {
    throw new RuntimeException('Expected an image response; received ' . ($contentType ?: 'unknown content type'));
}

if (file_put_contents(__DIR__ . '/screenshot.png', $response) === false) {
    throw new RuntimeException('Could not write screenshot.png');
}
echo "Saved screenshot.png ({$contentType})" . PHP_EOL;

The endpoint returns image data. With this example’s default binary response, write $response directly; do not base64-decode it. Browserless also documents requesting options.encoding: "base64". If you choose that mode, the successful response body is encoded text and the matching save step is base64_decode($response, true), followed by writing the decoded bytes. Do not mix the two modes.

Using Guzzle in an existing PHP application

Guzzle is useful when the project already depends on it and you want its HTTP response and exception handling. Install it with composer require guzzlehttp/guzzle, then use a small function such as this:

<?php
declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;

$token = getenv('BROWSERLESS_TOKEN');
if (!$token) {
    throw new RuntimeException('Set BROWSERLESS_TOKEN.');
}
$client = new Client([
    'base_uri' => 'https://production-sfo.browserless.io', // Set your deployment host.
    'connect_timeout' => 10,
    'timeout' => 90,
]);

try {
    $response = $client->post('/screenshot', [
        'query' => ['token' => $token],
        'json' => [
            'url' => 'https://example.com/',
            'options' => ['fullPage' => true, 'type' => 'png'],
        ],
    ]);
} catch (GuzzleException $e) {
    throw new RuntimeException('Browserless HTTP request failed: ' . $e->getMessage(), 0, $e);
}

$body = (string) $response->getBody();
$contentType = $response->getHeaderLine('Content-Type');
if (!str_starts_with($contentType, 'image/')) {
    throw new RuntimeException('Expected an image response; received ' . ($contentType ?: 'unknown content type'));
}
if (file_put_contents(__DIR__ . '/screenshot.png', $body) === false) {
    throw new RuntimeException('Could not write screenshot.png');
}

The Laravel Browserless package is another route, but Browserless documents it as community-supported and maintained by its creator rather than officially supported by Browserless. For a Laravel app, check the package’s current documentation and compatibility before adopting it.

Request shape and useful screenshot options

The current REST request is JSON sent with POST. A URL capture resembles {"url":"https://example.com/","options":{"fullPage":true,"type":"png"}}. For inline markup, send html instead of url; do not include both fields in the same request. The response is PNG, JPEG, or WebP according to the requested type. Consult the API reference for the complete current option schema.

Need Configuration Notes
Entire document options.fullPage: true Captures beyond the initial viewport. Long or dynamically growing pages may take longer and produce larger files.
Image format options.type: png, jpeg, or webp Use the matching extension and content type when saving or serving the response. Quality applies to lossy formats where supported.
One element Top-level selector Browserless waits for the selected element and crops to its bounding box. This is different from a fixed coordinate crop.
Fixed crop options.clip with x, y, width, and height Use when the crop is known in page coordinates.
Viewport and sharpness Viewport dimensions and deviceScaleFactor in the supported screenshot configuration Choose dimensions for the layout to render and the output pixel density. Confirm exact accepted fields in the API reference.
Wait for content Shared request wait settings, such as selector, event, function, or timeout conditions Wait for a meaningful page-ready condition instead of relying on a fixed delay where possible.
Lazy-loaded content Top-level scrollPage: true, often with full-page mode Scrolling can trigger images and sections that load only when they approach the viewport.
Reduce unnecessary loads rejectResourceTypes or rejectRequestPattern Blocking resources may reduce work, but can also remove fonts, images, or scripts needed for a faithful capture.
Navigation behavior gotoOptions and bestAttempt Use navigation settings and continue-on-error behavior only when they fit the desired failure policy.

Full-page capture with lazy content

For a page that loads content as it scrolls, add scrollPage: true at the request’s top level while keeping fullPage inside options:

$payload = [
    'url' => 'https://example.com/articles/long-page',
    'scrollPage' => true,
    'options' => ['fullPage' => true, 'type' => 'webp'],
];

Lazy loading does not guarantee that every third-party widget or infinite-scroll feed will finish. For an infinite page, decide on a bounded capture strategy and avoid waiting forever for content that never stabilizes.

Capture inline HTML

Use html when the page is markup supplied by your application rather than a public URL. The API supports script and style injection before capture; use those only for content you control and need to render.

$payload = [
    'html' => '<!doctype html><html><body><h1>Invoice preview</h1></body></html>',
    'options' => ['type' => 'png'],
];

cURL, Python, and Node.js request equivalents

These examples make the same server-side request and save the returned binary image. They are useful for checking the endpoint independently or implementing a capture worker in another runtime. Use the base URL assigned to your deployment.

cURL

curl --fail-with-body -sS -X POST \
  'https://production-sfo.browserless.io/screenshot?token=YOUR_API_TOKEN_HERE' \
  -H 'Content-Type: application/json' \
  --data '{"url":"https://example.com/","options":{"fullPage":true,"type":"png"}}' \
  --output screenshot.png

Python

import os
import requests

base_url = os.environ.get("BROWSERLESS_BASE_URL", "https://production-sfo.browserless.io")
token = os.environ["BROWSERLESS_TOKEN"]
response = requests.post(
    f"{base_url}/screenshot",
    params={"token": token},
    json={"url": "https://example.com/", "options": {"fullPage": True, "type": "png"}},
    timeout=90,
)
response.raise_for_status()
if not response.headers.get("content-type", "").startswith("image/"):
    raise RuntimeError(f"Expected image response, got {response.headers.get('content-type')}")
with open("screenshot.png", "wb") as image_file:
    image_file.write(response.content)

Node.js

const baseUrl = process.env.BROWSERLESS_BASE_URL || 'https://production-sfo.browserless.io';
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');
const endpoint = new URL('/screenshot', baseUrl);
endpoint.searchParams.set('token', token);
const response = await fetch(endpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com/',
    options: { fullPage: true, type: 'png' }
  })
});
if (!response.ok) throw new Error(`Browserless returned HTTP ${response.status}: ${await response.text()}`);
const bytes = Buffer.from(await response.arrayBuffer());
await (await import('node:fs/promises')).writeFile('screenshot.png', bytes);

Errors and troubleshooting

Symptom Likely cause What to check
401 or 403 response Missing, invalid, or wrong deployment token; incorrect host or endpoint Confirm the token and deployment URL, and ensure the query parameter is named token. Do not paste token-bearing URLs into logs or support tickets.
400 response Malformed JSON, unsupported option placement, or conflicting request fields Send valid JSON with Content-Type: application/json. Use html or url, not both. Put selector at the top level and screenshot settings under options as documented.
cURL returns false DNS, TLS, connection, or timeout problem Read curl_error(), verify the host is reachable from the PHP server, and set a suitable connection and total timeout.
File is unreadable or corrupt Binary response was base64-decoded, or base64 response was written as binary Keep response mode and save logic paired. Check status and Content-Type before writing.
HTML error saved with .png extension Application saved an error response without checking HTTP status or content type Reject non-2xx statuses and non-image content types before writing output.
Blank page, CAPTCHA, access denied, or missing page content The destination may block automation, or capture occurred before content appeared Check the actual returned image, wait for the relevant selector or event, and review Browserless’s documented Unblock API for cases it supports. Do not assume the basic screenshot endpoint bypasses bot checks.
Missing images or sections on a long page Lazy-loaded content was never brought into view Try scrollPage: true together with fullPage: true. Pages using endless scrolling may need a bounded application-specific approach.
Request times out Slow destination, heavy page, full-page capture, or a wait condition that never resolves Use a bounded wait and timeout, narrow the capture if possible, and retry only transient failures with a limit.
Guzzle throws an exception Connection failure or HTTP error configured to throw Catch GuzzleException, log a sanitized error and status, and preserve the response body only when safe.

REST limitations, performance, reliability, and cost

Browserless REST calls are stateless single-action requests: each request launches a browser, performs one task, and closes the session. A screenshot request is a good fit for independent captures, but it does not retain a logged-in browser or a sequence of click-and-fill actions between requests. For multi-step interaction, branching, or persistent state, use a documented session-based browser-control route or BrowserQL. See the REST API overview.

Capture time depends on the destination page, its assets, the wait condition, and whether the whole page must be rendered. This guide makes no fixed latency or throughput claim. Keep timeouts finite; select the smallest capture region and image format that meet your use case; avoid loading resources you do not need, while checking that blocking does not damage fidelity. For a production worker, record sanitized status, duration, response content type, and failure category; do not record the token. Retry only plausible transient failures, with a cap and backoff, to avoid multiplying load and charges. Check your Browserless plan and current pricing for quota and billing details; no price or quota is asserted here.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its single GET request returns an image or PDF, and the same parameter names used by other screenshot APIs work to make switching easier. Read the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.

FAQ

Can I capture only one DOM element?

Yes. Use the top-level selector field to target an element and let the API crop to its bounds. Use options.clip when the crop is a fixed rectangle instead.

Can one REST request click a button and then capture?

The screenshot REST call performs one capture action and does not preserve a browser session for a multi-step workflow. Use a session-oriented browser-control approach when the page must be manipulated first.

Can I use a URL that requires my application’s login?

The screenshot endpoint does not keep a login session between calls. For authenticated pages, use an approach that supplies the required authentication within the request’s supported configuration or use a session-based flow when login requires multiple interactions.

Does full-page mode include content that appears after scrolling?

It can, but lazy-loaded content may require scrolling first. Set scrollPage: true and verify the resulting image for pages with dynamic or endless content.