PHP has no built-in way to render a web page, so the moment you search for "php screenshot website" you hit a fork: install a headless browser stack and drive it from PHP, or skip the browser and call a hosted API over HTTP. This guide covers both honestly, with working code and the gotcha each one hits. First, a trap to clear out of the way.
The trap: imagegrabscreen is not what you want
The top result people find is PHP's imagegrabscreen(). It captures the operating system's desktop screen, the actual pixels on a monitor. It needs a graphical session, only works where a display is attached, and has nothing to do with loading a URL. On a typical headless Linux web server there is no screen to grab. If you want a screenshot of a website from its URL, imagegrabscreen is the wrong function. You need something that renders the page in a browser.
Approach 1: Spatie Browsershot (the standard DIY route)
Browsershot is the most popular PHP screenshot library. It does not render anything itself; it shells out to a Node.js script that drives Puppeteer and a headless Chromium. So the API is clean PHP, but the work happens in a real browser:
use Spatie\Browsershot\Browsershot;
Browsershot::url('https://example.com')
->windowSize(1280, 720)
->fullPage()
->waitUntilNetworkIdle()
->save('example.png');
Install it with composer require spatie/browsershot. The catch is everything that has to exist before that line runs: the server needs Node.js, the puppeteer npm package, and a Chromium binary, all installed and kept current. ->fullPage() captures the entire scrolling page instead of the viewport, and ->waitUntilNetworkIdle() is how you avoid capturing a half-loaded page. Browsershot is the right pick when you control the server and can run that Node plus Chromium stack. It is the wrong pick when you are on shared hosting, a locked-down container, or a platform where you would rather not babysit a browser.
The gotcha: waiting for the page
The most common bug in any headless capture is firing the screenshot before the page is ready. Do not lean on a fixed sleep. Browsershot gives you real signals:
Browsershot::url('https://example.com')
->waitUntilNetworkIdle()
->waitForFunction('document.fonts.ready')
->setDelay(500)
->save('example.png');
waitUntilNetworkIdle() waits for in-flight requests to settle, waitForFunction('document.fonts.ready') avoids a fallback-font flash, and setDelay adds a final cushion in milliseconds for animations or late hydration. The same class of problem and the same controls show up whether you drive Puppeteer directly in Node or a browser in Python.
Approach 2: Skip the browser, call an API
Browsershot means you install and operate a browser on your PHP server: Node, Puppeteer, a Chromium binary, the memory each headless instance eats, and the upgrade treadmill when Chrome ships a breaking change. If screenshots are a production feature rather than a one-off script, a screenshot API removes all of that. You send one HTTP request:
curl https://api.grabbit.live/v1/grabs \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"width": 1280,
"full_page": true,
"format": "webp"
}'
The same call from PHP with Guzzle:
use GuzzleHttp\Client;
$client = new Client();
$response = $client->post('https://api.grabbit.live/v1/grabs', [
'headers' => ['Authorization' => 'Bearer sk_live_...'],
'json' => [
'url' => 'https://example.com',
'width' => 1280,
'full_page' => true,
'format' => 'webp',
],
]);
$imageUrl = json_decode((string) $response->getBody(), true)['image_url'];
The response is a hosted image URL you can store and reuse:
{
"id": "grb_01jx...",
"status": "done",
"image_url": "https://cdn.grabbit.live/grabs/grb_01jx....webp",
"width": 1280,
"format": "webp",
"bytes": 48210,
"execution_ms": 1180
}
The Browsershot options map cleanly onto request parameters: window size is width (320 to 1920) and height (240 to 1080), ->fullPage() is full_page, the wait becomes delay_ms (0 to 10000), and a single-element capture is a selector field. format is png, jpeg, or webp. There is no Node, Puppeteer, or Chrome to install on your side, because the render runs on the provider's infrastructure.
Which to use
- You control the server and can run Node plus Chromium: Browsershot. It is well maintained and gives you full control over the headless browser.
- Shared hosting, a slim container, or you would rather not operate a browser: a screenshot API, so PHP just makes an HTTP request.
-
Whatever you do, not
imagegrabscreen: it captures the desktop, not a web page.
For the same decision in other languages, see screenshots in Python and screenshots in Node.js. If you are weighing hosted providers, the honest comparison of screenshot APIs covers the trade-offs without the marketing. Test-environment keys render free placeholders, so you can wire the request into your PHP code before adding a card.
Originally published on the Grabbit blog.
Top comments (0)