---
seo_title: URL to PDF API for Converting Web Pages to PDF | EnConvert
meta_desc: Convert any URL to PDF with POST /v1/convert/url-to-pdf. Handles lazy loading, cookie banners, and auth; sync or async modes return presigned URLs or raw bytes.
keywords: url to pdf api, convert webpage to pdf api, website to pdf api, html url to pdf rest api, puppeteer alternative api, save web page as pdf api, full page pdf api, batch url to pdf conversion api
---

# URL to PDF API

The `POST /v1/convert/url-to-pdf` endpoint converts any publicly accessible URL into a high-fidelity PDF document. It supports single-page continuous rendering or paginated output with custom page sizes, plus lazy-load handling, cookie banner dismissal, HTTP Basic Auth, cookie injection, and custom headers. Run conversions synchronously for a presigned download URL or raw PDF bytes, or use async mode to batch-convert multiple URLs with webhook and email notifications.

---

## 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_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_your_public_key
```

**Step 2 -- Use the token:**

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
```

<div class="alert alert-info">
<strong>Note:</strong> 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.
</div>

---

## 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). | -- |
| `wait_for_selector` | `string` | No | `null` | CSS selector to wait for before capture. Returns `422` if it never appears within `wait_for_selector_timeout`. Useful for SPAs that hydrate content after load. | -- |
| `wait_for_selector_timeout` | `integer` | No | `10000` | Milliseconds to wait for `wait_for_selector` (maximum `60000`). | -- |
| `block_ads` | `boolean` | No | `false` | Abort requests to known ad/tracker domains so they never load, render, or slow the capture. | -- |
| `block_media` | `boolean` | No | `false` | Abort image and audio/video requests entirely for a faster, lighter render. Unlike `load_media` (which only controls waiting), this prevents media from downloading at all. | -- |

### 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}}`

<div class="alert alert-info">
<strong>Note:</strong> Headers and footers are only rendered in paginated mode (<code>single_page=false</code>). They have no effect in single-page continuous mode.
</div>

---

## Cookie Object Schema

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:

```json
{
    "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.

```json
{
    "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
```

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

When `output_format=true` (ZIP bundling):

```json
{
    "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_your_private_key
```

**Response:**

```json
{
    "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:**

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

**Batch job:**

```json
{
    "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.

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

### Cookie Injection

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

```json
{
    "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.

```json
{
    "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:

```json
{
    "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 | Founding | Indie | Studio | 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`:

```json
{
    "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:

```json
{
    "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:

```json
{
    "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:

```json
{
    "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)

```python
import requests

response = requests.post(
    "https://api.enconvert.com/v1/convert/url-to-pdf",
    headers={"X-API-Key": "sk_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)

```php
$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_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)

```javascript
const response = await fetch("https://api.enconvert.com/v1/convert/url-to-pdf", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_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)

```go
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_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)

```javascript
// Step 1: Get a JWT token
const tokenRes = await fetch("https://api.enconvert.com/v1/auth/token", {
    method: "POST",
    headers: { "X-API-Key": "pk_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)

```jsx
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_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 ops allowance exhausted |
| `402 Payment Required` | Batch would exceed remaining monthly ops allowance |
| `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 ops | Plan-dependent (Founding: 500) |
| Batch size | Plan-dependent (Founding: disabled) |
| File retention | Plan-dependent (Founding: 1 hour) |
| Webhook delivery timeout | 30 seconds |

---

## Frequently asked questions

### How do I convert a web page to PDF with a REST API?

Send a `POST` request to `/v1/convert/url-to-pdf` with a JSON body containing the `url` to convert, authenticating with your private key in the `X-API-Key` header (or a JWT Bearer token from a public key). The synchronous response returns a `presigned_url` to download the PDF, or raw PDF bytes when `direct_download=true`.

### Can I convert multiple URLs to PDF in one API request?

Yes. Pass an array of URLs in the `url` parameter with `async_mode=true` (a private key is required); the API returns `HTTP 202` with a `batch_id` you poll via `GET /v1/convert/batch/{batch_id}`. Set `output_format=true` to bundle all PDFs into a single ZIP archive.

### How do I capture an entire web page as a single continuous PDF page?

Single-page mode is the default (`single_page=true`): the whole page renders as one continuous PDF page whose height is calculated from the actual content. Set `single_page=false` for paginated output with `page_size`, `orientation`, `margins`, and optional headers and footers via `pdf_options`.

### Why does my PDF show cookie banners or missing images?

Cookie banner dismissal (`handle_cookies`) and lazy-load handling (`enable_scroll`, `load_media`, `wait_for_images`) all default to `true`, covering OneTrust, Cookiebot, Didomi, Usercentrics, and generic banners. If you set `load_media=false`, conversion is faster but media may appear as placeholders.

### Can I convert a page behind a login to PDF?

Yes. Use the `auth` parameter for HTTP Basic Auth, inject up to 50 session cookies with `cookies`, or send up to 20 custom HTTP headers with `headers`. These options require basic auth access on your plan, and `auth` cannot be combined with a custom `Authorization` header.
