How to Capture a Full-Page Website Screenshot with ApiFlash in Laravel
Capture a full-page PNG with ApiFlash from Laravel, store the image safely, and handle readiness, errors, and rate limits.
Use Laravel’s HTTP client to call ApiFlash’s https://api.apiflash.com/v1/urltoimage endpoint, set full_page=true, and save the successful response body as a PNG. ApiFlash requires an access_key and a complete target URL including its protocol. Full-page capture is available only in PNG format; ApiFlash ignores height and thumbnail_width in this mode. ApiFlash API documentation.
1. Configure the ApiFlash key
Keep the key on the server. Add it to your environment file and expose it through Laravel configuration so application code does not read environment variables directly.
# .env
APIFLASH_ACCESS_KEY=your_api_key
Add the service entry to config/services.php:
'apiflash' => [
'key' => env('APIFLASH_ACCESS_KEY'),
],
After changing configuration in an environment that caches config, rebuild the configuration cache using your normal deployment process. Do not put the key in a rendered link or make the request from browser JavaScript: ApiFlash accepts it as a request parameter, and exposing that request exposes the credential.
2. Capture and store the full-page PNG
This Laravel example validates the inputs, makes the API request, checks the response before treating it as image data, and stores the resulting bytes on Laravel’s local storage disk. Laravel’s HTTP client provides the request and response methods used here. Laravel HTTP client documentation.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use RuntimeException;
class ScreenshotController
{
public function capture(Request $request)
{
$validated = $request->validate([
'url' => ['required', 'url'],
]);
$key = config('services.apiflash.key');
if (! is_string($key) || $key === '') {
throw new RuntimeException('ApiFlash is not configured.');
}
$response = Http::timeout(90)->get(
'https://api.apiflash.com/v1/urltoimage',
[
'access_key' => $key,
'url' => $validated['url'],
'full_page' => 'true',
'format' => 'png',
]
);
if ($response->failed()) {
// Log status and a safely limited error body in production.
abort($response->status(), 'ApiFlash screenshot request failed.');
}
$path = 'screenshots/'.bin2hex(random_bytes(16)).'.png';
Storage::disk('local')->put($path, $response->body());
return response()->json([
'path' => $path,
]);
}
}
Register the controller using your application’s routing conventions, then submit a URL such as https://example.com. The validation rule checks URL syntax; it does not establish that the destination is safe for your application to fetch. If users control the target, apply your own outbound URL and network restrictions to prevent requests to internal services.
3. Choose how the API responds
Raw image bytes (default)
The example uses ApiFlash’s default response: image bytes with content headers. Save $response->body() only after checking the HTTP status. Do not call json() on this response and treat it as a screenshot.
JSON response with a screenshot URL
Set response_type to json when you need a screenshot URL rather than the image bytes in the response. Handle the JSON response as metadata, not as a PNG file:
$response = Http::timeout(90)->get(
'https://api.apiflash.com/v1/urltoimage',
[
'access_key' => config('services.apiflash.key'),
'url' => 'https://example.com',
'full_page' => 'true',
'response_type' => 'json',
]
);
if ($response->failed()) {
abort($response->status(), 'ApiFlash screenshot request failed.');
}
$data = $response->json();
$screenshotUrl = $data['url'] ?? null;
if (! is_string($screenshotUrl) || $screenshotUrl === '') {
throw new RuntimeException('ApiFlash returned no screenshot URL.');
}
ApiFlash’s JSON response can also include extracted HTML or text when those options are requested. Use the API documentation to confirm the relevant parameter names and response fields for your use case.
4. Set the page readiness behavior
A screenshot can be technically successful while still missing content that loads late. ApiFlash defaults wait_until to network_idle; documented alternatives include dom_loaded and page_loaded. Use the readiness mode that fits the target page:
network_idle: useful when the page settles after network activity, but pages with persistent requests may take longer.dom_loadedorpage_loaded: can suit pages where waiting for network idleness is unnecessary.wait_for: wait for a CSS selector that marks the content you need. ApiFlash documents an error if the selector does not appear within 15 seconds.delay: adds a delay of up to 10 seconds after load. ApiFlash recommends considering a readiness condition instead when possible.
For content revealed by scrolling, such as lazy-loaded images, set scroll_page=true so the page is scrolled before capture. For example, add 'wait_for' => '.article-content' or 'scroll_page' => 'true' to the parameter array. A selector should identify content that actually exists on the target page.
5. Request shape and configuration choices
| Choice | Use it when | Trade-off |
|---|---|---|
| GET query parameters | You want the concise Laravel pattern shown above. | The access key is part of the URL sent to the API. Keep requests and logs containing it private. |
| POST form data | Your deployment prefers parameters in a form body. | ApiFlash documents POST form parameters; confirm the request shape against the current API docs. |
| Raw binary response | Your application needs to save, stream, or transform the PNG. | Check status before writing the body to a file. |
| JSON response | Your workflow needs the returned screenshot URL or requested extraction metadata. | Parse JSON and use its fields; the response body is not the image itself. |
ApiFlash accepts GET query parameters or POST form parameters. A Laravel form POST can be written as follows; keep full_page enabled and request PNG for full-page capture:
$response = Http::asForm()->timeout(90)->post(
'https://api.apiflash.com/v1/urltoimage',
[
'access_key' => config('services.apiflash.key'),
'url' => 'https://example.com',
'full_page' => 'true',
'format' => 'png',
]
);
Do not combine full-page capture with assumptions about height or thumbnail_width; ApiFlash says those parameters are ignored in full-page mode. For non-full-page captures, consult the API documentation for available output formats and dimensions.
6. Other runnable client examples
These examples make the same full-page PNG request outside Laravel. Keep the ApiFlash key in a server-side environment variable in each runtime.
cURL
curl -G 'https://api.apiflash.com/v1/urltoimage' \
--data-urlencode "access_key=$APIFLASH_ACCESS_KEY" \
--data-urlencode 'url=https://example.com' \
--data-urlencode 'full_page=true' \
--data-urlencode 'format=png' \
--output page.png
Check the HTTP status when scripting around cURL so an API error body is not mistaken for a PNG; for example, add --fail-with-body on cURL versions that support it.
Python
import os
import requests
response = requests.get(
'https://api.apiflash.com/v1/urltoimage',
params={
'access_key': os.environ['APIFLASH_ACCESS_KEY'],
'url': 'https://example.com',
'full_page': 'true',
'format': 'png',
},
timeout=90,
)
response.raise_for_status()
with open('page.png', 'wb') as image_file:
image_file.write(response.content)
Node.js
const params = new URLSearchParams({
access_key: process.env.APIFLASH_ACCESS_KEY,
url: 'https://example.com',
full_page: 'true',
format: 'png',
});
const response = await fetch(
`https://api.apiflash.com/v1/urltoimage?${params}`,
{ signal: AbortSignal.timeout(90_000) }
);
if (!response.ok) {
const detail = await response.text();
throw new Error(`ApiFlash returned HTTP ${response.status}: ${detail}`);
}
const image = Buffer.from(await response.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('page.png', image)
);
7. Troubleshooting
| Symptom or status | Likely cause | What to do |
|---|---|---|
| 400 Bad Request | An invalid parameter or a target that ApiFlash cannot capture. | Check parameter names and values, use a complete URL with https:// or http://, and confirm the target is reachable. |
| 401 Unauthorized | The key is invalid or revoked. | Check the server-side key configuration and account credentials. Never paste the key into a public URL or client-side code. |
| 402 Payment Required | The monthly quota is exhausted. | Review account usage and plan limits before retrying. |
| 403 Forbidden | A requested feature is unsupported by the current plan. | Check plan eligibility for the feature or remove that parameter. |
| 429 Too Many Requests | The request rate exceeded the documented limit. | Reduce concurrency, queue work, and retry with backoff. The documented leaky-bucket rate is 20 requests per second with a burst size of 400. |
| 500 or capture failure | The capture failed at the service or target page. | Check the target and readiness settings. Avoid immediately repeating identical failures: ApiFlash documents a limit of five failed captures per hour for exactly the same parameters. |
| Image file contains an error message | The code saved an unsuccessful response body as if it were image data. | Check the HTTP status before saving. Log a limited, sanitized error detail for diagnosis. |
| Page is blank or missing content | The target rendered content after the capture condition, or requires scrolling. | Try an appropriate wait_until, a meaningful wait_for selector, or scroll_page=true for lazy-loaded content. |
| Selector wait fails | The CSS selector did not match within the documented 15-second wait. | Verify the selector against the target page and choose a stable element that appears reliably. |
| Wrong format or dimensions | Full-page mode was combined with non-PNG assumptions or sizing parameters. | Request PNG and remember that height and thumbnail_width are ignored for full-page captures. |
8. Performance, caching, and reliability
Full-page captures can take longer and produce larger files than viewport screenshots, particularly for long pages and pages with many images. Set a client timeout that fits your request path and avoid tying up a short web request for bulk work. For larger workloads, queue capture jobs and persist their result or error status so callers can retrieve completion asynchronously.
ApiFlash documents ttl as a cache duration in seconds, defaulting to 86,400 seconds and configurable up to 2,592,000 seconds. Repeated identical parameter sets can use the cached screenshot. The FAQ says successful cached screenshots and failed screenshots do not count against monthly quota. Choose a TTL based on how fresh the target content must be; cached output may not reflect recent changes.
ApiFlash describes creating and destroying an isolated Chrome instance for each screenshot and says cached screenshots cannot be accessed without the API key. Treat those as vendor statements, not an independent security guarantee. Continue to protect credentials, limit access to stored screenshots, and decide whether sending a target URL to a third-party capture service is appropriate for your data.
For quota and request behavior, monitor API responses and account usage. The documented limit is 20 requests per second with a burst size of 400; excess requests may be delayed and requests beyond the burst can receive 429. A bounded queue and backoff help smooth spikes without creating retry storms.
9. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. Its one-call API can return an image or PDF, and the parameter names used by other screenshot APIs also work, which can make switching straightforward. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can ApiFlash capture a full page as JPEG?
ApiFlash documents full-page capture as PNG-only. Use PNG for this mode.
Can I return the image directly from a Laravel route?
Yes. After checking that the API response succeeded, return the response body with an image content type, or store it and return a controlled download response. Avoid returning an upstream error body with an image content type.
Does full_page=true wait for every lazy image?
Full-page mode sets the capture extent. For lazy-loaded content, ApiFlash documents scroll_page=true to scroll through the page before capture.
Does a cached capture use quota?
ApiFlash’s FAQ says successful cached screenshots and failed screenshots do not count against monthly quota. Check the current account terms for the rules that apply to your plan.


