A quick disclaimer: I run ScreenshotOne, so I’m not neutral. I’ll stick to differences you can check in both services’ docs, and I won’t claim one renders every site better or faster. Test that on your own URLs.
Microlink and ScreenshotOne overlap on screenshots, but they start from different places. Microlink is a URL-to-data API: by default it returns metadata (title, description, images, and so on), and screenshots and PDFs are add-ons. ScreenshotOne is a rendering API: you get an image or a PDF, with lots of control over how the page is loaded, and a few metadata fields if you ask for them.
So if your app mainly uses Microlink for screenshots, moving is fairly simple. If it relies on Microlink’s metadata and embeds, you might move only the screenshots and keep Microlink for the rest. That’s a perfectly reasonable setup.
What to check before you switch
| If you need to… | Compare |
|---|---|
| Save a PNG when a job finishes | Raw image bytes vs. JSON with an image URL you then download |
| Build link cards | Exactly which metadata fields your code reads, and what happens when they’re missing |
| Capture JavaScript-heavy pages | Waiting, scrolling, viewport, session, and blocking options, tested on your real URLs |
| Keep an archive | Where the file is stored long-term and who controls retention |
| Process a queue | Concurrency, rate limits, retries, timeouts, and cost at your volume |
One successful request against a simple landing page won’t tell you much. Use a handful of the pages that give you trouble today.
Option mapping
For a full-page PNG at a 1280 × 800 viewport:
| What you want | Microlink | ScreenshotOne |
|---|---|---|
| Page URL | url | url |
| Take a screenshot | screenshot=true | Call /take |
| Full page | screenshot.fullPage=true | full_page=true |
| PNG | screenshot.type=png | format=png |
| Viewport width | viewport.width=1280 | viewport_width=1280 |
| Viewport height | viewport.height=800 | viewport_height=800 |
| Pixel density | viewport.deviceScaleFactor=1 | device_scale_factor=1 |
References: Microlink’s fullPage, type, and viewport docs, and ScreenshotOne’s options reference.
Matching the settings makes the comparison fair, but don’t expect identical pixels. Browser versions, fonts, when each service decides the page is “ready,” and how each one scrolls will all differ a little.
One script, both providers
This Node.js 22+ script takes the same screenshot with either provider and saves a PNG, so you can compare the results side by side.
For Microlink, it uses the Pro endpoint (https://pro.microlink.io) with an x-api-key header. The free endpoint is https://api.microlink.io. Don’t send your Pro key there. See Microlink authentication.
Set MICROLINK_API_KEY and/or SCREENSHOTONE_ACCESS_KEY, and save this as capture.mjs:
import { writeFile } from "node:fs/promises";
const [provider, inputUrl, outputPath = `${provider}.png`] = process.argv.slice(2);if (!["microlink", "screenshotone"].includes(provider) || !inputUrl) { throw new Error("Usage: node capture.mjs microlink|screenshotone URL [output.png]");}const target = new URL(inputUrl);
async function checkedFetch(url, options = {}) { const response = await fetch(url, { ...options, signal: AbortSignal.timeout(120_000), }); if (!response.ok) throw new Error(`Request failed: HTTP ${response.status}`); return response;}
async function captureWithMicrolink() { const key = process.env.MICROLINK_API_KEY; if (!key) throw new Error("Missing MICROLINK_API_KEY."); const endpoint = new URL("https://pro.microlink.io"); endpoint.search = new URLSearchParams({ url: target.href, screenshot: "true", "screenshot.fullPage": "true", "screenshot.type": "png", "viewport.width": "1280", "viewport.height": "800", "viewport.deviceScaleFactor": "1", }).toString();
// Microlink returns JSON with an image URL, so this takes two requests. const response = await checkedFetch(endpoint, { headers: { "x-api-key": key } }); const result = await response.json(); const imageUrl = result.data?.screenshot?.url; if (result.status !== "success" || typeof imageUrl !== "string") { throw new Error("Microlink didn't return a screenshot."); } return checkedFetch(imageUrl);}
async function captureWithScreenshotOne() { const key = process.env.SCREENSHOTONE_ACCESS_KEY; if (!key) throw new Error("Missing SCREENSHOTONE_ACCESS_KEY."); // ScreenshotOne returns the image bytes directly. return checkedFetch("https://api.screenshotone.com/take", { method: "POST", headers: { "Content-Type": "application/json", "X-Access-Key": key }, body: JSON.stringify({ url: target.href, format: "png", full_page: true, viewport_width: 1280, viewport_height: 800, device_scale_factor: 1, }), });}
const imageResponse = provider === "microlink" ? await captureWithMicrolink() : await captureWithScreenshotOne();
const image = Buffer.from(await imageResponse.arrayBuffer());const pngHeader = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);if (!image.subarray(0, 8).equals(pngHeader)) { throw new Error("Didn't get a PNG back; nothing was written.");}await writeFile(outputPath, image);console.log(`Saved ${outputPath} (${image.length} bytes)`);Run it once per provider against the same URL:
node capture.mjs microlink 'https://example.com/' microlink.pngnode capture.mjs screenshotone 'https://example.com/' screenshotone.pngBoth requests count against your plans. Open the two files at full size and compare them, preferably on harder pages than example.com. This script isn’t a speed or price benchmark.
The response shape is the main code change
Microlink’s screenshot response is JSON, with the image URL in data.screenshot.url. ScreenshotOne’s default response is the image. Anywhere your code calls response.json() and reads data.screenshot.url will break after the switch.
If the rest of your app expects a URL, ScreenshotOne can return JSON too: set response_type=json and read screenshot_url. That URL is temporary and lasts up to four hours by default, so copy anything you want to keep into your own storage. Don’t store it as a permanent link.
Either way, I’d put provider-specific code behind a small adapter that returns something like { file, capturedAt, sourceUrl }. Then the switch happens in one file, and you can roll it out gradually.
Don’t lose your metadata
Before migrating, search your code for every field you read from Microlink’s response. A screenshot might be used in one component while the title, description, or logo is used in three others.
ScreenshotOne can return some metadata if you ask: metadata_page_title, metadata_icon, and metadata_open_graph (see metadata options). They’re opt-in, the response is structured differently, and they don’t cover everything Microlink extracts. If you rely on its embeds or its wider extraction, keep Microlink for that part.
Whatever you use, plan for missing titles and images, and keep the screenshot separate from the site’s own Open Graph image. They’re different things.
The Node.js screenshot and metadata guide provides a complete example that saves the image and handles missing titles, favicons, and Open Graph fields.
Test on real pages
Pick a small set that covers what usually breaks:
- A simple landing page, to check dimensions and basic output
- A long page with lazy-loaded images, to check the bottom of the capture
- A JavaScript app, to check when it decides the page is ready
- A logged-in page, if you pass sessions
- A page with cookie banners, chat widgets, or other overlays
Record successful captures, incomplete renders, latency, and retries. Compare like with like on caching: a cached response from one provider against a fresh render from the other isn’t a fair test.
Before moving a production queue, check ScreenshotOne’s plans and Microlink’s for your actual volume, including concurrency, caching, retries, storage, and any features you’d use. Per-request price is only part of the cost.
Then send a small share of traffic through the new adapter, review the output, and keep rollback simple until you’re happy. If you’re still choosing between vendors, here’s our roundup of screenshot APIs.


