WordPress Screenshot API — Capture Any Website from WordPress
Most WordPress screenshot plugins rely on the same free service under the hood: WordPress.com's mShots API. It captures a 1280x960 viewport, returns a JPEG, and calls it a day. No full-page capture, no cookie banner blocking, no format choice, no retina support. The first request doesn't even return an image. You get a placeholder and have to poll until the screenshot is ready.
That works for a personal blog showing three link previews. It stops working the moment you're building a directory with 500 listings, running an agency portfolio, or need screenshots that look sharp on a Retina display.
ScreenshotRun connects to WordPress through plain PHP. No plugin to install, no dependency to maintain. A shortcode, a few WordPress-native functions, and your site captures any URL in PNG, WebP, AVIF, PDF, or five other formats. Full-page rendering, cookie blocking, dark mode, mobile emulation, and 40+ other parameters are all built in.
A working shortcode in 15 lines of PHP
Drop this into your theme's functions.php or use the Code Snippets plugin if you'd rather not touch theme files directly.
function screenshotrun_shortcode( $atts ) {
$atts = shortcode_atts( [
'url' => '',
'width' => 1280,
'format' => 'png',
'full_page' => 'false',
], $atts );
if ( empty( $atts['url'] ) ) {
return '<!-- screenshot: missing url -->';
}
$api_url = add_query_arg( [
'url' => rawurlencode( $atts['url'] ),
'width' => (int) $atts['width'],
'format' => $atts['format'],
'full_page' => $atts['full_page'],
], 'https://api.screenshotrun.com/v1/screenshots/capture' );
$response = wp_remote_get( $api_url, [
'headers' => [ 'Authorization' => 'Bearer ' . SCREENSHOTRUN_API_KEY ],
'timeout' => 30,
] );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return '<!-- screenshot: capture failed -->';
}
$image_data = wp_remote_retrieve_body( $response );
$src = 'data:image/' . $atts['format'] . ';base64,' . base64_encode( $image_data );
return '<img src="' . esc_attr( $src ) . '" alt="Screenshot of ' . esc_attr( $atts['url'] ) . '" loading="lazy">';
}
add_shortcode( 'screenshot', 'screenshotrun_shortcode' );
Store your API key in wp-config.php as a constant:
define( 'SCREENSHOTRUN_API_KEY', 'sr_live_your_key_here' );
I've seen WordPress themes that store API keys in theme options, visible to any editor with dashboard access. Don't do that. The wp-config.php constant is the same pattern WordPress uses for database credentials and Stripe keys.
Now use it anywhere in posts or pages:
[screenshot url="https://stripe.com" width="1440" format="webp" full_page="true"]
That renders a full-page WebP screenshot of Stripe's homepage at 1440px width. Every API parameter works as a shortcode attribute: dark_mode="true", block_cookies="true", device="mobile", selector, custom dimensions, all of it.
Cache with transients so you're not burning API calls
The shortcode above calls the API on every page load. Fine for testing. For production, wrap it in WordPress's Transients API (a built-in key-value cache with expiration).
function screenshotrun_shortcode( $atts ) {
$atts = shortcode_atts( [
'url' => '',
'width' => 1280,
'format' => 'png',
'full_page' => 'false',
'cache' => DAY_IN_SECONDS,
], $atts );
if ( empty( $atts['url'] ) ) {
return '';
}
$cache_key = 'sr_' . md5( $atts['url'] . $atts['width'] . $atts['format'] . $atts['full_page'] );
$cached = get_transient( $cache_key );
if ( $cached !== false ) {
return $cached;
}
$api_url = add_query_arg( [
'url' => rawurlencode( $atts['url'] ),
'width' => (int) $atts['width'],
'format' => $atts['format'],
'full_page' => $atts['full_page'],
], 'https://api.screenshotrun.com/v1/screenshots/capture' );
$response = wp_remote_get( $api_url, [
'headers' => [ 'Authorization' => 'Bearer ' . SCREENSHOTRUN_API_KEY ],
'timeout' => 30,
] );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return '<!-- screenshot: capture failed -->';
}
$body = wp_remote_retrieve_body( $response );
$src = 'data:image/' . $atts['format'] . ';base64,' . base64_encode( $body );
$html = '<img src="' . esc_attr( $src ) . '" alt="Screenshot of ' . esc_attr( $atts['url'] ) . '" loading="lazy">';
set_transient( $cache_key, $html, (int) $atts['cache'] );
return $html;
}
I prefer MD5 for cache keys over slug-based keys because URL query parameters make slugs unreliable. Change the URL or switch from PNG to WebP? Different hash, fresh capture. The default TTL is one day, but you can override it per shortcode: [screenshot url="..." cache="3600"] refreshes hourly.
After the first visitor triggers the capture, every subsequent page load serves the cached HTML string. Zero API calls, zero latency. The transient expires, the next visitor triggers a fresh capture, and the cycle repeats.
One caveat worth knowing: if your site gets a traffic spike right after the cache expires, multiple visitors could trigger the same API call before the first one finishes and writes the new transient. This bit me on a directory site with 10K monthly visitors. For high-traffic sites, use the WP-Cron approach below to refresh screenshots in the background instead of on visitor request.
Save screenshots to your WordPress Media Library
Base64 inline images work, but they bloat your HTML and can't be reused across pages. For directories, portfolios, or any site that displays the same screenshot in multiple places, save it to the Media Library instead.
function screenshotrun_save_to_media( $url, $params = [] ) {
$defaults = [ 'format' => 'png', 'width' => 1280, 'full_page' => 'false' ];
$params = wp_parse_args( $params, $defaults );
$api_url = add_query_arg(
array_merge( [ 'url' => rawurlencode( $url ) ], $params ),
'https://api.screenshotrun.com/v1/screenshots/capture'
);
$response = wp_remote_get( $api_url, [
'headers' => [ 'Authorization' => 'Bearer ' . SCREENSHOTRUN_API_KEY ],
'timeout' => 60,
] );
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
return false;
}
$image_data = wp_remote_retrieve_body( $response );
$filename = 'screenshot-' . sanitize_title( wp_parse_url( $url, PHP_URL_HOST ) ) . '.' . $params['format'];
$upload = wp_upload_bits( $filename, null, $image_data );
if ( $upload['error'] ) {
return false;
}
$attachment_id = wp_insert_attachment( [
'post_mime_type' => 'image/' . $params['format'],
'post_title' => 'Screenshot of ' . $url,
'post_status' => 'inherit',
], $upload['file'] );
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
return $attachment_id;
}
This function returns an attachment ID. Use wp_get_attachment_url( $attachment_id ) to get the image URL, or wp_get_attachment_image( $attachment_id, 'large' ) for a responsive <img> tag with srcset. The screenshot sits in your Media Library like any other uploaded image. Searchable, deletable, usable as a featured image.
Directory and portfolio sites benefit most from this approach. Each listing gets its own media attachment. WordPress handles thumbnails, responsive sizes, and CDN delivery (if you're using one) automatically.
Refresh screenshots on a schedule with WP-Cron
Websites change. A portfolio screenshot from three months ago doesn't show the client's latest redesign. A directory thumbnail still showing the old branding looks neglected. WP-Cron lets you refresh screenshots in the background without any visitor waiting for the capture.
// Register the scheduled event on theme activation
function screenshotrun_schedule_refresh() {
if ( ! wp_next_scheduled( 'screenshotrun_refresh_screenshots' ) ) {
wp_schedule_event( time(), 'weekly', 'screenshotrun_refresh_screenshots' );
}
}
add_action( 'after_setup_theme', 'screenshotrun_schedule_refresh' );
// The refresh callback
function screenshotrun_do_refresh() {
$posts = get_posts( [
'post_type' => 'listing', // your CPT
'meta_key' => 'website_url', // ACF or custom field
'numberposts' => -1,
] );
foreach ( $posts as $post ) {
$url = get_post_meta( $post->ID, 'website_url', true );
if ( empty( $url ) ) continue;
$attachment_id = screenshotrun_save_to_media( $url, [
'width' => 1280,
'format' => 'webp',
'full_page' => 'false',
'block_cookies' => 'true',
] );
if ( $attachment_id ) {
set_post_thumbnail( $post->ID, $attachment_id );
}
}
}
add_action( 'screenshotrun_refresh_screenshots', 'screenshotrun_do_refresh' );
This iterates your "listing" custom post type every week, captures a fresh screenshot of each listing's URL, saves it to the Media Library, and sets it as the featured image. Swap 'listing' for 'portfolio', 'project', or whatever custom post type fits your setup.
WP-Cron doesn't run on a true system clock. It fires when someone visits your site. If nobody visits at 3 AM on Sunday, the refresh won't trigger until the first Monday morning visitor. For reliable timing, add a system cron entry (wget -q -O - https://yoursite.com/wp-cron.php) and set define( 'DISABLE_WP_CRON', true ) in wp-config.php. Most managed WordPress hosts (Kinsta, WP Engine, Cloudways) handle this out of the box.
On a client directory with about 800 listings, I batch 50 per cron run and it completes in around 3 minutes. For directories with 500+ listings, use a meta_query to track which posts were last refreshed and pick the stalest ones first. That keeps each run well within PHP's max_execution_time and the API rate limit.
mShots vs ScreenshotRun: what you actually get
Every free WordPress screenshot plugin uses WordPress.com's mShots service. The table below shows what that means in practice versus a dedicated screenshot API.
| Capability | mShots (free) | ScreenshotRun |
|---|---|---|
| Max viewport | 1280 x 960 | Any size up to 1920 x 10000+ |
| Full-page capture | No | Yes |
| Output format | JPEG only | PNG, JPEG, WebP, AVIF, TIFF, PDF |
| Retina (2x) | No | Yes |
| Dark mode | No | Yes |
| Block cookie banners | No | Yes |
| Block ads / chat widgets | No | Yes |
| Mobile / tablet emulation | No | Yes (desktop, mobile, tablet) |
| Custom CSS / JS injection | No | Yes |
| Wait for selector | No | Yes |
| First request behavior | Returns placeholder (HTTP 307) | Returns screenshot immediately |
| SLA / uptime guarantee | None (best-effort) | Yes |
| Authentication | None (public, rate-limited) | API key via Authorization header |
| Commercial use | Requires Automattic license | Included in all plans |
| Free tier | Unlimited (with limits above) | 100 screenshots/month |
If your WordPress site shows three or four link preview thumbnails and pixel quality doesn't matter much, mShots handles the job. The moment you need full-page captures, modern image formats, or clean screenshots without GDPR consent overlays covering half the viewport, you've outgrown it. Honestly, the mShots approach still feels stuck around 2015.
Where WordPress sites use screenshot APIs
Web directories and listing sites get the most out of automated screenshots. The WP-Cron pattern above auto-generates a thumbnail for every listed URL. A directory with 1,000 listings captures all thumbnails for about $7 on the Starter plan, instead of 40+ hours of manual work in Chrome and Photoshop.
Agency portfolios are the second big use case. Fresh screenshots of client sites, updated weekly, set as featured images on portfolio posts. No more quarterly screenshot sessions where someone opens 30 tabs and hits Cmd+Shift+4 for an hour.
Link preview cards for bloggers and resource sites work the same way. Auto-generate visual previews for external links, similar to how Notion and Slack render URL previews, but hosted on your own WordPress media library.
OG image generation is another natural fit. Capture your own pages at 1200x630 and use the result as og:image. Social shares always show the current page design, not a stale graphic from last quarter. Works well for WooCommerce product pages that change pricing or imagery frequently.
For competitor monitoring, WordPress makes a decent lightweight dashboard. Custom post type with URLs, daily WP-Cron captures, visual change tracking over time. Pair it with batch captures and thumbnail generation for review sites that embed product page screenshots alongside written reviews.
API parameters for WordPress developers
Pass any of these as shortcode attributes or as query parameters in the add_query_arg() call. The full feature set maps directly to these. For the complete integrations reference, including raw PHP examples without the WordPress wrapper, see the integration hub.
| Parameter | Type | Default | What it does |
|---|---|---|---|
url | string | — | Page to capture. Required |
format | string | png | png, jpeg, webp, avif, or pdf |
width | integer | 1280 | Viewport width in pixels |
height | integer | 720 | Viewport height in pixels |
full_page | boolean | false | Capture the entire scrollable page |
device | string | desktop | desktop, mobile, or tablet emulation |
retina | boolean | false | 2x pixel density (file size quadruples, not doubles) |
dark_mode | boolean | false | Force dark color scheme |
block_cookies | boolean | false | Strip GDPR consent popups |
block_ads | boolean | false | Remove ad banners and tracking scripts |
block_chats | boolean | false | Hide live chat overlays |
delay | integer | 0 | Wait N ms before capturing. Useful for JS-heavy pages |
selector | string | — | Crop to a CSS element |
css | string | — | Inject custom CSS before capture |
stealth | boolean | false | Bypass bot detection on protected sites |
cache_ttl | integer | — | Server-side cache in seconds (complements WP transients) |
webhook_url | string | — | Async delivery for heavy pages that exceed PHP timeout |
Heads up: retina screenshots at 2x produce images that are 4x the file size, not 2x, because both width and height double. For directory thumbnails displayed at 300px wide, you probably don't need retina. For portfolio hero sections on Retina MacBooks, it makes a visible difference. The first time I saw the file size jump from a 180KB PNG to 720KB, I assumed something had broken in the upload.
Add screenshots to your WordPress site
The free tier covers 100 screenshots per month. Enough to test every code example on this page and wire up your first shortcode. Copy the snippet into functions.php, pick up an API key, and your first WordPress screenshot can be live before your coffee gets cold.
Get Your Free API Key