Urlbox

PHP website screenshots

Take website screenshots in PHP and Laravel with the Urlbox SDK — signed render links, sync renders saved to disk, and async renders with verified webhooks.

The urlbox/screenshots Composer package (source on GitHub) wraps the Urlbox API for plain PHP and Laravel. Use it to:

  • Generate a render link — a signed URL for an <img> tag. The browser fetches the screenshot, so your page renders straight away.
  • Render synchronously — wait for the render and get a renderUrl, or save the file straight to disk.
  • Render asynchronously — start a render and get the result by webhook or by polling. Best for long or bulk renders.

See Sync vs Async for the trade-offs.

Install

composer require urlbox/screenshots

Find your API key, secret and webhook secret in your project's settings on the dashboard.

<?php
require 'vendor/autoload.php';

use Urlbox\Screenshots\Urlbox;

$urlbox = Urlbox::fromCredentials(
    getenv('URLBOX_KEY'),
    getenv('URLBOX_SECRET'),
    getenv('URLBOX_WEBHOOK_SECRET') // optional, only needed to verify webhooks
);

Laravel

The package registers itself in Laravel 5.5 and later. Add your credentials to config/services.php:

'urlbox' => [
    'key'            => env('URLBOX_KEY'),
    'secret'         => env('URLBOX_SECRET'),
    'webhook_secret' => env('URLBOX_WEBHOOK_SECRET'),
],

and to your .env:

URLBOX_KEY=YOUR_URLBOX_KEY
URLBOX_SECRET=YOUR_URLBOX_SECRET
URLBOX_WEBHOOK_SECRET=YOUR_URLBOX_WEBHOOK_SECRET
$imgUrl = $urlbox->generateSignedUrl([
    'url'    => 'example.com',
    'width'  => 1280,
    'height' => 1024,
]);
// https://api.urlbox.com/v1/API_KEY/TOKEN/png?url=example.com&width=1280&height=1024

echo '<img src="' . htmlspecialchars($imgUrl) . '" alt="Screenshot of example.com">';

The link is signed with your secret, so nobody can change the options and spend your credits. The first request renders the page, and later requests for the same link are served from the cache. See Render links for caching and refresh options.

Render synchronously

render() calls POST /v1/render/sync and returns the decoded response. Pass a path as the second argument to download the file too:

$result = $urlbox->render(
    [
        'url'                 => 'https://example.com',
        'full_page'           => true,
        'hide_cookie_banners' => true,
    ],
    __DIR__ . '/screenshot.png'
);

echo $result['renderUrl']; // temporary URL, expires after 30 days
echo $result['localPath']; // where the file was saved

Render asynchronously

renderAsync() starts a render and returns straight away with a renderId and a statusUrl:

$render = $urlbox->renderAsync([
    'url'         => 'https://example.com',
    'full_page'   => true,
    // Omit webhook_url if you'd rather poll (option 2 below).
    'webhook_url' => 'https://your-app.com/webhooks/urlbox',
]);

$renderId  = $render['renderId'];
$statusUrl = $render['statusUrl'];

Then collect the result in one of two ways.

Option 1: receive a webhook

Urlbox posts the result to your webhook_url when the render finishes. Check the signature with verifyWebhookSignature() before you trust the payload. The client needs your project's webhook secret (the third argument to fromCredentials), which is different from your API secret.

$header = $_SERVER['HTTP_X_URLBOX_SIGNATURE'] ?? '';
$body   = file_get_contents('php://input'); // the raw body, exactly as sent

if (!$urlbox->verifyWebhookSignature($header, $body)) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
if ($event['event'] === 'render.succeeded') {
    $renderUrl = $event['result']['renderUrl'];
}

In Laravel, use $request->header('X-Urlbox-Signature') and $request->getContent() in the controller instead. See Webhooks for the payload format.

Option 2: poll the status URL

No public endpoint to receive webhooks (an Artisan command, a queued job, a CLI script)? Poll statusUrl until the render finishes. The SDK already depends on Guzzle:

use GuzzleHttp\Client;

function waitForRender(string $statusUrl, int $timeout = 300): array
{
    $http     = new Client(['timeout' => 30]);
    $deadline = time() + $timeout;

    while (time() < $deadline) {
        $response = $http->get($statusUrl, [
            'headers'     => ['Authorization' => 'Bearer ' . getenv('URLBOX_SECRET')],
            'http_errors' => false, // a 404 still carries a JSON status
        ]);
        $render = json_decode((string) $response->getBody(), true);

        if ($render['status'] === 'succeeded') {
            return $render; // includes renderUrl and size
        }
        if (in_array($render['status'], ['failed', 'not-found'], true)) {
            throw new RuntimeException($render['reason'] ?? $render['status']);
        }
        sleep(2); // "created" or "retrying": still working
    }

    throw new RuntimeException('Timed out waiting for render');
}

$renderUrl = waitForRender($statusUrl)['renderUrl'];

Send your secret with each poll, so only your project can read the render's status.

Next steps