Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Screenshots Laravel Package

urlbox/screenshots

Laravel wrapper for the Urlbox screenshot API, letting you capture full-page or viewport screenshots of any website as PNG or JPEG, with options for thumbnails, device sizes, retina, and custom rendering.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require urlbox/screenshots:^3.0

Grab an API key + secret from the Urlbox dashboard (they are separate values — the key goes in the URL path, the secret is used to HMAC-sign the query string). If you plan to consume render-completion webhooks, also copy the webhook secret — as of 3.0.1 it is the optional third argument to fromCredentials().

Keep credentials out of code and register the client once:

// config/services.php
'urlbox' => [
    'key'            => env('URLBOX_API_KEY'),
    'secret'         => env('URLBOX_API_SECRET'),
    'webhook_secret' => env('URLBOX_WEBHOOK_SECRET'),
],
// app/Providers/AppServiceProvider.php
use Urlbox\Screenshots\Urlbox;

public function register(): void
{
    $this->app->singleton(Urlbox::class, fn () => Urlbox::fromCredentials(
        config('services.urlbox.key'),
        config('services.urlbox.secret'),
        config('services.urlbox.webhook_secret'), // nullable — 3.0.1 default
    ));
}

First use case — a link-preview thumbnail for a saved URL:

use Urlbox\Screenshots\Urlbox;

$urlbox = app(Urlbox::class);

$options = [
    'url'         => 'https://laravel.com',
    'format'      => 'jpg',
    'thumb_width' => 600,
    'block_ads'   => true,
];

$urlbox->getImage($options, ['output' => 'download', 'file_name' => 'laravel.jpg']);

Read the package README first, then read src/Urlbox.php end to end — it is one small class, and the only things that matter are fromCredentials(), getApiUrl() (builds the signed URL), and getImage() (a thin Guzzle wrapper around that URL). Once you have seen getApiUrl() output you understand the whole package: it is a signed-URL builder, nothing more.

Implementation Patterns

Prefer getApiUrl() over getImage(). The wrapper's transport is the least valuable part of the package; the signature logic is the valuable part. Building the URL yourself and fetching it with Laravel's HTTP client gives you timeouts, retries, Http::fake(), and storage for free:

$signedUrl = $urlbox->getApiUrl($options);

$response = Http::timeout(30)
    ->retry(2, 2000, throw: false)
    ->get($signedUrl);

abort_unless(
    $response->successful() && str_starts_with($response->header('Content-Type'), 'image/'),
    502, 'Screenshot render failed'
);

Always queue it. A cold render takes 3–15 seconds. Do this in a job, never in the request cycle:

class GenerateThumbnail implements ShouldQueue
{
    public int $tries = 2;
    public int $timeout = 60;
    public array $backoff = [10, 60];

    public function __construct(public int $postId) {}

    public function handle(Urlbox $urlbox): void
    {
        $post = Post::findOrFail($this->postId);

        $url = $urlbox->getApiUrl([
            'url'           => $post->canonical_url,
            'format'        => 'webp',
            'width'         => 1200,
            'height'        => 630,
            'block_ads'     => true,
            'hide_selector' => '.cookie-banner, .newsletter-modal',
            'wait_for'      => 'main',
            'retina'        => true,
        ]);

        $bytes = Http::timeout(45)->get($url)->throw()->body();

        Storage::disk('public')->put("thumbs/{$post->id}.webp", $bytes);
        $post->update(['thumb_generated_at' => now()]);
    }
}

Cache aggressively. Screenshots are billed per render and the result for a given URL changes slowly. Key on a hash of the normalized options array, not the raw URL string:

$key = 'urlbox:'.md5(json_encode($options));
$url = Cache::remember($key, now()->addDays(7), fn () => $urlbox->getApiUrl($options));

On-demand signed URLs for the frontend. For user-generated previews, generate the signed URL server-side and emit it as an <img src> so bytes never touch your app server. Gate it behind your own route or short-lived cache so you can invalidate/rate-limit — see Gotchas.

Use the webhook secret (new in 3.0.1) for callback verification. If you pass render_complete/callback options or use Urlbox's async render endpoint, the callback arrives as an unsigned HTTP POST — anyone can forge it. Register the client with the webhook secret and verify the payload's HMAC against it in a dedicated route before trusting the body:

Route::post('/webhooks/urlbox', function (Request $request) {
    $expected = hash_hmac('sha256', $request->getContent(), config('services.urlbox.webhook_secret'));

    abort_unless(hash_equals($expected, (string) $request->header('X-Urlbox-Signature')), 403);

    RenderCompleted::dispatch($request->json('url'));
    return response()->noContent();
});

Check the installed src/Urlbox.php for the exact header name and hashing scheme your version emits — 3.0.1 only guarantees the secret is nullable in fromCredentials(), not the verification helper's shape.

Wrap it in a one-method interface. App\Contracts\Screenshotter with screenshot(array $options): string. Bind the Urlbox implementation in a service provider. This is a 300-line SDK with 11 stars; you want a one-file swap if it stalls.

Batch/bulk: loop the job dispatch with Bus::batch([...]), cap concurrency low (5–10). Urlbox renders in parallel on their side but your plan has concurrency limits and you will get 429s on a 200-item burst.

Gotchas and Tips

  • 3.0.0 → 3.0.1 is safe; 1.x/2.x → 3.x is not. The 3.0.1 patch only makes the webhook-secret argument nullable (previously you had to pass something), bumps guzzlehttp/psr7 to ^2.6.1, and adjusts composer.json. But if your installed version is still ^1.0, treat the jump to ^3.0 as a major upgrade: re-read vendor/urlbox/screenshots/src/Urlbox.php and re-check every method signature before bumping.
  • fromCredentials($key, $secret) still works. The whole point of PR #10 is that the third argument defaults to null — you will not get a compile error. You will silently lose the ability to verify webhooks, so if you consume callbacks, set it explicitly.
  • The secret is not a secret once signed. The signed URL is a capability: anyone holding it can burn your quota until you rotate keys. Don't leak it in public caches, logs, or analytics referrers. Proxy through your own route or use short-lived cache keys.
  • Never hand-build the query string. The token is an HMAC over the parameter string; true vs 1, key order, or different URL-encoding silently yields a 403. Always go through getApiUrl() / getImage().
  • Signature-affecting types: booleans must serialize the way the SDK expects. If you assemble options from a form request, cast explicitly ('full_page' => $request->boolean('full_page')) rather than passing "0"/"1" strings.
  • Check Content-Type, not just status. Render failures sometimes come back with a 200 and an error body or placeholder image. Guard for image/ before storing bytes, otherwise you will happily persist a JSON error as a .webp.
  • Timeouts double-bill. If your client times out at 10s but Urlbox completes the render at 12s, you paid for it and got nothing — then your retry pays again. Set client timeout above your expected render time (30–60s) and let the job handle retries.
  • delay vs wait_for: prefer wait_for on a selector you know exists post-render; delay is a blunt sleep that costs latency on every request. Combine with hide_selector for cookie banners and block_ads for deterministic output.
  • thumb_width is not width. width/height control the viewport and therefore the render; thumb_width only resizes the returned file (saves bytes, not render cost).
  • full_page is a trap on infinite-scroll sites — you get a 40,000px-tall image and a slow render. Gate it behind a per-domain allowlist.
  • Long URLs. Deep links with huge query strings or data-URI pages can exceed Urlbox's URL limit. Shorten via your own redirect route or store the target and pass an internal slug.
  • Testing: never hit the API in the suite. Http::fake(['api.urlbox.io/*' => Http::response($png, 200, ['Content-Type' => 'image/png'])]), or bind a fake Screenshotter in the container. Assert on the generated URL's query params rather than the network. Test the webhook route with a hand-computed hash_hmac so a broken signature check fails loudly.
  • Debugging a bad render: dd($urlbox->getApiUrl($options)), paste into a browser or curl -I. If it 403s, it is almost always signature/encoding; if it times out, it is a page that never reaches idle — add disable_js or wait_for.
  • Verify the API surface against your installed version. Method names, the fromCredentials() arity, and render-output options (['output' => 'download'|'display']) have shifted across releases; the local vendor/urlbox/screenshots/src is the source of truth, not the README.
  • Pin the version ("urlbox/screenshots": "^3.0") and schedule a quarterly look at the repo — low star count, single maintainer. Note that 3.0.1's composer.json change can narrow the PHP/Guzzle constraints your app must satisfy; run composer why urlbox/screenshots before bumping in a legacy app. Your interface wrapper is the insurance policy.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums