---
seo_title: API URL a PDF | Convierte Páginas Web a PDF | EnConvert
meta_desc: Convierte cualquier URL a PDF con POST /v1/convert/url-to-pdf. Gestiona carga diferida, cookies y auth; modos síncrono y asíncrono devuelven URL prefirmada o bytes.
keywords: convertir url a pdf api, api para convertir pagina web a pdf, html a pdf api rest, alternativa a puppeteer para pdf, guardar pagina web como pdf api, generar pdf de pagina completa api, conversion masiva de urls a pdf, capturar sitio web en pdf api
---

# API de URL a PDF

El endpoint `POST /v1/convert/url-to-pdf` convierte cualquier URL de acceso público en un documento PDF de alta fidelidad. Admite renderizado continuo de una sola página o salida paginada con tamaños de página personalizados, además de gestión de carga diferida, cierre de banners de cookies, HTTP Basic Auth, inyección de cookies y encabezados personalizados. Ejecuta las conversiones de forma síncrona para obtener una URL de descarga prefirmada o los bytes del PDF en bruto, o usa el modo asíncrono para convertir varias URLs por lotes con notificaciones por webhook y correo electrónico.

---

## Endpoint

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

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

---

## Autenticación

Este endpoint admite autenticación tanto con clave privada como con clave pública.

### Clave privada

Incluye tu clave secreta en el encabezado `X-API-Key`. Usa este método para llamadas servidor a servidor donde la clave nunca se expone al cliente.

```
X-API-Key: sk_your_private_key
```

### Clave pública con JWT

Para uso del lado del cliente, primero genera un token JWT con tu clave pública y luego pásalo como token Bearer.

**Paso 1 -- Obtén un token:**

```
POST /v1/auth/token
X-API-Key: pk_your_public_key
```

**Paso 2 -- Usa el token:**

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

<div class="alert alert-info">
<strong>Nota:</strong> Las solicitudes con clave pública están restringidas a una sola URL, modo síncrono y descarga directa. El modo asíncrono, el procesamiento por lotes, los webhooks y los correos de notificación no están disponibles con claves públicas.
</div>

---

## Parámetros de la solicitud

### Parámetros de nivel superior

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción | Restricción de plan |
|-----------|------|----------|---------|-------------|-------------|
| `url` | `string` or `string[]` | Sí | -- | Una cadena de URL única o un array de URLs para convertir. Varias URLs requieren el modo asíncrono. | -- |
| `async_mode` | `boolean` | No | `false` | Ejecuta la conversión de forma asíncrona. Devuelve un `batch_id` inmediatamente para sondeo (polling). Obligatorio para lotes (varias URLs). | Requiere acceso asíncrono |
| `direct_download` | `boolean` | No | `false` | Devuelve los bytes del PDF en bruto en el cuerpo de la respuesta en lugar de una respuesta JSON con una URL prefirmada. Se fuerza a `true` para claves públicas. Incompatible con `async_mode` y varias URLs. | -- |
| `output_format` | `boolean` | No | `false` | Cuando es `true` con varias URLs, agrupa todos los PDFs de salida en un único archivo ZIP. Requiere varias URLs. | Requiere acceso a salida ZIP |
| `output_filename` | `string` | No | Generado automáticamente | Nombre de archivo personalizado para el archivo de salida. La extensión `.pdf` se añade automáticamente. Formato predeterminado: `{domain}_{timestamp}.pdf`. | -- |
| `job_id` | `string` | No | -- | ID de trabajo proporcionado por el cliente para la recuperación tras timeout. **Solo claves públicas.** Cuando una conversión síncrona supera los límites de timeout del proxy inverso (60-120s en sitios pesados), el cliente puede sondear `GET /v1/convert/status/{job_id}` para recuperar el resultado después. Se ignora para claves privadas. | -- |
| `notification_email` | `string` | No | Correo del propietario del proyecto | Dirección de correo a notificar cuando finaliza un trabajo asíncrono. Solo claves privadas. | -- |
| `callback_url` | `string` | No | -- | URL de webhook que recibe una solicitud POST cuando finaliza la conversión. Solo claves privadas. | Requiere acceso a webhooks |

### Parámetros de navegador y renderizado

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción | Restricción de plan |
|-----------|------|----------|---------|-------------|-------------|
| `viewport_width` | `integer` | No | `1920` | Ancho del viewport del navegador en píxeles. | -- |
| `viewport_height` | `integer` | No | `1080` | Alto del viewport del navegador en píxeles. | -- |
| `single_page` | `boolean` | No | `true` | `true` renderiza toda la página como una única página continua de PDF. `false` produce una salida paginada usando el tamaño de página de `pdf_options`. | -- |
| `load_media` | `boolean` | No | `true` | Espera a que todas las imágenes y vídeos carguen por completo antes de la conversión. Cuando es `false`, la conversión es más rápida pero el contenido multimedia puede aparecer como marcador de posición. | -- |
| `enable_scroll` | `boolean` | No | `true` | Desplaza la página de arriba a abajo para activar el contenido de carga diferida (cargadores basados en IntersectionObserver). | -- |
| `handle_sticky_header` | `boolean` | No | `true` | Detecta encabezados sticky/fijos y desplaza hacia arriba antes de la captura para que el encabezado se renderice correctamente al inicio del PDF. | -- |
| `handle_cookies` | `boolean` | No | `true` | Cierra automáticamente los banners de consentimiento de cookies (OneTrust, Cookiebot, Didomi, Usercentrics y banners genéricos). | -- |
| `wait_for_images` | `boolean` | No | `true` | Espera a que todos los elementos `<img>` terminen de cargar (timeout de 5 segundos por imagen). | -- |
| `wait_for_selector` | `string` | No | `null` | Selector CSS que se espera antes de la captura. Devuelve `422` si nunca aparece dentro de `wait_for_selector_timeout`. Útil para SPAs que hidratan el contenido después de la carga. | -- |
| `wait_for_selector_timeout` | `integer` | No | `10000` | Milisegundos de espera para `wait_for_selector` (máximo `60000`). | -- |
| `block_ads` | `boolean` | No | `false` | Aborta las solicitudes a dominios conocidos de anuncios/rastreadores para que nunca se carguen, rendericen ni ralenticen la captura. | -- |
| `block_media` | `boolean` | No | `false` | Aborta por completo las solicitudes de imágenes y audio/vídeo para un renderizado más rápido y ligero. A diferencia de `load_media` (que solo controla la espera), esto evita por completo que los medios se descarguen. | -- |

### Autenticación y solicitudes personalizadas

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción | Restricción de plan |
|-----------|------|----------|---------|-------------|-------------|
| `auth` | `object` | No | `null` | Credenciales de HTTP Basic Auth para la URL de destino. Formato: `{"username": "...", "password": "..."}`. No se puede usar junto con un encabezado personalizado `Authorization`. | Requiere acceso a autenticación básica |
| `cookies` | `array` | No | `null` | Array de objetos cookie para inyectar antes de la navegación. Máximo 50 cookies. Cada cookie debe tener `name`, `value` y `domain` o `url`. | Requiere acceso a autenticación básica |
| `headers` | `object` | No | `null` | Diccionario de encabezados HTTP personalizados enviados con cada solicitud a la URL de destino. Máximo 20 encabezados. Encabezados bloqueados: `host`, `content-length`, `transfer-encoding`, `connection`, `upgrade`, `te`, `trailer`. | Requiere acceso a autenticación básica |

### Opciones de PDF

Pasa estos parámetros dentro de un objeto `pdf_options` en el cuerpo de la solicitud.

| Parámetro | Tipo | Predeterminado | Descripción |
|-----------|------|---------|-------------|
| `page_size` | `string` | `"A4"` | Tamaño de página con nombre. Se ignora cuando se establecen tanto `page_width` como `page_height`. |
| `page_width` | `float` | `null` | Ancho de página personalizado en milímetros. Debe ser positivo. `page_width` y `page_height` deben establecerse juntos. |
| `page_height` | `float` | `null` | Alto de página personalizado en milímetros. Debe ser positivo. Ambos deben establecerse juntos. |
| `orientation` | `string` | `"portrait"` | `"portrait"` o `"landscape"`. Intercambia ancho y alto cuando se establece en landscape. |
| `margins` | `object` | `{"top": 10, "bottom": 10, "left": 10, "right": 10}` | Márgenes de página en milímetros. Todos los valores deben ser no negativos. |
| `scale` | `float` | `1.0` | Factor de escala del contenido. Rango: `0.1` a `2.0`. Se aplica solo en modo paginado (`single_page=false`). |
| `grayscale` | `boolean` | `false` | Convierte el PDF de salida a escala de grises mediante posprocesamiento. |
| `header` | `object` | `null` | Encabezado de página para el modo paginado. Formato: `{"content": "<html>", "height": 15}`. Contenido máximo de 2000 caracteres. Altura en mm. |
| `footer` | `object` | `null` | Pie de página para el modo paginado. Mismo formato que el encabezado. |

**Tamaños de página admitidos:** `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `B0`, `B1`, `B2`, `B3`, `B4`, `B5`, `Letter`, `Legal`, `Tabloid`, `Ledger`

**Variables de plantilla de encabezado/pie:** `{{page}}`, `{{total_pages}}`, `{{date}}`, `{{title}}`, `{{url}}`

<div class="alert alert-info">
<strong>Nota:</strong> Los encabezados y pies de página solo se renderizan en modo paginado (<code>single_page=false</code>). No tienen efecto en el modo continuo de una sola página.
</div>

---

## Esquema del objeto cookie

Cada elemento del array `cookies` debe seguir esta estructura:

| Campo | Tipo | Obligatorio | Predeterminado | Descripción |
|-------|------|----------|---------|-------------|
| `name` | `string` | Sí | -- | Nombre de la cookie. |
| `value` | `string` | Sí | -- | Valor de la cookie. |
| `domain` | `string` | Condicional | -- | Dominio de la cookie. Se debe proporcionar `domain` o `url`. |
| `url` | `string` | Condicional | -- | URL a la que asociar la cookie. Se debe proporcionar `domain` o `url`. |
| `path` | `string` | No | `"/"` | Ruta de la cookie. Por defecto es `"/"` cuando se establece `domain`. |

---

## Respuesta

### Síncrono con descarga directa (`direct_download=true`)

**Clave privada** -- devuelve los bytes del PDF en bruto:

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

**Clave pública** -- devuelve JSON con una URL prefirmada:

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

### Síncrono sin descarga directa (`direct_download=false`)

Disponible solo con claves privadas.

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

### Modo asíncrono

Devuelve inmediatamente un `batch_id` para sondeo.

```
HTTP 202 Accepted
```

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

Cuando `output_format=true` (agrupación en ZIP):

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

### Sondeo de estado del trabajo (solo claves públicas)

El endpoint de sondeo de estado está diseñado para la **recuperación de timeout con clave pública**. Cuando una conversión síncrona tarda más que el timeout del proxy inverso (típicamente 60s), el cliente puede recuperar el resultado sondeando con el `job_id` que proporcionó en la solicitud original.

```
GET /v1/convert/status/{job_id}
Authorization: Bearer <jwt_token>
```

| Estado | Respuesta |
|--------|----------|
| Procesando | `{"status": "processing"}` |
| Éxito | `{"status": "success", "presigned_url": "...", "object_key": "..."}` |
| Fallido | `{"status": "failed", "error": "..."}` |

### Sondeo de estado del lote (solo claves privadas) {: #batch-status-polling-private-keys-only }

Para trabajos por lotes asíncronos, sondea el endpoint de estado del lote con el `batch_id` devuelto en la respuesta 202.

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

**Respuesta:**

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

**Valores de estado del lote:**

| Estado | Significado |
|--------|---------|
| `processing` | Al menos una URL todavía se está convirtiendo |
| `completed` | Todas las URLs se convirtieron correctamente |
| `partial` | Todas las URLs terminaron, pero algunas fallaron |
| `failed` | Todas las URLs fallaron |

Cuando `output_mode` es `"zip"`, se proporciona un único `zip_download_url` en lugar de valores `download_url` por elemento.

### Payload del webhook callback

Cuando se proporciona un `callback_url`, EnConvert envía una solicitud POST a esa URL al finalizar.

**Trabajo de una sola URL:**

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

**Trabajo por lotes:**

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

---

## Funcionalidades

### Modo de captura limpia

EnConvert gestiona automáticamente los obstáculos comunes de las páginas web para producir PDFs limpios:

- **Banners de consentimiento de cookies** -- Cierra automáticamente los banners de OneTrust, Cookiebot, Didomi, Usercentrics e implementaciones genéricas. Funciona tanto en la página principal como en los iframes. Usa una estrategia de cerrar primero y aceptar después.
- **Cierre de modales y popups** -- Cierra superposiciones usando varias estrategias: tecla Escape, botones de cierre ARIA, botones de cierre basados en clases y botones de diálogo basados en roles.
- **Revelado de animaciones por scroll** -- Fuerza la visibilidad de elementos ocultos por bibliotecas de animación activadas por scroll, incluidas WOW.js, AOS, ScrollReveal y GSAP ScrollTrigger.
- **Limpieza de menús desplegables** -- Cierra todos los menús desplegables abiertos y convierte los elementos de botón de navegación en enlaces ancla reales para que sigan siendo clicables en el PDF.

### Tamaño y dimensiones de página

- **Modo de una sola página** (predeterminado): toda la página web se renderiza como una única página continua de PDF. La altura se calcula dinámicamente a partir del contenido real mediante recorrido del DOM.
- **Modo paginado** (`single_page=false`): la salida usa el `page_size`, `orientation` y `margins` configurados. Admite 18 tamaños con nombre, de A0 a Ledger, o dimensiones personalizadas en milímetros.

### HTTP Basic Auth

Pasa `auth` con `username` y `password` para convertir páginas protegidas con HTTP Basic Authentication. Las credenciales se envían como credenciales HTTP en cada solicitud a la página de destino.

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

### Inyección de cookies

Inyecta hasta 50 cookies antes de que se cargue la página. Útil para convertir páginas que requieren una sesión activa o preferencias de usuario específicas.

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

### Encabezados personalizados

Envía hasta 20 encabezados HTTP personalizados con cada solicitud a la página de destino. Útil para pasar tokens de API, user agents personalizados u otros metadatos de la solicitud.

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

### Carga diferida de imágenes

Cuando `load_media` y `enable_scroll` están activados (ambos son `true` por defecto), el conversor:

1. Desplaza toda la página lentamente (120px cada 90ms) para activar los cargadores diferidos basados en IntersectionObserver
2. Espera a que todos los elementos `<img>` disparen su evento `onload` (timeout de 5 segundos por imagen)
3. Espera 500ms para la estabilización del layout después de que carguen todas las imágenes

Establece `load_media=false` para una conversión más rápida si la fidelidad del contenido multimedia no es crítica -- el conversor usará un desplazamiento rápido (300px cada 30ms) y añadirá estilos de marcador de posición para las imágenes no cargadas.

### Gestión de encabezados sticky

Cuando `handle_sticky_header` está activado (`true` por defecto), el conversor detecta elementos con posición fixed y sticky que parecen ser encabezados (usando etiquetas semánticas, roles ARIA y patrones comunes de nombres de clase), y luego desplaza hasta la parte superior de la página antes de la captura para que el encabezado se renderice correctamente al inicio del PDF.

### Encabezados y pies de página

Añade encabezados y pies de página repetidos en modo paginado con contenido HTML y variables de plantilla:

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

### Salida en escala de grises

Establece `pdf_options.grayscale` en `true` para convertir el PDF final a escala de grises mediante posprocesamiento con Ghostscript.

### Funcionalidades adicionales de renderizado

- **Normalización de unidades de viewport** -- Convierte las unidades CSS `vh`, `svh`, `lvh`, `dvh` a valores fijos en píxeles para evitar problemas de layout en el renderizado de impresión.
- **Modo stealth** -- Usa enmascaramiento de huella digital del navegador para evitar la detección de bots en páginas protegidas.
- **Interceptación de popups** -- Cierra automáticamente cualquier pestaña o popup nuevo del navegador activado por la página.
- **Bypass de CSP** -- Gestiona las restricciones de Content Security Policy y Trusted Types que de otro modo bloquearían la conversión.

---

## Restricciones por plan de suscripción

| Funcionalidad | Founding | Indie | Studio | Enterprise |
|---------|------|---------|-----|------------|
| Conversión básica (una URL, síncrona) | Sí | Sí | Sí | Sí |
| `pdf_options` personalizado | Sí | Sí | Sí | Sí |
| Opciones de viewport y renderizado | Sí | Sí | Sí | Sí |
| Modo asíncrono | No | Sí | Sí | Sí |
| Procesamiento por lotes (varias URLs) | No | Sí | Sí | Sí |
| Agrupación de salida en ZIP | No | No | Sí | Sí |
| Callbacks de webhook | No | No | Sí | Sí |
| HTTP Basic Auth | No | Sí | Sí | Sí |
| Inyección de cookies | No | Sí | Sí | Sí |
| Encabezados personalizados | No | Sí | Sí | Sí |
| Conversiones mensuales | 100 | Según el plan | Según el plan | Ilimitado |
| Límite de tamaño de lote | 0 | Según el plan | Según el plan | Ilimitado |
| Retención de archivos | 1 hora | Según el plan | Según el plan | Según el plan |

---

## Modo asíncrono

El modo asíncrono es útil para conversiones de larga duración o al convertir varias URLs.

### Cómo funciona

1. Envía una solicitud con `async_mode=true` (o pasa varias URLs, lo que activa el modo asíncrono automáticamente).
2. La API devuelve HTTP 202 inmediatamente con un `batch_id` y `url_count`.
3. Cada URL se convierte en segundo plano, se sube al almacenamiento y se rastrea individualmente.
4. Supervisa la finalización mediante **notificación por correo** o **callback de webhook**.

### Notificación por correo

Por defecto, se envía un correo de finalización a la dirección de correo del propietario del proyecto cuando termina el trabajo asíncrono. Puedes anular esto con `notification_email`:

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

### Callback de webhook

Proporciona un `callback_url` en la solicitud para recibir una notificación POST automática cuando finalice el trabajo:

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

El webhook se envía con un timeout de 30 segundos y considera HTTP 200, 201, 202 y 204 como entrega exitosa.

---

## Procesamiento por lotes y en masa

Convierte varias URLs en una sola solicitud. Requiere modo asíncrono y una clave privada.

### Salida individual (predeterminado)

Cada URL produce un archivo PDF independiente:

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

### Salida en paquete ZIP

Agrupa todos los PDFs en un único archivo ZIP:

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

El archivo ZIP se nombra `{output_filename}_{timestamp}.zip`, o `batch_{timestamp}.zip` si no se proporciona un nombre personalizado.

---

## Ejemplos de código

### Python (clave privada)

```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 (clave privada)

```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 (clave privada)

```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 (clave privada)

```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 -- Navegador (clave pública)

```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 (clave pública)

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

---

## Respuestas de error

| Estado | Condición |
|--------|-----------|
| `400 Bad Request` | Falta el parámetro `url` o está vacío |
| `400 Bad Request` | `output_format=true` con una sola URL (requiere varias URLs) |
| `400 Bad Request` | `direct_download=true` con varias URLs |
| `400 Bad Request` | `direct_download=true` con `async_mode=true` |
| `400 Bad Request` | Objeto `auth` inválido (falta `username` o `password`) |
| `400 Bad Request` | `cookies` inválido (no es un array, supera las 50 entradas, faltan campos obligatorios) |
| `400 Bad Request` | `headers` inválido (no es un objeto, supera las 20 entradas, nombres de encabezado bloqueados, valores que no son cadenas) |
| `400 Bad Request` | Conflicto entre `auth` y el encabezado personalizado `Authorization` |
| `400 Bad Request` | Clave pública intentando usar varias URLs |
| `400 Bad Request` | `pdf_options` inválido (tamaño de página no reconocido, escala fuera del rango 0.1-2.0, márgenes negativos, contenido de encabezado/pie que supera los 2000 caracteres) |
| `401 Unauthorized` | Falta la clave de API / token JWT o es inválido |
| `402 Payment Required` | Se agotó la cuota mensual de ops |
| `402 Payment Required` | El lote superaría la cuota mensual de ops restante |
| `402 Payment Required` | Se alcanzó el límite de almacenamiento |
| `403 Forbidden` | El endpoint no está entre los endpoints permitidos de la clave de API |
| `403 Forbidden` | Funcionalidad no disponible en el plan actual (asíncrono, webhook, ZIP, autenticación básica) |
| `403 Forbidden` | El tamaño del lote supera el límite de lote del plan |
| `404 Not Found` | ID de trabajo no encontrado (al sondear el estado) |
| `500 Internal Server Error` | La conversión falló (fallo del navegador, error de renderizado, fallo de posprocesamiento) |

---

## Límites

| Límite | Valor |
|-------|-------|
| Timeout de navegación de página | 60 segundos |
| Timeout de carga por imagen | 5 segundos |
| Timeout de cierre de banner de cookies | 3 segundos |
| Máximo de cookies por solicitud | 50 |
| Máximo de encabezados personalizados por solicitud | 20 |
| Longitud del contenido de encabezado/pie | 2000 caracteres |
| Rango de escala del PDF | 0.1 -- 2.0 |
| Operaciones mensuales | Según el plan (Founding: 500) |
| Tamaño de lote | Según el plan (Founding: deshabilitado) |
| Retención de archivos | Según el plan (Founding: 1 hora) |
| Timeout de entrega de webhook | 30 segundos |

---

## Preguntas frecuentes

### ¿Cómo convierto una página web a PDF con una API REST?

Envía una solicitud `POST` a `/v1/convert/url-to-pdf` con un cuerpo JSON que contenga la `url` a convertir, autenticándote con tu clave privada en el encabezado `X-API-Key` (o un token Bearer JWT de una clave pública). La respuesta síncrona devuelve un `presigned_url` para descargar el PDF, o los bytes del PDF en bruto cuando `direct_download=true`.

### ¿Puedo convertir varias URLs a PDF en una sola solicitud de API?

Sí. Pasa un array de URLs en el parámetro `url` con `async_mode=true` (se requiere una clave privada); la API devuelve `HTTP 202` con un `batch_id` que sondeas mediante `GET /v1/convert/batch/{batch_id}`. Establece `output_format=true` para agrupar todos los PDFs en un único archivo ZIP.

### ¿Cómo capturo una página web completa como una única página continua de PDF?

El modo de una sola página es el predeterminado (`single_page=true`): toda la página se renderiza como una única página continua de PDF cuya altura se calcula a partir del contenido real. Establece `single_page=false` para obtener una salida paginada con `page_size`, `orientation`, `margins` y encabezados y pies de página opcionales mediante `pdf_options`.

### ¿Por qué mi PDF muestra banners de cookies o imágenes faltantes?

El cierre de banners de cookies (`handle_cookies`) y la gestión de carga diferida (`enable_scroll`, `load_media`, `wait_for_images`) son todos `true` por defecto, y cubren OneTrust, Cookiebot, Didomi, Usercentrics y banners genéricos. Si estableces `load_media=false`, la conversión es más rápida pero el contenido multimedia puede aparecer como marcador de posición.

### ¿Puedo convertir a PDF una página protegida por un login?

Sí. Usa el parámetro `auth` para HTTP Basic Auth, inyecta hasta 50 cookies de sesión con `cookies`, o envía hasta 20 encabezados HTTP personalizados con `headers`. Estas opciones requieren acceso a autenticación básica en tu plan, y `auth` no se puede combinar con un encabezado personalizado `Authorization`.
