url-to-pdf

Convert any publicly accessible URL into a high-fidelity PDF document. Supports single-page continuous rendering, paginated output with custom page sizes, lazy-load handling, cookie banner dismissal, and more.


Endpoint

POST /v1/convert/url-to-pdf

Content-Type: application/json


Authentication

This endpoint supports both private and public key authentication.

Private Key

Include your secret key in the X-API-Key header. Use this for server-to-server calls where the key is never exposed to the client.

X-API-Key: sk_live_your_private_key

Public Key with JWT

For client-side usage, first generate a JWT token using your public key, then pass it as a Bearer token.

Step 1 -- Get a token:

POST /v1/auth/token
X-API-Key: pk_live_your_public_key

Step 2 -- Use the token:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Note: Public key requests are restricted to a single URL, synchronous mode, and direct download. Async mode, batch processing, webhooks, and notification emails are not available with public keys.

Request Parameters

Top-Level Parameters

Parameter Type Required Default Description Plan Gating
url string or string[] Yes -- A single URL string or an array of URLs to convert. Multiple URLs require async mode. --
async_mode boolean No false Run the conversion asynchronously. Returns a batch_id immediately for polling. Required for batch (multiple URLs). Requires async access
direct_download boolean No false Return raw PDF bytes in the response body instead of a JSON response with a presigned URL. Forced true for public keys. Incompatible with async_mode and multiple URLs. --
output_format boolean No false When true with multiple URLs, bundles all output PDFs into a single ZIP archive. Requires multiple URLs. Requires ZIP output access
output_filename string No Auto-generated Custom filename for the output file. The .pdf extension is added automatically. Default format: {domain}_{timestamp}.pdf. --
job_id string No -- Client-provided job ID for timeout recovery. Public keys only. When a sync conversion exceeds reverse-proxy timeout limits (60-120s on heavy sites), the client can poll GET /v1/convert/status/{job_id} to retrieve the result after the fact. Ignored for private keys. --
notification_email string No Project owner email Email address to notify when an async job completes. Private keys only. --
callback_url string No -- Webhook URL to receive a POST request when the conversion completes. Private keys only. Requires webhook access

Browser & Rendering Parameters

Parameter Type Required Default Description Plan Gating
viewport_width integer No 1920 Browser viewport width in pixels. --
viewport_height integer No 1080 Browser viewport height in pixels. --
single_page boolean No true true renders the entire page as one continuous PDF page. false produces paginated output using pdf_options page size. --
load_media boolean No true Wait for all images and videos to fully load before conversion. When false, conversion is faster but media may appear as placeholders. --
enable_scroll boolean No true Scroll the page top-to-bottom to trigger lazy-loading content (IntersectionObserver-based loaders). --
handle_sticky_header boolean No true Detect sticky/fixed headers and scroll to top before capture so the header renders correctly at the start of the PDF. --
handle_cookies boolean No true Auto-dismiss cookie consent banners (OneTrust, Cookiebot, Didomi, Usercentrics, and generic banners). --
wait_for_images boolean No true Wait for all <img> elements to finish loading (5-second timeout per image). --

Authentication & Custom Requests

Parameter Type Required Default Description Plan Gating
auth object No null HTTP Basic Auth credentials for the target URL. Format: {"username": "...", "password": "..."}. Cannot be used together with an Authorization custom header. Requires basic auth access
cookies array No null Array of cookie objects to inject before navigation. Maximum 50 cookies. Each cookie must have name, value, and either domain or url. Requires basic auth access
headers object No null Dictionary of custom HTTP headers sent with every request to the target URL. Maximum 20 headers. Blocked headers: host, content-length, transfer-encoding, connection, upgrade, te, trailer. Requires basic auth access

PDF Options

Pass these inside a pdf_options object in the request body.

Parameter Type Default Description
page_size string "A4" Named page size. Ignored when both page_width and page_height are set.
page_width float null Custom page width in millimeters. Must be positive. Both page_width and page_height must be set together.
page_height float null Custom page height in millimeters. Must be positive. Both must be set together.
orientation string "portrait" "portrait" or "landscape". Swaps width and height when set to landscape.
margins object {"top": 10, "bottom": 10, "left": 10, "right": 10} Page margins in millimeters. All values must be non-negative.
scale float 1.0 Content scale factor. Range: 0.1 to 2.0. Applied in paginated mode only (single_page=false).
grayscale boolean false Convert the PDF output to grayscale via post-processing.
header object null Page header for paginated mode. Format: {"content": "<html>", "height": 15}. Content max 2000 characters. Height in mm.
footer object null Page footer for paginated mode. Same format as header.

Supported page sizes: A0, A1, A2, A3, A4, A5, A6, B0, B1, B2, B3, B4, B5, Letter, Legal, Tabloid, Ledger

Header/footer template variables: {{page}}, {{total_pages}}, {{date}}, {{title}}, {{url}}

Note: Headers and footers are only rendered in paginated mode (single_page=false). They have no effect in single-page continuous mode.

Each item in the cookies array must follow this structure:

Field Type Required Default Description
name string Yes -- Cookie name.
value string Yes -- Cookie value.
domain string Conditional -- Cookie domain. Either domain or url must be provided.
url string Conditional -- URL to associate the cookie with. Either domain or url must be provided.
path string No "/" Cookie path. Defaults to "/" when domain is set.

Response

Synchronous with Direct Download (direct_download=true)

Private key -- returns raw PDF bytes:

HTTP 200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="example_20260404_123456789.pdf"
X-Object-Key: env/files/{project_id}/url-to-pdf/example_20260404_123456789.pdf
X-File-Size: 123456
X-Conversion-Time: 12.5
X-Filename: example_20260404_123456789.pdf

(binary PDF data)

Public key -- returns JSON with a presigned URL:

{
    "presigned_url": "https://spaces.example.com/...",
    "object_key": "env/files/{project_id}/url-to-pdf/example_20260404_123456789.pdf",
    "filename": "example_20260404_123456789.pdf",
    "file_size": 123456,
    "conversion_time_seconds": 12.5,
    "job_id": "client-provided-id"
}

Synchronous without Direct Download (direct_download=false)

Available with private keys only.

{
    "presigned_url": "https://spaces.example.com/...",
    "object_key": "env/files/{project_id}/url-to-pdf/example_20260404_123456789.pdf",
    "filename": "example_20260404_123456789.pdf",
    "file_size": 123456,
    "conversion_time_seconds": 12.5
}

Asynchronous Mode

Returns immediately with a batch_id for polling.

HTTP 202 Accepted
{
    "status": "processing",
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "url_count": 5,
    "output_format": "individual"
}

When output_format=true (ZIP bundling):

{
    "status": "processing",
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "url_count": 5,
    "output_format": "zip"
}

Job Status Polling (Public Keys Only)

The status polling endpoint is designed for public key timeout recovery. When a sync conversion takes longer than the reverse-proxy timeout (typically 60s), the client can recover the result by polling with the job_id it provided in the original request.

GET /v1/convert/status/{job_id}
Authorization: Bearer <jwt_token>
Status Response
Processing {"status": "processing"}
Success {"status": "success", "presigned_url": "...", "object_key": "..."}
Failed {"status": "failed", "error": "..."}

Batch Status Polling (Private Keys Only)

For async batch jobs, poll the batch status endpoint with the batch_id returned in the 202 response.

GET /v1/convert/batch/{batch_id}
X-API-Key: sk_live_your_private_key

Response:

{
    "batch_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "processing",
    "total": 5,
    "completed": 3,
    "failed": 1,
    "in_progress": 1,
    "output_mode": "individual",
    "zip_download_url": null,
    "items": [
        {
            "source_url": "https://example.com/page1",
            "status": "Success",
            "download_url": "https://spaces.example.com/...",
            "output_file_size": 102400,
            "duration": "2.34"
        },
        {
            "source_url": "https://example.com/page2",
            "status": "Failed",
            "download_url": null,
            "output_file_size": null,
            "duration": "0.87"
        },
        {
            "source_url": "https://example.com/page3",
            "status": "In Progress",
            "download_url": null,
            "output_file_size": null,
            "duration": null
        }
    ]
}

Batch status values:

Status Meaning
processing At least one URL is still being converted
completed All URLs converted successfully
partial All URLs finished, but some failed
failed All URLs failed

When output_mode is "zip", a single zip_download_url is provided instead of per-item download_url values.

Webhook Callback Payload

When a callback_url is provided, Enconvert sends a POST request to that URL on completion.

Single URL job:

{
    "job_id": "activity_id",
    "status": "success",
    "batch_id": "...",
    "gcs_uri": "object_key",
    "filename": "example_20260404_123456789.pdf",
    "file_size": 123456
}

Batch job:

{
    "job_id": "activity_id",
    "status": "success",
    "batch_id": "...",
    "total_tasks": 10,
    "successful_tasks": 8,
    "failed_tasks": 2,
    "tasks": [
        {"url": "https://example.com/page1", "status": "success", "filename": "page1.pdf"},
        {"url": "https://example.com/page2", "status": "failed", "filename": null}
    ]
}

Features

Clear Capture Mode

Enconvert automatically handles common web page obstacles to produce clean PDFs:

  • Cookie consent banners -- Auto-dismisses banners from OneTrust, Cookiebot, Didomi, Usercentrics, and generic implementations. Operates across the main page and iframes. Uses a close-first-then-accept strategy.
  • Modal and popup dismissal -- Closes overlays using multiple strategies: Escape key, ARIA close buttons, class-based close buttons, and role-based dialog buttons.
  • Scroll animation reveal -- Forces visibility on elements hidden by scroll-triggered animation libraries including WOW.js, AOS, ScrollReveal, and GSAP ScrollTrigger.
  • Dropdown cleanup -- Closes all open dropdowns and converts navigation button elements to real anchor links so they remain clickable in the PDF.

Page Sizing and Dimensions

  • Single-page mode (default): The entire web page is rendered as one continuous PDF page. The height is dynamically calculated from the actual content using DOM traversal.
  • Paginated mode (single_page=false): Output uses the configured page_size, orientation, and margins. Supports 18 named sizes from A0 to Ledger, or custom dimensions in millimeters.

HTTP Basic Auth

Pass auth with username and password to convert pages behind HTTP Basic Authentication. The credentials are sent as HTTP credentials with every request to the target page.

{
    "url": "https://staging.example.com/report",
    "auth": {
        "username": "admin",
        "password": "secret"
    }
}

Inject up to 50 cookies before the page loads. Useful for converting pages that require an active session or specific user preferences.

{
    "url": "https://example.com/dashboard",
    "cookies": [
        {"name": "session_id", "value": "abc123", "domain": "example.com"},
        {"name": "locale", "value": "en-US", "domain": "example.com"}
    ]
}

Custom Headers

Send up to 20 custom HTTP headers with every request to the target page. Useful for passing API tokens, custom user agents, or other request metadata.

{
    "url": "https://example.com/report",
    "headers": {
        "X-Custom-Token": "my-token-value",
        "Accept-Language": "en-US"
    }
}

Lazy Image Loading

When load_media and enable_scroll are enabled (both default to true), the converter:

  1. Scrolls the entire page slowly (120px every 90ms) to trigger IntersectionObserver-based lazy loaders
  2. Waits for all <img> elements to fire their onload event (5-second timeout per image)
  3. Waits 500ms for layout stabilization after all images load

Set load_media=false for faster conversion if media fidelity is not critical -- the converter will use fast scrolling (300px every 30ms) and add placeholder styles for unloaded images.

Sticky Header Handling

When handle_sticky_header is enabled (default true), the converter detects fixed and sticky positioned elements that appear to be headers (using semantic tags, ARIA roles, and common class name patterns), then scrolls to the top of the page before capture so the header renders correctly at the start of the PDF.

Headers and Footers

Add repeating headers and footers in paginated mode with HTML content and template variables:

{
    "url": "https://example.com/report",
    "single_page": false,
    "pdf_options": {
        "page_size": "A4",
        "header": {
            "content": "<div style='font-size:10px;text-align:center;width:100%'>Confidential Report</div>",
            "height": 15
        },
        "footer": {
            "content": "<div style='font-size:9px;text-align:center;width:100%'>Page {{page}} of {{total_pages}}</div>",
            "height": 10
        }
    }
}

Grayscale Output

Set pdf_options.grayscale to true to convert the final PDF to grayscale via Ghostscript post-processing.

Additional Rendering Features

  • Viewport unit normalization -- Converts vh, svh, lvh, dvh CSS units to fixed pixel values to prevent layout issues in print rendering.
  • Stealth mode -- Uses browser fingerprint masking to avoid bot detection on protected pages.
  • Popup interception -- Automatically closes any new browser tabs or popups triggered by the page.
  • CSP bypass -- Handles Content Security Policy and Trusted Types restrictions that would otherwise block conversion.

Subscription Plan Gating

Feature Free Starter Pro Enterprise
Basic conversion (single URL, sync) Yes Yes Yes Yes
Custom pdf_options Yes Yes Yes Yes
Viewport and rendering options Yes Yes Yes Yes
Async mode No Yes Yes Yes
Batch processing (multiple URLs) No Yes Yes Yes
ZIP output bundling No No Yes Yes
Webhook callbacks No No Yes Yes
HTTP Basic Auth No Yes Yes Yes
Cookie injection No Yes Yes Yes
Custom headers No Yes Yes Yes
Monthly conversions 100 Plan-based Plan-based Unlimited
Batch size limit 0 Plan-based Plan-based Unlimited
File retention 1 hour Plan-based Plan-based Plan-based

Async Mode

Asynchronous mode is useful for long-running conversions or when converting multiple URLs.

How It Works

  1. Send a request with async_mode=true (or pass multiple URLs, which enables async automatically).
  2. The API returns HTTP 202 immediately with a batch_id and url_count.
  3. Each URL is converted in the background, uploaded to storage, and tracked individually.
  4. Monitor completion via email notification or webhook callback.

Email Notification

By default, a completion email is sent to the project owner's email address when the async job finishes. You can override this with notification_email:

{
    "url": ["https://example.com/page1", "https://example.com/page2"],
    "async_mode": true,
    "notification_email": "team@example.com"
}

Webhook Callback

Provide a callback_url in the request to receive an automatic POST notification when the job completes:

{
    "url": ["https://example.com/page1", "https://example.com/page2"],
    "async_mode": true,
    "callback_url": "https://your-server.com/webhook/enconvert"
}

The webhook is sent with a 30-second timeout and considers HTTP 200, 201, 202, and 204 as successful delivery.


Batch and Bulk Processing

Convert multiple URLs in a single request. Requires async mode and a private key.

Individual Output (default)

Each URL produces a separate PDF file:

{
    "url": [
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3"
    ],
    "async_mode": true
}

ZIP Bundle Output

Bundle all PDFs into a single ZIP archive:

{
    "url": [
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3"
    ],
    "async_mode": true,
    "output_format": true,
    "output_filename": "monthly-reports"
}

The ZIP file is named {output_filename}_{timestamp}.zip or batch_{timestamp}.zip if no custom name is provided.


Code Examples

Python (Private Key)

import requests

response = requests.post(
    "https://api.enconvert.com/v1/convert/url-to-pdf",
    headers={"X-API-Key": "sk_live_your_private_key"},
    json={
        "url": "https://example.com",
        "single_page": True,
        "pdf_options": {
            "page_size": "A4",
            "margins": {"top": 15, "bottom": 15, "left": 10, "right": 10}
        }
    }
)

data = response.json()
print(data["presigned_url"])

PHP (Private Key)

$ch = curl_init("https://api.enconvert.com/v1/convert/url-to-pdf");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "X-API-Key: sk_live_your_private_key"
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "url" => "https://example.com",
        "single_page" => true,
        "pdf_options" => [
            "page_size" => "A4",
            "margins" => ["top" => 15, "bottom" => 15, "left" => 10, "right" => 10]
        ]
    ])
]);

$response = curl_exec($ch);
curl_close($ch);

$data = json_decode($response, true);
echo $data["presigned_url"];

Node.js (Private Key)

const response = await fetch("https://api.enconvert.com/v1/convert/url-to-pdf", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_live_your_private_key"
    },
    body: JSON.stringify({
        url: "https://example.com",
        single_page: true,
        pdf_options: {
            page_size: "A4",
            margins: { top: 15, bottom: 15, left: 10, right: 10 }
        }
    })
});

const data = await response.json();
console.log(data.presigned_url);

Go (Private Key)

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "io"
)

func main() {
    body, _ := json.Marshal(map[string]interface{}{
        "url":         "https://example.com",
        "single_page": true,
        "pdf_options": map[string]interface{}{
            "page_size": "A4",
            "margins":   map[string]int{"top": 15, "bottom": 15, "left": 10, "right": 10},
        },
    })

    req, _ := http.NewRequest("POST", "https://api.enconvert.com/v1/convert/url-to-pdf", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("X-API-Key", "sk_live_your_private_key")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    respBody, _ := io.ReadAll(resp.Body)
    fmt.Println(string(respBody))
}

JavaScript -- Browser (Public Key)

// Step 1: Get a JWT token
const tokenRes = await fetch("https://api.enconvert.com/v1/auth/token", {
    method: "POST",
    headers: { "X-API-Key": "pk_live_your_public_key" }
});
const { token } = await tokenRes.json();

// Step 2: Convert URL to PDF
const convertRes = await fetch("https://api.enconvert.com/v1/convert/url-to-pdf", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify({
        url: "https://example.com"
    })
});

const data = await convertRes.json();
// Open the PDF in a new tab
window.open(data.presigned_url, "_blank");

React (Public Key)

import { useState } from "react";

function UrlToPdf() {
    const [loading, setLoading] = useState(false);
    const [pdfUrl, setPdfUrl] = useState(null);

    async function convertUrl() {
        setLoading(true);
        try {
            // Get JWT token
            const tokenRes = await fetch("https://api.enconvert.com/v1/auth/token", {
                method: "POST",
                headers: { "X-API-Key": "pk_live_your_public_key" }
            });
            const { token } = await tokenRes.json();

            // Convert
            const convertRes = await fetch("https://api.enconvert.com/v1/convert/url-to-pdf", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${token}`
                },
                body: JSON.stringify({ url: "https://example.com" })
            });

            const data = await convertRes.json();
            setPdfUrl(data.presigned_url);
        } finally {
            setLoading(false);
        }
    }

    return (
        <div>
            <button onClick={convertUrl} disabled={loading}>
                {loading ? "Converting..." : "Convert to PDF"}
            </button>
            {pdfUrl && <a href={pdfUrl} target="_blank" rel="noreferrer">Download PDF</a>}
        </div>
    );
}

export default UrlToPdf;

Error Responses

Status Condition
400 Bad Request Missing or empty url parameter
400 Bad Request output_format=true with a single URL (requires multiple URLs)
400 Bad Request direct_download=true with multiple URLs
400 Bad Request direct_download=true with async_mode=true
400 Bad Request Invalid auth object (missing username or password)
400 Bad Request Invalid cookies (not an array, exceeds 50 entries, missing required fields)
400 Bad Request Invalid headers (not an object, exceeds 20 entries, blocked header names, non-string values)
400 Bad Request Conflicting auth and Authorization custom header
400 Bad Request Public key attempting multiple URLs
400 Bad Request Invalid pdf_options (unrecognized page size, scale out of 0.1-2.0 range, negative margins, header/footer content exceeding 2000 characters)
401 Unauthorized Missing or invalid API key / JWT token
402 Payment Required Monthly conversion limit reached
402 Payment Required Batch would exceed remaining monthly quota
402 Payment Required Storage limit reached
403 Forbidden Endpoint not in the API key's allowed endpoints
403 Forbidden Feature not available on current plan (async, webhook, ZIP, basic auth)
403 Forbidden Batch size exceeds plan's batch limit
404 Not Found Job ID not found (when polling status)
500 Internal Server Error Conversion failed (browser crash, rendering error, post-processing failure)

Limits

Limit Value
Page navigation timeout 60 seconds
Per-image load timeout 5 seconds
Cookie banner dismiss timeout 3 seconds
Maximum cookies per request 50
Maximum custom headers per request 20
Header/footer content length 2000 characters
PDF scale range 0.1 -- 2.0
Monthly conversions Plan-dependent (Free: 100)
Batch size Plan-dependent (Free: disabled)
File retention Plan-dependent (Free: 1 hour)
Webhook delivery timeout 30 seconds