Microlink Screenshot Returns a Blank Image: Causes and Fixes
A blank Microlink screenshot can mean the page was captured before its app rendered, or that access was blocked. Follow these checks to find the cause and fix it.
A Microlink screenshot that looks blank may have been captured before a client-rendered app finished loading its data. It may also show a spinner, a lazy-loaded section that was never triggered, a login page, or a bot challenge. Start by checking the API response and the image asset, then wait for an element that proves the content you need is present. If the capture shows a login or challenge page, investigate access rather than adding more delay.
A page reaching a browser navigation event does not guarantee that its application has hydrated or fetched its data. Microlink’s dynamic-content guide recommends waiting for a meaningful selector, using network-idle or a fixed timeout only when appropriate, and triggering lazy or interactive content before capture.
1. Check whether the screenshot asset is actually blank
Before changing wait settings, separate a capture problem from a problem displaying the returned image. For a basic Microlink capture, the API request uses the target url and screenshot=true. A successful response includes data.screenshot.url and metadata such as the image dimensions, type, and size. Check the HTTP status and response body, then open the asset URL directly. If the asset has nonzero dimensions and size but your application renders it blank, investigate how that application fetches or displays the URL.
curl -G 'https://api.microlink.io/' \
--data-urlencode 'url=https://app.example.com/report' \
--data 'screenshot=true' \
--data 'meta=false'
For a screenshot-only request, meta=false skips unrelated metadata extraction. It can reduce work, but it does not make an unrendered page ready. See Microlink’s screenshot parameter documentation for the response and screenshot options.
2. Wait for the content that should appear
For a client-rendered chart or dashboard, navigate at an early lifecycle event and wait for a selector tied to the finished content. A generic selector such as body is usually a poor readiness signal because it can exist while the application is still empty.
curl -G 'https://api.microlink.io/' \
--data-urlencode 'url=https://app.example.com/report' \
--data 'screenshot=true' \
--data 'meta=false' \
--data 'waitUntil=domcontentloaded' \
--data-urlencode 'waitForSelector=.chart svg'
Replace .chart svg with an element that appears only when the desired content has rendered. Microlink’s documented JavaScript pattern is:
import createClient from 'microlink.io'
const microlink = createClient({
apiKey: process.env.MICROLINK_API_KEY
})
const { url } = await microlink.screenshot(
'https://app.example.com/report',
{
meta: false,
waitUntil: 'domcontentloaded',
waitForSelector: '.chart svg'
}
)
console.log(url)
Install the client with npm install microlink.io and set MICROLINK_API_KEY if using a key. The selector is an example: inspect the target page and choose one that represents the actual data or component you need.
Choose the wait condition deliberately
| Option | What it waits for | Good fit | Watch out for |
|---|---|---|---|
waitForSelector |
A matching element in the page | A chart, result list, or app component that signals readiness | The selector must describe the desired content, not an early shell |
waitUntil: 'domcontentloaded' |
The DOM has been parsed | Starting capture promptly, then waiting for a content selector | It does not mean client-side data has loaded |
waitUntil: 'load' |
The page load event | Pages whose useful content depends on load-complete resources | It still may not mean a framework has hydrated or fetched data |
waitUntil: 'networkidle0' or 'networkidle2' |
A period of low or no network activity | Pages where outstanding requests are the clearest readiness signal | Long-polling or persistent connections can keep the page from becoming idle |
waitForTimeout |
A fixed delay | Only when no stable observable condition is available | Too short is flaky; too long wastes time. It must fit within the request timeout. |
Microlink also supports waitUntil: 'auto' and an array of lifecycle events. When a stable selector exists, it is generally the most direct readiness condition: it can finish as soon as the target content appears. A fixed delay is a fallback, not proof that the page is ready.
3. Trigger lazy or interactive content before capture
Some pages do not load a section until it enters the viewport. Others keep a tab, accordion, or panel hidden until it is clicked. In those cases, perform the action first, then wait for an element inside the resulting content.
Scroll to a lazy section
curl -G 'https://api.microlink.io/' \
--data-urlencode 'url=https://example.com/reviews' \
--data 'screenshot=true' \
--data 'meta=false' \
--data 'scroll=#reviews' \
--data-urlencode 'waitForSelector=#reviews .card' \
--data 'fullPage=true'
Scrolling brings the target section into view so its lazy-loaded content can start, while the selector wait checks for a card before the full-page capture. Replace the selectors with ones from the page.
Click a tab or expand control
curl -G 'https://api.microlink.io/' \
--data-urlencode 'url=https://app.example.com/analytics' \
--data 'screenshot=true' \
--data 'meta=false' \
--data 'click=#tab-revenue' \
--data-urlencode 'waitForSelector=#panel-revenue canvas'
Microlink’s browser automation options describe combining click, scroll, and waits. If you capture one element with screenshot.element, Microlink waits for that element to be visible; a separate wait is useful when capturing a viewport or full page and waiting for other content.
4. Treat login pages and bot challenges as access problems
If the image contains a login form, challenge, or access-denied page, extra rendering time is unlikely to fix the root cause. Confirm what a normal browser shows and inspect the response or error. Use this branch only when the result indicates a login or block; a blank-looking image alone does not prove bot protection.
If the capture shows a login page
The target may not have received a valid session. Microlink documents forwarding cookies or authorization headers through its Pro endpoint. Check that the cookie belongs to the target domain, has not expired, and is sent to the documented endpoint. Keep credentials out of public query strings; use request headers for sensitive values as described in Microlink’s official documentation.
If the capture shows a bot challenge
Microlink documents antibot handling separately from page timing: a free request may return EPROXYNEEDED, while its Pro offering can route blocked requests through proxy tiers. Check the actual error and current Microlink guidance before changing plans or proxy settings. Proxy behavior and service tiers can change, and not every empty screenshot is an antibot issue. See Microlink’s antibot detection guide.
5. Python and Node.js request examples
These examples call Microlink’s API directly and print the returned screenshot asset URL. They keep the API response available for inspection rather than assuming that a request produced the intended page state.
Python
import requests
params = {
"url": "https://app.example.com/report",
"screenshot": "true",
"meta": "false",
"waitUntil": "domcontentloaded",
"waitForSelector": ".chart svg",
}
response = requests.get("https://api.microlink.io/", params=params, timeout=90)
response.raise_for_status()
payload = response.json()
print(payload)
screenshot = payload.get("data", {}).get("screenshot", {})
print("Screenshot asset:", screenshot.get("url"))
print("Dimensions:", screenshot.get("width"), "x", screenshot.get("height"))
print("Type and size:", screenshot.get("type"), screenshot.get("size"))
Install the dependency with python -m pip install requests. Adjust the client timeout to suit your application, but remember that the upstream service has its own request timeout.
Node.js
const q = new URLSearchParams({
url: 'https://app.example.com/report',
screenshot: 'true',
meta: 'false',
waitUntil: 'domcontentloaded',
waitForSelector: '.chart svg'
})
const res = await fetch(`https://api.microlink.io/?${q}`)
if (!res.ok) {
throw new Error(`Microlink returned HTTP ${res.status}: ${await res.text()}`)
}
const payload = await res.json()
const screenshot = payload.data?.screenshot
console.log(screenshot)
console.log('Screenshot asset:', screenshot?.url)
Run this with a Node.js version that supports global fetch, or use a compatible fetch implementation. If your Microlink setup requires a key, follow its current documentation for authentication and do not expose secrets in a public client.
6. Troubleshooting checklist
| What you see | Likely cause | What to do |
|---|---|---|
| Empty app shell or spinner | Capture started before hydration or data rendering | Wait for a selector that appears with the desired content; pair it with domcontentloaded if useful. |
| Top of page appears, lower section is empty | Section loads on scroll or lazily | Use scroll on that section, wait for a child element, then capture full page if needed. |
| Tab or accordion content is missing | Content is hidden until interaction | Use click on its control and wait for an element inside the opened panel. |
| Request waits or times out on network idle | Persistent connection or background requests prevent network silence | Prefer a content selector. Use a fixed delay only if no reliable readiness signal exists. |
| Login form in the image | Missing, invalid, expired, or mis-scoped authentication | Check the session, cookie domain and expiry, endpoint, and supported header forwarding. |
Challenge or access-denied image; possible EPROXYNEEDED |
Target access or antibot protection | Inspect the API error and follow Microlink’s current proxy guidance; do not treat this as a timing issue. |
| API reports an image URL, but your page shows a blank image | Downstream image loading or display problem | Open the asset URL directly and inspect its status, dimensions, type, and size; then debug your app’s URL handling. |
| Selector wait never completes | Selector is wrong, content is absent, or it is in a frame/shadow root not matched as expected | Inspect the rendered DOM and choose a selector supported by the target page and capture setup. Verify the content appears in a normal browser. |
| Timeout after adding more waits | Total navigation and wait work exceeds the plan’s request limit | Remove unrelated waits, use the narrowest readiness condition, and check Microlink’s current timeout limits. |
For a useful incident record, save the target URL, request parameters, HTTP status and response body, screenshot metadata if present, and what the page displays in a regular browser. That information distinguishes rendering timing from access failures and image-display bugs.
7. Performance, reliability, and cost considerations
- Prefer conditions over guessed delays. A selector wait can end as soon as the required element appears. A fixed timer adds the same delay to fast requests and can still be too short for slow ones.
- Skip work you do not need. Set
meta=falsefor screenshot-only requests to avoid unrelated metadata extraction. It is a performance simplification, not a rendering fix. - Use network idle selectively. It can help when outstanding requests determine readiness, but long-polling and persistent connections may keep the page active.
- Keep waits inside the service timeout. Microlink’s dynamic-content page described 30 seconds for the free endpoint and 60 seconds for Pro when accessed for this research; these are volatile plan details, so verify the current limits before relying on them. Your HTTP client timeout should also allow for the full request.
- Retry only transient failures. A bounded retry with backoff may help with transient network errors, but repeating a deterministic wrong selector, expired login, or antibot block wastes requests. Check status and response before retrying.
- Cost depends on current service terms and request volume. Confirm Microlink’s live plan and quota details for your use case. Do not assume that adding a wait or retry is free or unlimited.
Or skip the browser setup
If you need a screenshot without building and tuning the browser flow, ScreenshotNeo is a website screenshot API and MCP server. One GET request accepts a URL and returns an image or PDF. Its clean-shot options can accept consent banners like a visitor and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. AI agents can use its MCP server tools, including take_screenshot, get_page_info, and capture_pdf.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options and setup. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 screenshots. Sign up free for ScreenshotNeo.
FAQ
Why is my Microlink screenshot blank?
The leading documented possibility is that capture happened before a client-rendered page finished hydrating or loading data. A login, bot challenge, lazy section, or downstream image-display issue can look similar, so check the response and image before choosing a fix.
How do I make Microlink wait until the page finishes loading?
Wait for a selector that appears when the exact content you need is ready. Use network-idle when network quiet is a useful signal, and a fixed delay only when no stable condition is available.
Does a blank-looking image always mean Microlink failed?
No. Check whether the response contains a screenshot asset URL with nonzero dimensions and size, and open the asset directly. The capture may exist even if the consuming application fails to display it.
Should I always use networkidle0?
No. A page with long-polling or persistent requests may never become idle. A selector for the target content is more specific when one is available.


