Features Full Page Screenshot Wait for Selector & Delay Block Cookie Banners Custom Viewport & Device Website to PDF HTML to Image Dark Mode Image Format & Quality MCP Server Webhook Pricing Docs Blog Log In Sign Up

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.

CapabilitymShots (free)ScreenshotRun
Max viewport1280 x 960Any size up to 1920 x 10000+
Full-page captureNoYes
Output formatJPEG onlyPNG, JPEG, WebP, AVIF, TIFF, PDF
Retina (2x)NoYes
Dark modeNoYes
Block cookie bannersNoYes
Block ads / chat widgetsNoYes
Mobile / tablet emulationNoYes (desktop, mobile, tablet)
Custom CSS / JS injectionNoYes
Wait for selectorNoYes
First request behaviorReturns placeholder (HTTP 307)Returns screenshot immediately
SLA / uptime guaranteeNone (best-effort)Yes
AuthenticationNone (public, rate-limited)API key via Authorization header
Commercial useRequires Automattic licenseIncluded in all plans
Free tierUnlimited (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.

ParameterTypeDefaultWhat it does
urlstring—Page to capture. Required
formatstringpngpng, jpeg, webp, avif, or pdf
widthinteger1280Viewport width in pixels
heightinteger720Viewport height in pixels
full_pagebooleanfalseCapture the entire scrollable page
devicestringdesktopdesktop, mobile, or tablet emulation
retinabooleanfalse2x pixel density (file size quadruples, not doubles)
dark_modebooleanfalseForce dark color scheme
block_cookiesbooleanfalseStrip GDPR consent popups
block_adsbooleanfalseRemove ad banners and tracking scripts
block_chatsbooleanfalseHide live chat overlays
delayinteger0Wait N ms before capturing. Useful for JS-heavy pages
selectorstring—Crop to a CSS element
cssstring—Inject custom CSS before capture
stealthbooleanfalseBypass bot detection on protected sites
cache_ttlinteger—Server-side cache in seconds (complements WP transients)
webhook_urlstring—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

Frequently asked questions

No. The integration runs through a PHP shortcode you add to functions.php or the Code Snippets plugin. There is no plugin to install, update, or worry about breaking during WordPress core updates. Every WordPress function used (wp_remote_get, shortcode_atts, set_transient) is part of WordPress core and has been stable for over a decade.
Not if you cache properly. The Transients API caches each screenshot after the first capture. Subsequent page loads serve the cached version with zero API calls and zero added latency. For high-traffic sites, use WP-Cron to refresh screenshots in the background so no visitor ever waits for a capture.
mShots captures a 1280x960 JPEG viewport with no options. ScreenshotRun supports full-page capture, 6 image formats (PNG, WebP, AVIF, JPEG, TIFF, PDF), retina rendering, cookie banner blocking, dark mode, mobile emulation, custom CSS injection, and 40+ other parameters. mShots also returns a placeholder on first request and requires polling. ScreenshotRun returns the screenshot immediately.
Yes. The screenshotrun_save_to_media() function in the guide above downloads the screenshot binary and saves it via wp_upload_bits and wp_insert_attachment. The screenshot appears in your Media Library as a regular attachment, complete with responsive sizes, and you can use it as a featured image or embed it anywhere.
Add it as a constant in wp-config.php: define( SCREENSHOTRUN_API_KEY, sr_live_... ). This keeps the key out of the database and invisible to editors or contributors who have dashboard access. It is the same approach WordPress uses for database credentials and payment gateway keys.
The free tier includes 100 screenshots per month. With transient caching enabled, each unique URL only uses one API credit regardless of how many page views it gets. A directory with 50 listings would use 50 credits on first capture, then zero until the cache expires. Paid plans start at $9/month for 5,000 captures.