You found a URL like https://s.wordpress.com/mshots/v1/https%3A%2F%2Fwordpress.org%2F?w=400 in a theme, a link-card plugin or an old Stack Overflow answer. Put it in an <img> tag and a screenshot of the page appears: no account, no key, no bill. That is mShots, the screenshot service behind the site thumbnails on WordPress.com.
Then you look for its documentation and find none: no API reference, no parameter list, no terms page, and the first request returns a "Generating Preview" image instead of a screenshot.
This guide works from mShots' open-source code and our own requests to it on 8 November 2026, and, where mShots is not enough, makes the same thumbnail on your own server with a documented API.
Disclosure: this guide is published by Site-Shot, a screenshot API, and it compares mShots with ours. We have kept the facts accurate and the trade-offs honest, including where mShots is the better fit. mShots was checked against its source code and the live service, and our prices on our pricing page, in November 2026.
- What mShots is, and who runs it
- The mShots URL format and its parameters
- What the first request returns: the "Generating Preview" placeholder
- How to use mShots well
- One thumbnail, no code: the free browser tool
- When a documented API is the better fit
- Your own thumbnails with your own key
- Where the honest limits are
- FAQ
What mShots is, and who runs it
mShots is an open-source project from Automattic, the company behind WordPress.com, published under GPL-2.0 at github.com/Automattic/mShots. It loads a page in headless Chrome and returns an image for the site thumbnails WordPress.com and WordPress.org show. Its README says: "The Automattic Systems Team manages the live deployment of mShots." It is maintained: in October 2026 it gained a color_scheme parameter for dark-mode screenshots.
What Automattic has not published is API terms. The README states none, the WordPress.com terms of service do not mention mShots (checked November 2026), and there is no documented rate limit or support channel. The statements that exist are second-hand:
- In 2010 Ben Gillbanks quoted Matt Mullenweg on mShots: "You can use it and link to it, but it's not official."
- In 2018 a comment attributed to Mullenweg, under Terence Eden's post about the mShots API, asked commercial users to "get in touch with Automattic about a license."
- The Browser Shots plugin page says "This function is free for non-commercial use." That is the plugin author's statement, not Automattic's.
Pz-LinkCard (20,000+ active installations in November 2026) uses mShots for link-card thumbnails by default, and Link Library offers it as an option.
The mShots URL format and its parameters
https://s.wordpress.com/mshots/v1/{URL-encoded page address}?w=400
Encode the whole target address, query string included: rawurlencode() in PHP, encodeURIComponent() in JavaScript. An <img> tag is the whole integration:
<img src="https://s.wordpress.com/mshots/v1/https%3A%2F%2Fwordpress.org%2F?w=400"
width="400" height="300" alt="wordpress.org home page" loading="lazy">
The parameters below are read from public_html/class-mshots.php in the repository and were checked with live requests on 8 November 2026:
| Parameter | What the code does | What we measured |
|---|---|---|
w |
Output width; the capture is scaled down to it, never up, at most 3072 px | w=400 gave 400 × 300; w=3000 still gave 1280 × 960 |
h |
Output height; with a small w it crops instead of scaling (centred horizontally, kept from the top) |
w=400&h=1000 gave 400 × 960 with the text clipped at both sides |
vpw, vph |
Browser viewport, 320 to 3072 px each, 1280 × 960 by default | vpw=1920&vph=1080&w=1920&h=1080 gave 1920 × 1080 |
requeue=true |
Capture the page again now instead of serving the stored snapshot | sent with Cache-Control: no-cache and a fresh Last-Modified |
format |
jpeg by default, or png |
format=png gave a 400 × 300 PNG, stored as its own snapshot, so its first request got the placeholder too |
The code also reads screen_width and screen_height (up to 1600 × 3600 px), scale and color_scheme, which we did not test. There is no WebP, no full page beyond 3600 px, and no option for a delay, cookie banners, a country or the user agent: the capture waits until the network is nearly idle, plus two seconds.
What the first request returns: the "Generating Preview" placeholder
mShots does not render while you wait. The first request for a page it has not captured answers 307 Temporary Redirect to https://s0.wp.com/mshots/v1/default, a 400 × 300 GIF with the WordPress logo and "Generating Preview...", and queues the capture. (If mShots already has the site's home page, the code redirects to that snapshot instead.) The redirect is marked no-cache, so the browser asks again on the next view.
Measured from a server on 8 November 2026, for a page mShots had never seen: the request at 13:30:06 UTC got the 307, and the same URL at 13:30:21 got a 400 × 300 JPEG of 33 KB with Last-Modified 13:30:11, five seconds after the first request. From then on it is cached: the HTTP answer says max-age=43200 (12 hours), while the code treats a snapshot as fresh for 24 hours. After that the next request still gets the old image and queues a new capture; nothing is re-captured on a timer.
The rest are observations, not rules. From two networks, requests with WordPress's default user agent (WordPress/7.1.4; https://example.com, what wp_remote_get() sends) got 429 Too Many Requests whenever the image was not already cached; with a user agent naming a site, none were refused. Some earlier requests got 403 Forbidden whatever the user agent. Automattic publishes no policy, so send your own user agent and treat 403 and 429 as "try later".
How to use mShots well
Warm the cache before anyone sees the placeholder
When you add a link, request its mShots URL from your server until the answer is an image. In WordPress, run this from a background job (wp_schedule_single_event()), not a page request, because it sleeps:
function mshots_url($target, $width = 400) {
return 'https://s.wordpress.com/mshots/v1/' . rawurlencode($target) . '?w=' . (int) $width;
}
// Ask once, wait, ask again: true once mShots answers with the real JPEG.
function mshots_warm($target, $width = 400, $tries = 3) {
for ($i = 1; $i <= $tries; $i++) {
$response = wp_remote_get(mshots_url($target, $width), [
'redirection' => 0, // a 307 here means the placeholder
'timeout' => 20,
// WordPress's own user agent got 429 from mShots in our tests: name your site.
'user-agent' => 'mysite-thumbnails/1.0 (+' . home_url('/') . ')',
]);
$code = wp_remote_retrieve_response_code($response);
$type = implode(',', (array) wp_remote_retrieve_header($response, 'content-type'));
if ($code === 200 && strpos($type, 'image/') === 0) {
return true;
}
if ($i < $tries) {
sleep(15);
}
}
return false;
}
Once mshots_warm() returns true, the snapshot exists and visitors get the screenshot. On our WordPress test site on 8 November 2026 it returned true after 16 seconds: a 307, then the JPEG. Without the user-agent line, all three tries got 429 and it returned false.
The same goes for social cards. SEO Ultimate's changelog records that in 2012 it stopped falling back to mShots for Open Graph images because this "was causing 'Generating Image' placeholders to be cached by Facebook". Never put a cold mShots URL in og:image. And if search matters to you: wordpress.com, s.wordpress.com and s0.wp.com all carry Disallow: /mshots/v1/ in robots.txt, so a thumbnail you want indexed has to be served from a host search engines may crawl, such as your own domain.
Size it with w, not h
Let the height follow the viewport: the default 1280 × 960 gives a 4:3 thumbnail, so w=400 comes back 400 × 300, numbers you can put in the <img> tag to avoid layout shift. For another shape, change vpw and vph; h crops.
Refresh with requeue, sparingly
Send requeue=true from your server once, when the page changed, never in the src visitors load: each such request starts a new capture and is not cached.
One thumbnail, no code: the free browser tool
For a single thumbnail in a portfolio entry or a post, you need neither mShots nor an API:
- Open site-shot.com and paste the page's address. There is no signup.
- Under Browser options, set the width to 1280 and the height to 960, the window an mShots thumbnail shows; under Image options, set the format to JPEG.
- Press Shot, download the 1280 × 960 image, and upload it to your Media Library, which makes the smaller sizes from it.
When a documented API is the better fit
For the thumbnail job, here is mShots next to Site-Shot, the API we publish:
| mShots | Site-Shot API | |
|---|---|---|
| Price | Free; no published terms | Free plan: 100 captures/mo (no card); paid plans from $5/mo for 2,000 |
| Key and account | None | API key from a confirmed account |
| Documentation and terms | None; the source code | API docs, pricing and terms of use |
| How you embed it | The URL goes straight into <img> |
Capture on your server, store the file, serve your copy |
| First request | 307 to a placeholder, image a few seconds later | Waits for the render and returns the image |
| Failure | Not documented | An HTTP status for most failures: 404 with an error-card image, 401, 403 or 429 |
| Output | JPEG by default, or PNG with format=png |
PNG, JPEG or WebP |
| Full page | No; at most 3600 px tall | full_size=1, up to 20,000 px (WebP stops at 16,383) |
| Country | None | country=DE&strict_country=1, any paid plan |
| Cookie banners, ads | No option | no_cookie_popup=1, no_ads=1 |
| Freshness | Fresh 24 hours, then refreshed lazily, on the next request | Captured when your code asks |
The short version: keep mShots for a few thumbnails where an occasional placeholder is fine and its unwritten terms suit you. Move to an API with stated commercial terms when the thumbnails are part of something you sell, or when you need full pages, WebP, a country, or a status code you can act on.
What you give up is real. mShots is free, keyless, and one URL in an <img> tag with nothing to store. Site-Shot needs a key, and a key cannot go into a public src: anyone could copy it and spend your captures, and every page view would start a render that takes seconds. So it is not a drop-in replacement: you need a server that captures, storage, and a refresh job.
Your own thumbnails with your own key
For a link directory or a portfolio that should stay current, get a key from /start/; the free plan needs no card, and paid plans are on the pricing page.
The match for an mShots w=400 thumbnail is width=1280&height=960&scaled_width=400&format=jpeg: the same window, scaled to 400 px. Choose JPEG: Site-Shot's PNG and WebP are lossless, and a photo-heavy page usually makes a smaller JPEG.
Plain PHP from cron
This runs from cron, writes each thumbnail into a folder your web server serves, and keeps the previous file when the API reports a failure. Keep the script itself outside the web root, so no visitor can start your captures:
<?php
// thumbs.php: kept outside the web root, run from cron with SITESHOT_API_KEY in its
// environment, for example every Monday at 04:00: 0 4 * * 1 php /opt/thumbs/thumbs.php
if (PHP_SAPI !== 'cli') {
exit(1); // a command-line job, never a web page
}
$sites = [
'wordpress-org' => 'https://wordpress.org/',
// 'another-site' => 'https://another-site.example/',
];
$dir = '/var/www/example/public/thumbs'; // served by your web server as /thumbs/
$failed = false;
foreach ($sites as $name => $url) {
$query = http_build_query([
'url' => $url,
'userkey' => getenv('SITESHOT_API_KEY'),
'width' => 1280,
'height' => 960,
'scaled_width' => 400,
'format' => 'jpeg',
]);
$ch = curl_init("https://api.site-shot.com/?{$query}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 70);
$image = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 200 || !$image) {
// 404: the page could not be captured (the body is an error card).
// 403: this month's captures are used up. 429: a parallel request on the free plan.
$why = $status ? "HTTP {$status}" : curl_error($ch);
error_log("thumbs: {$url} failed ({$why}); keeping the old file");
$failed = true;
continue;
}
$tmp = "{$dir}/{$name}.jpg.tmp";
if (file_put_contents($tmp, $image) !== strlen($image)
|| !rename($tmp, "{$dir}/{$name}.jpg")) { // swap in one step
error_log("thumbs: could not write {$dir}/{$name}.jpg; keeping the old file");
$failed = true;
}
}
exit($failed ? 1 : 0); // 1 when any site failed
A failed capture still has a body, so the status decides. Besides the cases in the comments, 401 means the key was not accepted and 403 can also mean the key has no active plan; 429 is why the script captures one site at a time. Most failures arrive this way, though a 200 is not a guarantee of a real page. Renaming a completely written temporary file means nobody gets a half-written image.
When we ran it on 8 November 2026, wordpress.org came back as a 400 × 300 JPEG of about 30 KB in about 6 seconds; that is one page, and yours will differ. Your pages then point at /thumbs/wordpress-org.jpg like any static file.
Budget the refresh against your plan. Every capture counts, failed ones included, so the free plan covers up to 20 sites refreshed weekly (a month can hold five weekly runs), or 100 refreshed monthly.
WordPress: WP-Cron and the Media Library
In WordPress, reuse the site_shot_capture() helper from our PHP screenshot guide: it reads the key from a SITE_SHOT_API_KEY constant in wp-config.php and returns false for any status but 200. Around it: a weekly event, an attachment per site, and the old attachment deleted only after the new one is in:
// Uses site_shot_capture() from the PHP guide; SITE_SHOT_API_KEY lives in wp-config.php.
add_action('init', function () {
if (!wp_next_scheduled('mysite_refresh_thumbs')) {
wp_schedule_event(time(), 'weekly', 'mysite_refresh_thumbs');
}
});
add_action('mysite_refresh_thumbs', function () {
require_once ABSPATH . 'wp-admin/includes/image.php';
$sites = ['https://wordpress.org/']; // the links you show thumbnails for
$thumbs = get_option('mysite_thumbs', []); // url => attachment ID
foreach ($sites as $url) {
$image = site_shot_capture($url, [
'width' => 1280,
'height' => 960,
'scaled_width' => 400,
'format' => 'jpeg',
]);
if (!$image) {
continue; // any status but 200: keep the thumbnail you already have
}
$host = wp_parse_url($url, PHP_URL_HOST);
$upload = wp_upload_bits(sanitize_title($host) . '.jpg', null, $image);
if (!empty($upload['error'])) {
continue;
}
$id = wp_insert_attachment([
'post_mime_type' => 'image/jpeg',
'post_title' => $url,
'post_status' => 'inherit',
], $upload['file']);
if (!$id) {
wp_delete_file($upload['file']); // the insert failed: keep the old thumbnail
continue;
}
wp_update_attachment_metadata($id, wp_generate_attachment_metadata($id, $upload['file']));
update_post_meta($id, '_wp_attachment_image_alt', "Screenshot of {$host}");
$old = $thumbs[$url] ?? 0;
$thumbs[$url] = $id;
update_option('mysite_thumbs', $thumbs); // saved first, so it never names a deleted ID
if ($old) {
wp_delete_attachment($old, true); // the old copy goes once the new one is in
}
}
});
// In a template:
// $thumbs = get_option('mysite_thumbs', []);
// echo wp_get_attachment_image($thumbs['https://wordpress.org/'] ?? 0, 'full');
On a WordPress 7.1 test site on 8 November 2026, with the API's answers simulated: a 200 made a 400 × 300 attachment with its alt text, a 404 left it in place, a failed insert and a run cut off midway each left a working thumbnail, and a second 200 replaced it and deleted the old one with its files.
The weekly recurrence is built into WordPress since 5.4. But WP-Cron, in the developer handbook's words, "is only triggered on page load", so on a quiet site a job runs late; the handbook's fix is define( 'DISABLE_WP_CRON', true ); plus a system cron that requests wp-cron.php.
Where the honest limits are
- The free plan has bounds. 100 captures a month, failed ones included, one at a time, from the default location or the US, and no storage on our side: the files live on your server.
- Your copy is only as fresh as your schedule. With a weekly job, a thumbnail can be a week old.
- Our mShots facts are a snapshot, from the repository in November 2026 and requests on one day from two networks. Automattic can change the live service without notice.
For a full-page capture instead of a thumbnail, see full page vs viewport screenshots; to capture every page of a site in one run, see screenshot every page of a website.
FAQ
What is the WordPress mShots screenshot API URL?
The WordPress.com mShots URL is https://s.wordpress.com/mshots/v1/ followed by the URL-encoded page address, for example https://s.wordpress.com/mshots/v1/https%3A%2F%2Fwordpress.org%2F?w=400. It needs no key. The w parameter sets the output width, vpw and vph set the browser viewport (1280 × 960 by default), and requeue=true forces a fresh capture. Automattic publishes no documentation; these parameters come from its open-source code.
Why does mShots show "Generating Preview" instead of a screenshot?
The first request for a page mShots has not captured yet answers with a 307 redirect to a 400 × 300 placeholder GIF that says Generating Preview, and queues the capture. Request the same URL again a few seconds later and you get the JPEG; in our November 2026 test the image was ready five seconds after the first request.
Can I use mShots for commercial use?
Automattic publishes no terms of service for mShots. A 2018 blog comment attributed to Matt Mullenweg asks commercial users to "get in touch with Automattic about a license." The Browser Shots plugin describes mShots as free for non-commercial use. For a commercial site, ask Automattic, or use a screenshot API with published pricing and terms.
Is there an mShots alternative with published terms?
Site-Shot is a screenshot API with published pricing: a confirmed account's API key comes with the free plan: 100 API captures a month, no card needed; paid plans start at $5/mo for 2,000. Unlike mShots it needs a key, so you capture on your server and serve your own copy rather than put the API URL in an img tag. It adds full-page capture, WebP output, a choice of country on paid plans, and an HTTP error status for most failed captures.
Take a one-off thumbnail free in your browser, no signup, at site-shot.com, or compare API plans on the pricing page.