Converting a URL to PDF means loading the page in a real browser engine, waiting for it to finish rendering, and printing the result. The three things that determine whether the output is correct are when you decide the page is ready, whether lazy-loaded content has been triggered, and whether overlays like cookie banners are covering the content.
Get those right and the output matches what you see. Get them wrong and you get a blank page, a half-loaded page, or a cookie banner across the top of every capture.
How does URL to PDF actually work?
The renderer opens a headless browser, navigates to your URL, waits for a defined condition, then prints to PDF.
curl -X POST https://www.ashdocs.com/api/v1/tools/url-to-pdf \
-H "X-API-Key: ash_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/report",
"options": {
"format": "A4",
"printBackground": true,
"waitUntil": "networkidle",
"margin": "15mm"
}
}'
The waitUntil value is the most consequential option on that request.
When should I consider the page ready?
| Condition | Fires when | Use for |
|---|---|---|
load | The load event fires | Static pages, fastest |
domcontentloaded | HTML parsed, resources may be pending | Rarely correct for PDF |
networkidle | No network activity for a short interval | Most pages — the sensible default |
| Element selector | A named element appears | Single-page apps and dashboards |
| Fixed delay | After N milliseconds | Last resort |
Use networkidle unless you have a reason not to. It handles the common case — a page that fetches data after initial load — without guessing at a delay.
For single-page applications, wait on a selector. A React or Vue dashboard may reach network idle before rendering its content. Waiting for .report-loaded or a similar marker is far more reliable than any timing heuristic.
A fixed delay is a last resort. Too short and you capture a spinner. Too long and every request is slow. It fails in both directions on a slow day.
Why are my images missing?
Lazy loading. Images below the fold do not load until the user scrolls, and a headless renderer never scrolls.
Three fixes, in order of preference:
Enable full-page capture with automatic scrolling, if your renderer supports it — it scrolls to the bottom, triggering the lazy loaders, then returns to the top and prints.
Inject CSS to disable lazy loading before capture:
img { loading: eager !important; }
[data-src] { content: attr(data-src); }
Set a large viewport height so more of the page is "above the fold" from the browser's perspective.
How do I get rid of cookie banners?
They appear on almost every European site and cover the top of every capture.
Inject CSS to remove them:
{
"injectCss": "#cookie-banner, .cookie-consent, [class*='cookie'], [id*='gdpr'], .modal-overlay { display: none !important; } body { overflow: visible !important; }"
}
That overflow: visible matters — many banners lock body scrolling, which can truncate the capture even after the banner itself is hidden.
Or set the consent cookie before navigating, if you know its name. Cleaner, because the banner never renders at all.
Can I capture pages behind a login?
Yes, with credentials passed to the renderer — but think carefully first.
{
"url": "https://app.example.com/dashboard",
"cookies": [
{ "name": "session", "value": "...", "domain": "app.example.com" }
]
}
Or with custom headers for token-based authentication.
The security consideration: you are sending a session credential to a third-party service. For your own application that may be acceptable; for a customer's credentials it usually is not. Prefer a signed, time-limited URL that grants read access to the specific page, rather than transmitting a session token.
What usually goes wrong
Blank pages. The most common failure. Almost always a capture that fired before rendering completed. Switch to networkidle or wait on a selector.
Content cut off at one page. The renderer captured the viewport rather than the full document. Enable full-page capture.
Backgrounds missing. printBackground: true. Same as with HTML to PDF, and worth checking first.
Fonts wrong. The page loads web fonts from a CDN. If the renderer's egress is restricted, they fail and fall back. Verify the renderer can reach your font host.
Timeouts on heavy pages. Dashboards with many API calls may exceed the render timeout. Increase it, or use an asynchronous pattern where the API returns a job ID and a webhook delivers the finished file.
Layout differs from the browser. The render viewport width differs from yours. Set it explicitly rather than relying on a default.
Infinite scroll never finishes. A feed that loads more content forever will never reach network idle. Cap the scroll depth or wait on a selector instead.
Frequently asked questions
How do I convert a URL to PDF?
Send the URL to a rendering service that loads it in a headless browser and prints the result. Set waitUntil to networkidle so the page finishes rendering, and enable printBackground to keep colours and backgrounds.
Why is my URL to PDF output blank?
The capture fired before the page finished rendering. Use networkidle rather than load, or wait for a specific element to appear — particularly on single-page applications.
Can I convert a password-protected page to PDF?
Yes, by passing session cookies or authentication headers to the renderer. Consider the security implications first; a signed time-limited URL is usually safer than transmitting a session token.
How do I remove cookie banners from a PDF capture?
Inject CSS that hides the banner elements, and reset body { overflow: visible } since many banners lock scrolling. Alternatively set the consent cookie before navigating so the banner never renders.
Why are images missing from my captured page?
Lazy loading. Images below the fold never load because the renderer does not scroll. Enable full-page capture with auto-scroll, or inject CSS forcing eager loading.
In short
Use networkidle, or wait on a selector for single-page apps. Enable printBackground. Handle lazy loading with full-page capture. Hide cookie banners with injected CSS and reset body overflow. Set the viewport explicitly.
Blank output is nearly always a timing problem, and it is nearly always fixed by changing what you wait for.