How to Convert URLs to PDF with an API (React, Kotlin, Java, Swift)

EnConvert Team ·
How to Convert URLs to PDF with an API (React, Kotlin, Java, Swift)

The Simplest Way to Generate PDFs from URLs

Generating a PDF from a webpage sounds simple until you actually try to build it. Headless browsers eat memory. Lazy-loaded images break. Cookie banners end up in the output. Sticky headers repeat on every page.

EnConvert's url-to-pdf endpoint handles all of this for you. You send a URL, and you get back a perfectly rendered PDF. Cookie banners are dismissed automatically. Lazy content is loaded by scrolling through the entire page. Images are fully rendered before capture. Sticky headers are detected and handled.

One API call. One clean PDF. Let's integrate it.


Before You Start

You'll need an EnConvert API key. Here's how to get one:

  1. Create a free account at enconvert.com

  2. From your dashboard, go to the API Keys section

  3. Click Create New Key

  4. For server-side use (Kotlin, Java, Swift backend), choose Private Key

  5. For browser-side use (React), choose Public Key — you'll need to set your allowed domains and select url-to-pdf as an allowed endpoint

  6. Copy the key immediately — it's shown only once

Private keys look like: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Public keys look like: pk_live_xxxxxxxxxxxxxxxxxxxxxxxx


How the API Works

The conversion endpoint is:

POST /v1/convert/url-to-pdf

Server-side (Private Key): You send the request with your API key in the X-API-Key header along with a JSON body containing the URL. The API returns the PDF binary directly, with metadata in the response headers (X-Filename, X-File-Size, X-Conversion-Time).

Client-side (Public Key): Since you can't expose a private key in the browser, public keys follow a two-step flow. First, you exchange your public key for a short-lived JWT token (valid for 1 hour) through the /v1/auth/token endpoint. Then you use that JWT as a Bearer token for the conversion request. Cloudflare Turnstile verification is required during the token exchange to prevent bot abuse.

The API also supports several optional parameters to control the rendering:

  • load_media — Whether to load images and media (default: true)

  • enable_scroll — Scroll through the page to trigger lazy-loaded content (default: true)

  • handle_sticky_header — Detect and handle fixed/sticky navigation bars (default: true)

  • handle_cookies — Automatically dismiss cookie consent banners (default: true)

  • wait_for_images — Wait for all images to fully load before rendering (default: true)

  • viewport_width — Browser viewport width in pixels (default: 1920)

  • viewport_height — Browser viewport height in pixels (default: 1080)

  • single_page — Render as a single continuous page (default: true)

Now let's look at real code.


React

React runs in the browser, so you'll use a public API key with the JWT token exchange flow. This matches exactly how EnConvert's own Playground works — exchange the key for a token, then call the conversion endpoint with that token.

import { useState, useRef } from "react";
import { Turnstile } from "@marsidev/react-turnstile";

const GATEWAY = "https://api.enconvert.com";
const PUBLIC_KEY = "pk_live_your_public_key_here";
const TURNSTILE_SITE_KEY = "your_turnstile_site_key";

export default function PdfConverter() {
  const [url, setUrl] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");
  const turnstileRef = useRef(null);

  const handleConvert = async () => {
    setError("");
    setLoading(true);

    try {
      // Step 1: Get Turnstile verification token
      const turnstileToken = turnstileRef.current?.getResponse();
      if (!turnstileToken) {
        throw new Error("Verification pending — please try again.");
      }

      // Step 2: Exchange public API key for a short-lived JWT
      const authRes = await fetch(`${GATEWAY}/v1/auth/token`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-Key": PUBLIC_KEY,
        },
        body: JSON.stringify({ turnstile_token: turnstileToken }),
      });

      if (!authRes.ok) throw new Error("Authentication failed");
      const { token } = await authRes.json();

      // Step 3: Call the conversion endpoint with the JWT
      const convRes = await fetch(`${GATEWAY}/v1/convert/url-to-pdf`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ url }),
      });

      if (!convRes.ok) {
        const errData = await convRes.json().catch(() => ({}));
        throw new Error(errData.detail || "Conversion failed");
      }

      // Step 4: Handle the response
      const contentType = convRes.headers.get("Content-Type") || "";

      if (contentType.includes("application/json")) {
        // Response contains a presigned download URL
        const data = await convRes.json();
        window.open(data.presigned_url, "_blank");
      } else {
        // Response is the PDF binary — create a download link
        const blob = await convRes.blob();
        const blobUrl = URL.createObjectURL(blob);
        const filename =
          convRes.headers.get("X-Filename") || "converted.pdf";

        const a = document.createElement("a");
        a.href = blobUrl;
        a.download = filename;
        a.click();
        URL.revokeObjectURL(blobUrl);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
      turnstileRef.current?.reset();
    }
  };

  return (
    <div>
      <input
        type="url"
        placeholder="https://example.com"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
      />
      <Turnstile
        ref={turnstileRef}
        siteKey={TURNSTILE_SITE_KEY}
        options={{ size: "invisible" }}
      />
      <button onClick={handleConvert} disabled={loading || !url.trim()}>
        {loading ? "Converting..." : "Convert to PDF"}
      </button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </div>
  );
}

What's happening here:

  • The Turnstile widget runs invisibly in the background — no user friction

  • The public key is exchanged for a JWT that expires in 1 hour

  • The conversion response either returns the PDF directly as binary or a JSON object with a presigned_url for downloading

  • After the conversion completes, the Turnstile widget resets for the next request

Handling slow conversions: Some heavy pages can take longer than 60 seconds to render. If the request times out (returns a 504 or the network connection drops), you can poll for the result using a job_id. Pass a job_id (use crypto.randomUUID()) in your conversion request body, then poll GET /v1/convert/status/{jobId} every 5 seconds until the status is "success" or "failed".


Kotlin

Kotlin is commonly used for Android apps and server-side JVM applications. With a private API key, the integration is clean and direct.

import java.io.File
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse

fun convertUrlToPdf(url: String, outputPath: String) {
    val client = HttpClient.newHttpClient()

    val requestBody = """{"url": "$url"}"""

    val request = HttpRequest.newBuilder()
        .uri(URI.create("https://api.enconvert.com/v1/convert/url-to-pdf"))
        .header("X-API-Key", "sk_live_your_private_key_here")
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(requestBody))
        .build()

    val response = client.send(request, HttpResponse.BodyHandlers.ofByteArray())

    if (response.statusCode() != 200) {
        throw RuntimeException("Conversion failed: ${String(response.body())}")
    }

    val filename = response.headers()
        .firstValue("X-Filename")
        .orElse("converted.pdf")

    File(outputPath, filename).writeBytes(response.body())
    println("PDF saved: $outputPath/$filename")
}

fun main() {
    convertUrlToPdf("https://example.com", "./output")
}

For Android apps, you'll want to run this off the main thread. Here's how it looks with Kotlin coroutines and OkHttp:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.util.concurrent.TimeUnit

suspend fun convertUrlToPdf(url: String, outputFile: File) {
    withContext(Dispatchers.IO) {
        val client = OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(120, TimeUnit.SECONDS)
            .build()

        val json = """{"url": "$url"}"""
        val body = json.toRequestBody("application/json".toMediaType())

        val request = Request.Builder()
            .url("https://api.enconvert.com/v1/convert/url-to-pdf")
            .addHeader("X-API-Key", "sk_live_your_private_key_here")
            .post(body)
            .build()

        val response = client.newCall(request).execute()

        if (!response.isSuccessful) {
            throw Exception("Conversion failed: ${response.body?.string()}")
        }

        val pdfBytes = response.body?.bytes()
            ?: throw Exception("Empty response")

        outputFile.writeBytes(pdfBytes)
    }
}

A note on timeouts: URL-to-PDF conversions involve rendering full web pages with JavaScript, images, and dynamic content. Set your read timeout to at least 120 seconds to accommodate complex pages. The default EnConvert server timeout is 300 seconds.


Java

Java's HttpClient (Java 11+) makes this straightforward. No external dependencies needed.

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;

public class PdfConverter {

    private static final String API_KEY = "sk_live_your_private_key_here";
    private static final String ENDPOINT = "https://api.enconvert.com/v1/convert/url-to-pdf";

    public static Path convertUrlToPdf(String url, String outputDir)
            throws IOException, InterruptedException {

        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(30))
                .build();

        String jsonBody = String.format("{\"url\": \"%s\"}", url);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(ENDPOINT))
                .header("X-API-Key", API_KEY)
                .header("Content-Type", "application/json")
                .timeout(Duration.ofSeconds(120))
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();

        HttpResponse<byte[]> response = client.send(
                request, HttpResponse.BodyHandlers.ofByteArray()
        );

        if (response.statusCode() != 200) {
            throw new IOException(
                "Conversion failed (" + response.statusCode() + "): "
                + new String(response.body())
            );
        }

        String filename = response.headers()
                .firstValue("X-Filename")
                .orElse("converted.pdf");

        Path outputPath = Path.of(outputDir, filename);
        Files.write(outputPath, response.body());

        System.out.println("PDF saved: " + outputPath);
        return outputPath;
    }

    public static void main(String[] args) throws Exception {
        convertUrlToPdf("https://example.com", "./output");
    }
}

With Spring Boot, you might want to expose this as a service endpoint so your users can trigger conversions from your own application:

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/api/pdf")
public class PdfController {

    private static final String API_KEY = "sk_live_your_private_key_here";
    private static final String ENDPOINT = "https://api.enconvert.com/v1/convert/url-to-pdf";

    private final RestTemplate restTemplate = new RestTemplate();

    @PostMapping("/from-url")
    public ResponseEntity<byte[]> convertUrl(@RequestParam String url) {

        HttpHeaders headers = new HttpHeaders();
        headers.set("X-API-Key", API_KEY);
        headers.setContentType(MediaType.APPLICATION_JSON);

        String body = String.format("{\"url\": \"%s\"}", url);

        HttpEntity<String> request = new HttpEntity<>(body, headers);

        ResponseEntity<byte[]> response = restTemplate.exchange(
                ENDPOINT, HttpMethod.POST, request, byte[].class
        );

        String filename = response.getHeaders().getFirst("X-Filename");
        if (filename == null) filename = "converted.pdf";

        return ResponseEntity.ok()
                .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                .contentType(MediaType.APPLICATION_PDF)
                .body(response.getBody());
    }
}

This creates a /api/pdf/from-url?url=https://example.com endpoint on your own server that proxies the conversion through EnConvert — keeping your API key safely on the server while your frontend gets a direct PDF download.


Swift

For iOS and macOS apps, Swift's URLSession handles the conversion cleanly. Here's how to do it with async/await (Swift 5.5+):

import Foundation

struct PdfConverter {
    
    static let apiKey = "sk_live_your_private_key_here"
    static let endpoint = URL(string: "https://api.enconvert.com/v1/convert/url-to-pdf")!
    
    static func convertUrlToPdf(url: String) async throws -> (Data, String) {
        
        var request = URLRequest(url: endpoint)
        request.httpMethod = "POST"
        request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.timeoutInterval = 120
        
        let body: [String: Any] = ["url": url]
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse else {
            throw URLError(.badServerResponse)
        }
        
        guard httpResponse.statusCode == 200 else {
            let errorMessage = String(data: data, encoding: .utf8) ?? "Unknown error"
            throw NSError(
                domain: "PdfConverter",
                code: httpResponse.statusCode,
                userInfo: [NSLocalizedDescriptionKey: "Conversion failed: \(errorMessage)"]
            )
        }
        
        let filename = httpResponse.value(forHTTPHeaderField: "X-Filename") ?? "converted.pdf"
        
        return (data, filename)
    }
}

Using it in a SwiftUI view:

import SwiftUI

struct ContentView: View {
    @State private var url = ""
    @State private var isConverting = false
    @State private var message = ""
    
    var body: some View {
        VStack(spacing: 20) {
            TextField("Enter URL", text: $url)
                .textFieldStyle(.roundedBorder)
                .autocapitalization(.none)
                .keyboardType(.URL)
            
            Button(isConverting ? "Converting..." : "Convert to PDF") {
                Task { await convert() }
            }
            .disabled(url.isEmpty || isConverting)
            
            if !message.isEmpty {
                Text(message)
                    .foregroundColor(.secondary)
            }
        }
        .padding()
    }
    
    func convert() async {
        isConverting = true
        message = ""
        
        do {
            let (pdfData, filename) = try await PdfConverter.convertUrlToPdf(url: url)
            
            // Save to Documents directory
            let documentsUrl = FileManager.default.urls(
                for: .documentDirectory, in: .userDomainMask
            ).first!
            let fileUrl = documentsUrl.appendingPathComponent(filename)
            try pdfData.write(to: fileUrl)
            
            message = "Saved: \(filename) (\(pdfData.count / 1024) KB)"
        } catch {
            message = "Error: \(error.localizedDescription)"
        }
        
        isConverting = false
    }
}

For sharing the PDF with the user (email, AirDrop, Files app), wrap the file URL in a UIActivityViewController on iOS or use NSSharingServicePicker on macOS after saving.


What Happens Under the Hood

When your request hits the url-to-pdf endpoint, EnConvert doesn't just take a quick snapshot. Here's the full rendering pipeline:

1. A real browser opens your URL — Powered by Playwright, a full Chromium instance navigates to the page and executes all JavaScript, just like a real visitor.

2. Cookie banners are dismissed — The system detects consent popups from major providers like OneTrust, Cookiebot, Usercentrics, and Didomi. It finds and clicks the accept or dismiss button so the banner doesn't appear in your PDF. It even checks inside iframes for embedded consent managers.

3. The page is scrolled top to bottom — In 120-pixel increments, the system scrolls through the entire page. This triggers lazy-loaded content, deferred images, and scroll-activated animations that would otherwise be missing from the output.

4. Images are verified — Every <img> tag is checked to confirm the image has fully loaded. The system waits up to 5 seconds per image, with a layout stabilization pause to prevent content shift.

5. Sticky headers are handled — Fixed-position navigation bars, sticky headers, and position: sticky elements are detected. The page scrolls back to the top so these elements render correctly in the final PDF without repeating on every page.

6. Viewport-relative units are normalized — CSS units like vh, svh, and dvh are converted to fixed pixel values so the PDF layout matches what you'd see in a real browser window.

7. The PDF is rendered and stored — The final PDF is generated, uploaded to secure cloud storage, and returned to you — either as a direct binary download or as a presigned URL.

All of this happens automatically. You don't configure any of it.


Handling Edge Cases

Long-Running Conversions

Complex pages with heavy JavaScript or many images can take over 60 seconds. If you're calling the API from an environment with proxy timeouts (like many cloud platforms), pass a job_id in your request body and poll for the result:

// Kotlin example with polling fallback
val jobId = UUID.randomUUID().toString()
val requestBody = """{"url": "$url", "job_id": "$jobId"}"""

// If the request times out or returns 504...
// Poll GET /v1/convert/status/{jobId} every 5 seconds

The status endpoint returns "processing", "success" (with a presigned_url), or "failed" (with an error message).

Batch Conversions

Need to convert multiple URLs at once? The API supports batch processing — send an array of URLs and receive a ZIP file with all the PDFs, or get them processed asynchronously with webhook notifications when the batch completes.

Webhook Notifications

For asynchronous workflows, provide a callback_url in your request. When the conversion finishes, EnConvert sends a POST request to your URL with the job status, file metadata, and download links — no polling required.


Private Key vs. Public Key: Which Should You Use?

Use a Private Key when:

  • You're calling the API from a backend server (Kotlin, Java, Swift server-side, Node.js server)

  • The key stays on your server and is never exposed to users

  • You want direct access without CAPTCHA verification

Use a Public Key when:

  • You're calling the API from a browser (React, Vue, Angular, vanilla JavaScript)

  • The key is visible in your frontend source code

  • You set allowed domains so the key only works from your website

  • You select specific endpoints the key can access (like only url-to-pdf)

  • Cloudflare Turnstile runs during token exchange to block bots

Public keys are designed to be safe even when visible. They're locked to your domains, restricted to your chosen endpoints, and require CAPTCHA verification before issuing a JWT. Even if someone copies the key, they can't use it from an unauthorized domain.


Try It Before You Build

Not ready to write code yet? EnConvert has an interactive Playground where you can test URL-to-PDF and all other conversion endpoints right in your browser. Enter a URL, hit convert, and see the result — including a live PDF preview. No account or API key needed.

You can also create an embeddable widget from your dashboard — just pick url-to-pdf as the endpoint, set your domain, and paste the generated iframe code into any HTML page. Your users get a working converter on your site without you writing any integration code.


Getting Started

  1. Sign up for free at enconvert.com

  2. Create a project and generate your API key

  3. Copy one of the code examples above

  4. Replace the API key placeholder with your real key

  5. Run it

Your first URL-to-PDF conversion is one request away.