Disclosure: I build RenderScreenshot. Microlink is a good API with a broader data focus than ours, and this page says when to keep it.
Microlink is a browser API for data about a URL (metadata, logos, palettes, Lighthouse, Markdown) that also takes screenshots. If screenshots are most of what you use it for, a dedicated screenshot API can be simpler: the image comes back in the first response, there are presets for common sizes, and the price fits low volumes better. This page shows what I measured on Microlink's free endpoint, one default that trips up most comparisons, a parameter map, prices at three volumes, and a drop-in function in Node, Python and Ruby. For the wider market, see the best screenshot API comparison.
Last verified: October 15, 2026, against Microlink's screenshot, embed, authentication, rate limit and cache docs, and its pricing page, as published on October 14 and 15, 2026.
The short answer
| Choose Microlink if | Choose RenderScreenshot if |
|---|---|
| You want metadata, logo, palette, Markdown or Lighthouse data with the screenshot | You want the image bytes in the first response, without a second download |
| You need more than 60,000 requests a month on a self-serve plan | You run 1,000 to 12,000 shots a month and want to pay for that, not for 46,000 |
| You prefer a pause at 100% quota to any chance of overage | You want presets (og_card, twitter_card_large, mobile, desktop_hd) |
| You want to test without signing up | You need signed URLs for public HTML, or a cache purge API |
| You like a free keyless endpoint for prototypes | You want SDKs in Python, Ruby, PHP, Java or Go |
What I measured on Microlink
Microlink is the one vendor in our comparison I could test live without an account, so here are real numbers. Treat them as a small sample, not a benchmark.
Method box. October 15, 2026. Client: Apple M1 Pro, Node.js 22.18.0, network in Toronto, Canada. Endpoint: api.microlink.io (keyless). Target: https://example.com/ with a unique rsbench query parameter per run, so the first request could not be served from cache. Request: 1280×800, PNG, waitUntil=load, embed=screenshot.url. "Cold" is the first request for that unique URL; "warm" is the same request repeated at once. Time is measured to the last byte of the image.
| Phase | Runs | Times | Success | Output |
|---|---|---|---|---|
| Cold | 4 | 1,135, 757, 1,181, 1,006 ms | 4 of 4 | 70,539 bytes, 1280×800 PNG |
| Warm | 4 | 99, 49, 45, 51 ms | 4 of 4 | 70,539 bytes |
The warm numbers show Microlink's edge cache working: once a URL is cached, the response came back from a nearby edge in under 100 ms. Its cache docs describe a unified cache plus per-location edge caches, and the response carried x-cache-status: MISS on the first call.
The default that changes every size comparison. Without viewport.deviceScaleFactor=1, the same request returned a 2560×1600 PNG of 168,630 bytes. Microlink renders at a device scale factor of 2 unless you say otherwise. If you compare Microlink with any API that defaults to 1x (RenderScreenshot, ScreenshotOne, ApiFlash), set the factor explicitly on both sides, or you are comparing different images.
Not measured yet: RenderScreenshot under the same conditions. That needs our paid benchmark run, and I will add the row with the raw CSV when it is done, whichever way it goes. You can produce both rows yourself with the script in the best screenshot API comparison:
node compare.mjs --providers microlink,renderscreenshot --urls urls.txt --runs 3 --out microlink-vs-rs.csv
Parameter map
Microlink uses dot notation in the query string (viewport.width, screenshot.type). RenderScreenshot uses nested JSON on POST and flat names on GET.
| Microlink | RenderScreenshot (POST body) |
Notes |
|---|---|---|
x-api-key header (pro) or no key (free) |
Authorization: Bearer rs_live_... |
Or api_key on GET, or a signed URL |
url |
url |
|
screenshot=true |
(always a screenshot) | |
embed=screenshot.url |
(default: image bytes) | |
screenshot.type (png, jpeg) |
output.format (png, jpeg, webp, avif, pdf, html) |
|
screenshot.quality |
output.quality |
|
screenshot.fullPage |
capture.mode: "full_page" |
|
screenshot.element |
capture.mode: "element", capture.selector |
|
screenshot.omitBackground |
output.transparent |
|
screenshot.overlay, screenshot.codeScheme, screenshot.animated |
- | Browser-frame overlays, code themes and animated captures are Microlink-only |
viewport.width, viewport.height |
viewport.width, viewport.height |
|
viewport.deviceScaleFactor (default 2) |
viewport.scale (default 1, max 3) |
Set it on both sides |
device |
viewport.device or a preset |
|
colorScheme |
browser.dark_mode |
|
mediaType |
browser.media |
|
waitUntil |
wait.until (load, domcontentloaded, networkidle) |
|
waitForSelector |
wait.for_selector |
|
waitForTimeout |
wait.delay (ms) |
|
timeout |
wait.timeout (seconds, max 60) |
|
adblock |
block.ads, block.cookie_banners |
Two separate switches on our side |
click |
page.click |
|
scripts, styles |
page.scripts, page.styles |
|
headers |
network.headers |
|
proxy |
network.proxy |
|
ttl, force, cacheKey |
cache.ttl, cache.refresh, cache.key |
|
filename |
output.filename |
|
meta (metadata fields) |
metadata.include |
Ours: title, description, favicon, Open Graph, Twitter card, fonts, size, status, headers |
pdf |
output.format: "pdf" + pdf options |
|
data, function, insights, palette, audio, video |
- | Microlink's data features |
The response shape is the real migration
Microlink's default answer is JSON:
{ "status": "success", "data": { "screenshot": { "url": "https://...", "type": "png", "size": 70539 } } }
Your code then downloads the file from Microlink's CDN. RenderScreenshot's default answer is the image itself, with metadata in headers: X-Cache-URL (a public CDN link to the same image, no key in it), X-Cache-Hit, X-Screenshot-Width, X-Screenshot-Height and X-Credits-Used (response headers).
If your code stores the Microlink CDN URL and shows it later, you have two choices on our side. Store X-Cache-URL, which is public until the cache TTL expires (default 24 hours, up to 30 days with cache.ttl). Or store the bytes yourself, or let us upload them to your bucket with the storage options.
The adapter below hides the difference: both providers return { image, publicUrl }.
Migration code
Raw HTTP on both sides, so you can read every parameter. With RS_ROLLOUT_PERCENT unset, it calls Microlink; at 100, RenderScreenshot.
Node.js
// screenshot.mjs: Microlink and RenderScreenshot behind one function, raw HTTP on both sides. // Microlink answers with JSON that points at a CDN file; RenderScreenshot answers with the image // and puts a public CDN link in the X-Cache-URL header. Both become { image, publicUrl }. // Node 22+, no dependencies. import { writeFile } from 'node:fs/promises'; const USE_RS = process.env.RS_ROLLOUT_PERCENT === '100'; async function viaMicrolink({ url, width, height, fullPage }) { const key = process.env.MICROLINK_API_KEY; // optional: without it you get the small keyless quota const q = new URLSearchParams({ url, screenshot: 'true', 'viewport.width': String(width), 'viewport.height': String(height), 'viewport.deviceScaleFactor': '1', // Microlink renders at 2x unless you ask for 1x 'screenshot.type': 'png', 'screenshot.fullPage': String(fullPage), }); const host = key ? 'https://pro.microlink.io' : 'https://api.microlink.io'; const res = await fetch(`${host}/?${q}`, { headers: key ? { 'x-api-key': key } : {}, signal: AbortSignal.timeout(60_000) }); const body = await res.json(); if (!res.ok || body.status !== 'success') throw new Error(`Microlink ${res.status}: ${body.message ?? body.code}`); const file = await fetch(body.data.screenshot.url); if (!file.ok) throw new Error(`Microlink CDN ${file.status}`); return { image: Buffer.from(await file.arrayBuffer()), publicUrl: body.data.screenshot.url }; } async function viaRenderScreenshot({ url, width, height, fullPage }) { const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, viewport: { width, height, scale: 1 }, capture: { mode: fullPage ? 'full_page' : 'viewport' }, output: { format: 'png' }, }), signal: AbortSignal.timeout(60_000), }); if (!res.ok) throw new Error(`RenderScreenshot ${res.status}: ${await res.text()}`); return { image: Buffer.from(await res.arrayBuffer()), publicUrl: res.headers.get('x-cache-url') }; } export function screenshot(job) { const full = { width: 1280, height: 800, fullPage: false, ...job }; return USE_RS ? viaRenderScreenshot(full) : viaMicrolink(full); } if (import.meta.url === `file://${process.argv[1]}`) { const { image, publicUrl } = await screenshot({ url: process.argv[2] ?? 'https://example.com/' }); await writeFile('out.png', image); console.log(`out.png: ${image.length} bytes, public URL: ${publicUrl ? 'yes' : 'no'}`); }
Python
# screenshot.py: Microlink and RenderScreenshot behind one function; returns (image_bytes, public_url). # Python 3.9+. pip install httpx import os import sys import httpx USE_RS = os.environ.get('RS_ROLLOUT_PERCENT') == '100' def via_microlink(url, width=1280, height=800, full_page=False): key = os.environ.get('MICROLINK_API_KEY') params = { 'url': url, 'screenshot': 'true', 'viewport.width': width, 'viewport.height': height, 'viewport.deviceScaleFactor': 1, 'screenshot.type': 'png', 'screenshot.fullPage': str(full_page).lower(), } host = 'https://pro.microlink.io' if key else 'https://api.microlink.io' res = httpx.get(host + '/', params=params, headers={'x-api-key': key} if key else {}, timeout=60) body = res.json() if res.status_code != 200 or body.get('status') != 'success': raise RuntimeError(f"Microlink {res.status_code}: {body.get('message') or body.get('code')}") public_url = body['data']['screenshot']['url'] image = httpx.get(public_url, timeout=60) image.raise_for_status() return image.content, public_url def via_renderscreenshot(url, width=1280, height=800, full_page=False): res = httpx.post( 'https://api.renderscreenshot.com/v1/screenshot', headers={'Authorization': f"Bearer {os.environ['RS_API_KEY']}"}, json={ 'url': url, 'viewport': {'width': width, 'height': height, 'scale': 1}, 'capture': {'mode': 'full_page' if full_page else 'viewport'}, 'output': {'format': 'png'}, }, timeout=60, ) res.raise_for_status() return res.content, res.headers.get('x-cache-url') def screenshot(url, **kwargs): return (via_renderscreenshot if USE_RS else via_microlink)(url, **kwargs) if __name__ == '__main__': image, public_url = screenshot(sys.argv[1] if len(sys.argv) > 1 else 'https://example.com/') with open('out.png', 'wb') as f: f.write(image) print(f"out.png: {len(image)} bytes, public URL: {'yes' if public_url else 'no'}")
Ruby
# screenshot.rb: Microlink and RenderScreenshot behind one method; returns [image_bytes, public_url]. # Ruby 3.0+, standard library only. require 'json' require 'net/http' USE_RS = ENV['RS_ROLLOUT_PERCENT'] == '100' def via_microlink(url, width: 1280, height: 800, full_page: false) key = ENV['MICROLINK_API_KEY'] uri = URI(key ? 'https://pro.microlink.io/' : 'https://api.microlink.io/') uri.query = URI.encode_www_form( 'url' => url, 'screenshot' => 'true', 'viewport.width' => width, 'viewport.height' => height, 'viewport.deviceScaleFactor' => 1, 'screenshot.type' => 'png', 'screenshot.fullPage' => full_page ) req = Net::HTTP::Get.new(uri) req['x-api-key'] = key if key res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') { |http| http.request(req) } body = JSON.parse(res.body) raise "Microlink #{res.code}: #{body['message'] || body['code']}" unless body['status'] == 'success' public_url = body.dig('data', 'screenshot', 'url') [Net::HTTP.get(URI(public_url)), public_url] end def via_renderscreenshot(url, width: 1280, height: 800, full_page: false) uri = URI('https://api.renderscreenshot.com/v1/screenshot') req = Net::HTTP::Post.new(uri, 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}", 'Content-Type' => 'application/json') req.body = { url: url, viewport: { width: width, height: height, scale: 1 }, capture: { mode: full_page ? 'full_page' : 'viewport' }, output: { format: 'png' } }.to_json res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 60) { |http| http.request(req) } raise "RenderScreenshot #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess) [res.body, res['x-cache-url']] end def screenshot(url, **kwargs) USE_RS ? via_renderscreenshot(url, **kwargs) : via_microlink(url, **kwargs) end if $PROGRAM_NAME == __FILE__ image, public_url = screenshot(ARGV[0] || 'https://example.com/') File.binwrite('out.png', image) puts "out.png: #{image.bytesize} bytes, public URL: #{public_url ? 'yes' : 'no'}" end
Tested on October 15, 2026. The Microlink path ran live against api.microlink.io in all three languages (Node.js 22.18.0, Python 3.14 with httpx 0.28.1, Ruby 4.0.7) and wrote a 1280×800 PNG each time. The RenderScreenshot path ran against a local mock that checks the endpoint, Bearer auth and JSON body. If you prefer an SDK on our side, the same request is client.take(TakeOptions.url(url).width(1280).height(800).scale(1).format('png')) in the Node SDK.
Price at three volumes
As listed on each pricing page on October 14, 2026:
| Monthly volume | Microlink | RenderScreenshot |
|---|---|---|
| Free | 100 requests a month (keyless endpoint: soft limit of 25) | 150 credits a month, no credit card |
| 1,000 | €39 (Pro, 46,000 requests) | $10 (Starter, 2,500 credits) |
| 10,000 | €39 (Pro, 46,000 requests) | $40 (Growth, 12,000 credits) |
| 100,000 | €120 (Pro, 140,000 requests) | No self-serve plan; Scale is $100 for 60,000 |
Microlink's EUR prices come from the structured price data on its pricing page; the page displayed $49 for the first Pro tier to me. Microlink counts every API call as a request, whichever endpoint, and does not count cache hits. At 100% of quota it pauses requests until the next cycle or an upgrade, with no overage fees. The pricing comparison has the other vendors.
When to stay with Microlink
- You use the data features. Metadata, logos, palettes, Markdown, technology detection and Lighthouse in the same call have no equivalent on our side beyond basic metadata.
- You are above 60,000 requests a month and want a self-serve plan.
- You want the keyless endpoint for prototypes, demos or docs examples.
- You want hard caps. Microlink pauses at quota instead of billing more; we stop at your credit balance too, so this is a tie if that is your only concern.
- You use browser-frame overlays or animated screenshots.
Try it on your URLs
Create a free RenderScreenshot key and run the adapter above with RS_ROLLOUT_PERCENT=100 on the same ten URLs you use with Microlink. Set the scale to 1 on both, and compare the images, not only the times.
Related
- Best screenshot API comparison: ten services, one checklist, one script.
- Screenshot API pricing comparison: what one "request" or "credit" really buys.
- ApiFlash alternative: a step-by-step rollout pattern with a URL hash.
- Caching:
X-Cache-URL, TTL and zero-credit cache hits.