---
seo_title: Website Screenshot API for Full Page URL Capture | EnConvert
meta_desc: Capture full-page website screenshots as PNG via POST /v1/convert/url-to-screenshot. Auto-dismisses cookie banners, loads lazy content, returns presigned URLs.
keywords: website screenshot api, full page screenshot api, url to screenshot api, capture webpage screenshot api, screenshot api png, puppeteer alternative screenshot api, take screenshot of website programmatically, web page capture api
---

# Website Screenshot API

The `POST /v1/convert/url-to-screenshot` endpoint captures a full-page screenshot of any publicly accessible URL as a high-fidelity PNG image. It automatically handles cookie banners, modals, lazy-loaded content, scroll-triggered animations, and sticky headers to produce a clean, accurate capture. Run it synchronously for a presigned download URL or raw PNG bytes, or use async mode to screenshot multiple URLs in a batch.

---

## Endpoint

```
POST /v1/convert/url-to-screenshot
```

**Content-Type:** `application/json`

**Output format:** PNG (always). The output format is not configurable, and all screenshots are captured as full-page PNG images.

---

## 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 capture. Multiple URLs require async mode. | -- |
| `async_mode` | `boolean` | No | `false` | Run the capture asynchronously. Returns a `batch_id` immediately. Required for batch (multiple URLs). | Requires async access |
| `direct_download` | `boolean` | No | `false` | Return raw PNG 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 PNGs 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 `.png` extension is added automatically. Default format: `{domain}_{timestamp}.png`. | -- |
| `job_id` | `string` | No | -- | Client-provided job ID for timeout recovery. **Public keys only.** When a sync conversion exceeds reverse-proxy timeout limits, the client can poll `GET /v1/convert/status/{job_id}` to retrieve the result. 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 capture 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. The screenshot width matches this value. | -- |
| `viewport_height` | `integer` | No | `1080` | Browser viewport height in pixels. Used as a reference for rendering and viewport-unit calculation. The actual screenshot height is determined by the full page content height. | -- |
| `load_media` | `boolean` | No | `true` | Wait for all images and videos to fully load before capture. When `false`, capture 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 top of the screenshot. | -- |
| `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 |

<div class="alert alert-warning">
<strong>Not supported:</strong> The <code>single_page</code> and <code>pdf_options</code> parameters from the <a href="/docs/endpoints/convert/web-pages/url-to-pdf">url-to-pdf</a> endpoint are not applicable to screenshots. Screenshots always capture the full page as a single continuous image.
</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 PNG bytes:

```
HTTP 200 OK
Content-Type: image/png
Content-Disposition: inline; filename="example_20260405_123456789.png"
X-Object-Key: env/files/{project_id}/url-to-screenshot/example_20260405_123456789.png
X-File-Size: 456789
X-Conversion-Time: 8.2
X-Filename: example_20260405_123456789.png

(binary PNG data)
```

**Public key** -- returns JSON with a presigned URL:

```json
{
    "presigned_url": "https://spaces.example.com/...",
    "object_key": "env/files/{project_id}/url-to-screenshot/example_20260405_123456789.png",
    "filename": "example_20260405_123456789.png",
    "file_size": 456789,
    "conversion_time_seconds": 8.2,
    "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-screenshot/example_20260405_123456789.png",
    "filename": "example_20260405_123456789.png",
    "file_size": 456789,
    "conversion_time_seconds": 8.2
}
```

### Asynchronous Mode

Returns immediately with a `batch_id` for tracking.

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

For public key timeout recovery:

```
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 with the `batch_id` from the 202 response:

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

Returns aggregate status, per-URL statuses, and presigned download URLs. See [Batch Status Polling](/docs/endpoints/convert/web-pages/url-to-pdf.md#batch-status-polling-private-keys-only) for the full response schema.

### 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_20260405_123456789.png",
    "file_size": 456789
}
```

**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.png"},
        {"url": "https://example.com/page2", "status": "failed", "filename": null}
    ]
}
```

---

## Features

### Full-Page Capture

Every screenshot captures the **entire page content**, not just the visible viewport. The converter:

1. Renders the page at the specified `viewport_width` and `viewport_height`
2. Scrolls through the page to trigger all lazy-loaded content
3. Calculates the true content height using a DOM tree walker that measures the maximum bottom position of all visible elements
4. Resizes the viewport to encompass the full content height
5. Captures the screenshot with `full_page=true`

The result is a single tall PNG image of the complete page.

### Clear Capture Mode

EnConvert automatically handles common web page obstacles to produce clean screenshots:

- **Cookie consent banners** -- Auto-dismisses banners from OneTrust, Cookiebot, Didomi, Usercentrics, and generic implementations. Operates across the main page and iframes.
- **Modal and popup dismissal** -- Closes overlays using multiple strategies: Escape key, ARIA close buttons, class-based close buttons (`"Close"`, `"Not now"`, `"No thanks"`, `"Skip"`), and role-based dialog buttons. Removes residual blur, backdrop, and inert effects after dismissal.
- **Scroll animation reveal** -- Forces visibility on elements hidden by scroll-triggered animation libraries including WOW.js, AOS, ScrollReveal, GSAP ScrollTrigger, and generic animation classes (`.fadeIn`, `.slideIn`, etc.). Also reveals all Swiper slides.
- **Dropdown cleanup** -- Closes all open dropdowns, converts navigation button elements to real anchor links so they remain visually clean, hides `role="menu"` elements, and repositions fixed headers to static positioning.

### Viewport Unit Normalization

Screenshots require special handling of CSS viewport units (`vh`, `svh`, `lvh`, `dvh`) because the viewport is resized to the full page height. Without normalization, elements sized with viewport units would stretch to enormous sizes. The converter:

- Converts all viewport-relative units to fixed pixel values based on the original viewport height
- Caps abnormally tall images and videos to 1.5x the original viewport height
- Handles Elementor-specific height quirks (flex containers, motion effects, background containers)
- Preserves video dimensions through the normalization process

### HTTP Basic Auth

Pass `auth` with `username` and `password` to capture pages behind HTTP Basic Authentication.

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

### Cookie Injection

Inject up to 50 cookies before the page loads. Useful for capturing pages that require an active session.

```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.

```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 scrolls the page slowly (120px every 90ms) to trigger lazy loaders, then waits for all images to finish loading with a 500ms layout stabilization period.

Set `load_media=false` for faster capture. The converter then uses fast scrolling (300px every 30ms) with a shorter 100ms stabilization, but media may appear as placeholders.

### Sticky Header Handling

When enabled (default `true`), the converter detects fixed and sticky positioned elements that appear to be headers, repositions them to static positioning for a clean screenshot, and scrolls to the top of the page before capture.

### Additional Rendering Features

- **Screen media emulation** -- The page is rendered using `screen` CSS media (not `print`), so the screenshot matches what users see in their browser.
- **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 page manipulation.
- **Shadow preservation** -- Elements with `box-shadow` and `text-shadow` are tagged to ensure shadows render correctly in the screenshot output.

---

## Subscription Plan Gating

| Feature | Founding | Indie | Studio | Enterprise |
|---------|------|---------|-----|------------|
| Basic capture (single URL, sync) | Yes | Yes | Yes | Yes |
| Viewport sizing | 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 captures or when screenshotting 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 captured in the background, uploaded to storage, and tracked individually.
4. Monitor completion via **batch status polling**, **email notification**, or **webhook callback**.

### Email Notification

By default, a completion email is sent to the project owner's email address. Override 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` to receive an automatic POST notification on completion:

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

---

## Batch and Bulk Processing

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

### Individual Output (default)

Each URL produces a separate PNG file:

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

### ZIP Bundle Output

Bundle all screenshots 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-screenshots"
}
```

---

## Code Examples

### Python (Private Key)

```python
import requests

response = requests.post(
    "https://api.enconvert.com/v1/convert/url-to-screenshot",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "url": "https://example.com",
        "viewport_width": 1440,
        "viewport_height": 900
    }
)

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

### PHP (Private Key)

```php
$ch = curl_init("https://api.enconvert.com/v1/convert/url-to-screenshot");
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",
        "viewport_width" => 1440,
        "viewport_height" => 900
    ])
]);

$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-screenshot", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_your_private_key"
    },
    body: JSON.stringify({
        url: "https://example.com",
        viewport_width: 1440,
        viewport_height: 900
    })
});

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

### Go (Private Key)

```go
package main

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

func main() {
    body, _ := json.Marshal(map[string]interface{}{
        "url":             "https://example.com",
        "viewport_width":  1440,
        "viewport_height": 900,
    })

    req, _ := http.NewRequest("POST", "https://api.enconvert.com/v1/convert/url-to-screenshot", 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: Capture screenshot
const convertRes = await fetch("https://api.enconvert.com/v1/convert/url-to-screenshot", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${token}`
    },
    body: JSON.stringify({
        url: "https://example.com"
    })
});

const data = await convertRes.json();
// Display the screenshot
const img = document.createElement("img");
img.src = data.presigned_url;
document.body.appendChild(img);
```

### React (Public Key)

```jsx
import { useState } from "react";

function UrlToScreenshot() {
    const [loading, setLoading] = useState(false);
    const [imageUrl, setImageUrl] = useState(null);

    async function captureScreenshot() {
        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();

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

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

    return (
        <div>
            <button onClick={captureScreenshot} disabled={loading}>
                {loading ? "Capturing..." : "Take Screenshot"}
            </button>
            {imageUrl && <img src={imageUrl} alt="Screenshot" style={{ maxWidth: "100%" }} />}
        </div>
    );
}

export default UrlToScreenshot;
```

---

## 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 |
| `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` | Capture failed (browser crash, rendering error) |

---

## 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 |
| 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 take a full page screenshot of a website with an API?

Send a `POST` request to `/v1/convert/url-to-screenshot` with a JSON body containing the `url`, authenticating with your private key in the `X-API-Key` header (or a JWT Bearer token from a public key). Every capture includes the entire page content, not just the visible viewport, because the converter resizes the viewport to the full content height and captures with `full_page=true`.

### Can I change the screenshot output format to JPEG or WebP?

No. The output format is not configurable, so every screenshot is captured as a full-page PNG image.

### How do I control the width and size of the screenshot?

Set `viewport_width` (default `1920`), and the screenshot width matches this value. The screenshot height is determined by the full page content height, with `viewport_height` (default `1080`) used as a reference for rendering and viewport-unit calculation.

### Can I screenshot a page behind a login?

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.

### How does the API remove cookie banners and popups from screenshots?

With `handle_cookies` enabled (default `true`), the converter auto-dismisses consent banners from OneTrust, Cookiebot, Didomi, Usercentrics, and generic implementations, operating across the main page and iframes. Modals and popups are closed using the Escape key, ARIA close buttons, class-based close buttons, and role-based dialog buttons, with residual blur and backdrop effects removed.
