Convert a Webpage to PDF with WebKit on macOS
Generate PDF data from a webpage in macOS with WKWebView. Compare WebKit’s PDF and print APIs, save the output, and handle loading and layout edge cases.
To convert a webpage to PDF in a macOS app, load it in WKWebView and call pdf(configuration:) to asynchronously get PDF data. Save that Data to a file with Foundation. Use createPDF(configuration:completionHandler:) if your code uses completion handlers, or printOperation(with:) when the user should get WebKit’s AppKit print flow.
WKPDFConfiguration specifies which portion of the web view to capture. Apple’s API documentation does not establish universal pagination, margins, paper size, or rendering behavior for every macOS version, so validate the output with your target SDK and representative pages.
1. Choose the WebKit PDF or print API
| Need | API | Result |
|---|---|---|
| Save PDF bytes from your app | pdf(configuration:) |
Asynchronously returns PDF data. |
| Use a completion-handler codebase | createPDF(configuration:completionHandler:) |
Completes with PDF data or an error. |
| Print the web view | printOperation(with:) |
Returns an NSPrintOperation, or may return nil if printing is unsupported. |
Apple describes WKWebView as the native view for interactive web content in macOS apps, replacing the older WebView class on macOS 10.10 and later. Check API availability against your deployment target and selected SDK. See Apple’s WKWebView documentation.
2. Load a page and write PDF data to a file
The following Swift example creates a web view, loads a remote URL, waits for navigation to finish, requests PDF data, and writes it to a destination. Put the coordinator and view in your macOS app; for a command-line utility or background conversion service, account for the app and WebKit view lifecycle required by your target environment.
import AppKit
import WebKit
@MainActor
final class PagePDFExporter: NSObject, WKNavigationDelegate {
private let webView = WKWebView(frame: .zero)
private var continuation: CheckedContinuation<Data, Error>?
func export(url: URL, to destination: URL) async throws {
webView.navigationDelegate = self
let pdfData = try await withCheckedThrowingContinuation {
(continuation: CheckedContinuation<Data, Error>) in
self.continuation = continuation
webView.load(URLRequest(url: url))
}
try pdfData.write(to: destination, options: .atomic)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
guard let continuation else { return }
self.continuation = nil
let configuration = WKPDFConfiguration()
webView.pdf(configuration: configuration) { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
func webView(
_ webView: WKWebView,
didFail navigation: WKNavigation!,
withError error: Error
) {
finishWithError(error)
}
func webView(
_ webView: WKWebView,
didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error
) {
finishWithError(error)
}
private func finishWithError(_ error: Error) {
guard let continuation else { return }
self.continuation = nil
continuation.resume(throwing: error)
}
}
// Example use from app code:
// let exporter = PagePDFExporter()
// try await exporter.export(
// url: URL(string: "https://example.com")!,
// to: FileManager.default.temporaryDirectory.appendingPathComponent("page.pdf")
// )
This waits for WebKit’s navigation completion callback, which indicates that navigation finished; it does not guarantee that every page-specific asynchronous task, image, or lazy-loaded section is ready. For sites that render content after navigation, wait for a page-specific readiness condition before requesting the PDF. The mechanism for that condition depends on the page and app.
The PDF-data API is documented by Apple as asynchronous. The configuration specifies the portion of the web view captured. See WKWebView.pdf(configuration:).
3. Use the callback form or print operation
Completion-handler form
For an existing callback-based flow, use createPDF and handle both its data and error result. Write the data only after success.
import AppKit
import WebKit
func createPDF(from webView: WKWebView, destination: URL) {
let configuration = WKPDFConfiguration()
webView.createPDF(configuration: configuration) { result in
switch result {
case .success(let data):
do {
try data.write(to: destination, options: .atomic)
} catch {
NSLog("Could not save PDF: %@", error.localizedDescription)
}
case .failure(let error):
NSLog("Could not create PDF: %@", error.localizedDescription)
}
}
}
Print operation
For a user-initiated print flow, ask WebKit for its print operation and handle the documented possibility that it is unavailable. The print operation is not the same as directly receiving PDF bytes; use the PDF-data APIs when your app needs to own the resulting Data.
import AppKit
import WebKit
func printPage(from webView: WKWebView) {
guard let operation = webView.printOperation(with: NSPrintInfo.shared) else {
NSLog("Printing is not supported for this web view")
return
}
operation.run()
}
Apple documents printOperation(with:) as returning the operation used to print the web view’s contents, with nil possible when printing is unsupported.
4. Configure capture and validate layout
Pass a WKPDFConfiguration to the PDF API to specify the portion of the web view captured. Consult the SDK documentation for the properties available to your deployment target. Do not assume that the API provides a fixed cross-version contract for page size, margins, page breaks, or scaling: the cited documentation does not establish those details for every version.
Before shipping, check PDFs from the target macOS versions against pages that represent your actual workload:
- Short and very long pages, including content that extends below the initial viewport.
- Pages with lazy-loaded images or content inserted after navigation.
- Print styles, explicit CSS page breaks, wide tables, and fixed-position elements.
- Different fonts, image sizes, and network conditions.
- Pages that require authentication, cookies, or a particular user state.
Web content controls much of its own layout. If an output has unexpected breaks or clipping, inspect the page’s print CSS and the captured portion, then reproduce the issue on the macOS version and SDK you support.
5. cURL, Python, and Node.js alternatives
These examples use ScreenshotNeo’s screenshot API to create a PDF from a URL in one request. They are useful when you want a server-side API workflow rather than embedding WebKit in a macOS app. The API supports PDF output; see the ScreenshotNeo API documentation for request options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d format=pdf \
-o page.pdf
Python
import requests
response = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"format": "pdf",
},
timeout=90,
)
response.raise_for_status()
with open("page.pdf", "wb") as output:
output.write(response.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
format: 'pdf',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('page.pdf', Buffer.from(await res.arrayBuffer()))
);
6. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a screenshot or PDF. Cookie banners are accepted like a visitor and removed before the shot, along with 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d format=pdf \
-o page.pdf
Read the ScreenshotNeo API docs and sign up for 1,000 free screenshots a month with no card.
7. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| PDF generation returns an error | The view has not finished loading, or WebKit reports a capture failure. | Wait for navigation completion, handle the result’s error, and confirm the web view is still alive when capture runs. |
| The saved file is missing or empty | The write failed, the app wrote to an unexpected destination, or data was not returned successfully. | Check the thrown file error and destination permissions; write only in the success branch and verify the resulting file in app code. |
| Some page content is absent | It may load after navigation, use lazy loading, or require a logged-in session. | Wait for the page’s own readiness condition, preserve the needed web view state, and verify images and dynamic sections before capture. |
| Page breaks or margins look wrong | Web content and WebKit layout can vary; no universal pagination behavior is established here. | Inspect print CSS and configuration, then validate with the same SDK, OS versions, and representative pages used in production. |
printOperation(with:) returns nil |
Printing is unsupported for the current web view. | Handle nil and use the PDF-data API if the app needs PDF bytes. |
| API request returns an error | Check credentials, URL encoding, network access, and the API response. | Use the docs for request parameters, inspect the HTTP status, and avoid treating an error response as PDF bytes. |
8. Performance, reliability, and cost
WebKit conversion runs through a live web view, so the cost in time and memory depends on page complexity, network loading, and the amount of content captured. Reuse a suitable app-level flow where practical, avoid starting overlapping captures on the same view, and ensure each export has a clear completion or failure path. For dynamic sites, define readiness explicitly rather than assuming navigation completion means all content is settled.
The cited Apple documentation does not provide timing or resource benchmarks, nor a cross-version guarantee for pagination. Measure representative pages in your application and test the macOS versions you support. Local WebKit usage does not have a per-request API price in this workflow; the app still bears its own machine, development, and operational costs.
With ScreenshotNeo, only clean shots are billed: bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses include X-Page-Verdict and X-Billed headers. Pricing is Free for 1,000 shots monthly, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan.
9. FAQ
Can WKWebView create a PDF without showing a print dialog?
Use the PDF-data APIs, pdf(configuration:) or createPDF(configuration:completionHandler:), to obtain PDF data for your app to save. The print operation is for the printing workflow.
Does didFinish mean every image and script has finished?
Do not rely on it as a guarantee that all page-specific asynchronous work is complete. Pages can continue loading or inserting content after navigation finishes; wait for a readiness condition that matches the page.
Does WebKit guarantee identical pagination on every macOS release?
The consulted API documentation does not establish that guarantee. Test output using your target SDKs and actual page types.
Can a macOS app use this method for local HTML?
WKWebView supports loading remote requests, local files, and HTML strings. Load the content into the view, wait until it is ready for your use case, then request its PDF data.


