---
seo_title: API URL a Markdown | Convierte Páginas Web para LLMs | EnConvert
meta_desc: Convierte URLs a Markdown limpio (GitHub-Flavored) con POST /v1/convert/url-to-markdown. Extracción con Readability y frontmatter YAML para pipelines de ingesta LLM.
keywords: convertir url a markdown api, api para convertir página web a markdown, html a markdown api, extraer contenido de artículo a markdown, url a markdown para pipeline rag, conversión de url a markdown en lote, api de markdown para llm, extracción de artículos con readability api
---

# API de URL a Markdown

El endpoint `POST /v1/convert/url-to-markdown` convierte cualquier página web de acceso público en Markdown GitHub-Flavored limpio con un bloque de metadatos YAML frontmatter. Cada página se renderiza en un navegador real, se pasa por un extractor de legibilidad que elimina el contenido superfluo (navegación, pies de página, barras laterales, scripts, formularios, botones) y luego se serializa a Markdown con enlaces normalizados, bloques de código delimitados y URLs relativas resueltas a absolutas. Es exactamente lo que necesitan los pipelines de ingesta para LLM y RAG en lugar de HTML sin procesar. Las conversiones se ejecutan de forma síncrona o asíncrona en lote, y los resultados se devuelven como bytes Markdown sin procesar o como una URL de descarga prefirmada.

---

## Endpoint

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

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

**Formato de salida:** Markdown (`.md`, UTF-8) con un bloque de metadatos YAML frontmatter al principio del archivo, que contiene los metadatos de la página. El formato de salida no es configurable. Siempre se genera Markdown con YAML frontmatter.

---

## 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`. Úsalo 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 usando tu clave pública y luego pásalo como token Bearer.

**Paso 1 -- Obtener un token:**

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

**Paso 2 -- Usar 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

| Parameter | Type | Required | Default | Description | Plan Gating |
|-----------|------|----------|---------|-------------|-------------|
| `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 consultar el estado (polling). Obligatorio para lotes (varias URLs). | Requiere acceso asíncrono |
| `direct_download` | `boolean` | No | `false` | Devuelve los bytes Markdown sin procesar 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 con varias URLs. | -- |
| `output_format` | `boolean` | No | `false` | Cuando es `true` con varias URLs, agrupa todos los archivos Markdown de salida en un único archivo ZIP. Requiere varias URLs. | Requiere acceso a salida ZIP |
| `output_filename` | `string` | No | Autogenerado | Nombre de archivo personalizado para el archivo de salida. La extensión `.md` se añade automáticamente. Formato predeterminado: `{domain}_{timestamp}.md`. | -- |
| `job_id` | `string` | No | -- | ID de job proporcionado por el cliente para recuperación ante timeout. **Solo claves públicas.** Cuando una conversión síncrona supera los límites de timeout del proxy inverso, el cliente puede consultar `GET /v1/convert/status/{job_id}` para obtener el resultado. Se ignora en claves privadas. | -- |
| `notification_email` | `string` | No | Correo del propietario del proyecto | Dirección de correo electrónico a la que se notifica cuando finaliza un job 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

| Parameter | Type | Required | Default | Description | Plan Gating |
|-----------|------|----------|---------|-------------|-------------|
| `viewport_width` | `integer` | No | `1920` | Ancho del viewport del navegador en píxeles. Afecta al contenido responsive y a qué variante de diseño se captura antes de la extracción. | -- |
| `viewport_height` | `integer` | No | `1080` | Alto del viewport del navegador en píxeles. Se usa como referencia para el renderizado y el cálculo de unidades de viewport. | -- |
| `load_media` | `boolean` | No | `true` | Espera a que todas las imágenes y videos terminen de cargar antes de la extracción. Cuando es `false`, la extracción es más rápida, pero las imágenes con carga diferida (lazy-loaded) pueden tener valores `src` de marcador de posición en la salida Markdown. | -- |
| `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 la página hasta arriba antes de la extracción para preservar correctamente el orden del contenido. | -- |
| `handle_cookies` | `boolean` | No | `true` | Cierra automáticamente los banners de consentimiento de cookies (OneTrust, Cookiebot, Didomi, Usercentrics y banners genéricos) antes de la extracción. | -- |
| `wait_for_images` | `boolean` | No | `true` | Espera a que todos los elementos `<img>` terminen de cargar (timeout de 5 segundos por imagen) para capturar correctamente el texto `alt` y los valores finales de `src`. | -- |
| `wait_for_selector` | `string` | No | `null` | Selector CSS que se espera antes de la extracción. 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 ni ralenticen la extracción. | -- |
| `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

| Parameter | Type | Required | Default | Description | Plan Gating |
|-----------|------|----------|---------|-------------|-------------|
| `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 de cookies 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 que se envían 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 |

<div class="alert alert-warning">
<strong>No admitido:</strong> Los parámetros <code>single_page</code> y <code>pdf_options</code> del endpoint <a href="/es/docs/endpoints/convert/web-pages/url-to-pdf">url-to-pdf</a> se aceptan por paridad en la forma de la solicitud, pero no tienen efecto en la salida Markdown. Markdown no tiene concepto de páginas, márgenes ni orientación.
</div>

---

## Esquema del objeto cookie

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

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `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 con la que asociar la cookie. Se debe proporcionar `domain` o `url`. |
| `path` | `string` | No | `"/"` | Ruta de la cookie. El valor predeterminado es `"/"` cuando se define `domain`. |

---

## Respuesta

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

**Clave privada** -- devuelve los bytes Markdown sin procesar:

```
HTTP 200 OK
Content-Type: text/markdown; charset=utf-8
Content-Disposition: inline; filename="example_20260421_123456789.md"
X-Object-Key: env/files/{project_id}/url-to-markdown/example_20260421_123456789.md
X-File-Size: 8421
X-Conversion-Time: 6.3
X-Filename: example_20260421_123456789.md

(UTF-8 Markdown with YAML frontmatter)
```

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

```json
{
    "presigned_url": "https://spaces.example.com/...",
    "object_key": "env/files/{project_id}/url-to-markdown/example_20260421_123456789.md",
    "filename": "example_20260421_123456789.md",
    "file_size": 8421,
    "conversion_time_seconds": 6.3,
    "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-markdown/example_20260421_123456789.md",
    "filename": "example_20260421_123456789.md",
    "file_size": 8421,
    "conversion_time_seconds": 6.3
}
```

### Modo asíncrono

Devuelve una respuesta inmediata con un `batch_id` para consultar el estado (polling).

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

### Consulta del estado del job (solo claves públicas)

Para recuperación ante timeout con clave pública:

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

| Status | Response |
|--------|----------|
| Procesando | `{"status": "processing"}` |
| Correcto | `{"status": "success", "presigned_url": "...", "object_key": "..."}` |
| Fallido | `{"status": "failed", "error": "..."}` |

### Consulta del estado de lote (solo claves privadas)

Para jobs de lote asíncronos, consulta el estado usando el `batch_id` de la respuesta 202:

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

Devuelve el estado agregado, el estado por cada URL y las URLs de descarga prefirmadas. Consulta [Consulta del estado de lote](/es/docs/endpoints/convert/web-pages/url-to-pdf.md#batch-status-polling-private-keys-only) para ver el esquema completo de la respuesta.

### Payload del callback de webhook

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

**Job de una sola URL:**

```json
{
    "job_id": "activity_id",
    "status": "success",
    "batch_id": "...",
    "gcs_uri": "object_key",
    "filename": "example_20260421_123456789.md",
    "file_size": 8421
}
```

**Job de lote:**

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

---

## Formato de salida

Todo archivo Markdown comienza con un bloque de metadatos YAML frontmatter, seguido del cuerpo del artículo extraído.

```markdown
---
url: https://example.com/articles/my-post
title: My Post Title
description: A short summary from the meta description or og:description tag.
links:
  - url: https://example.com/related
    text: Related article
  - url: https://example.com/about
    text: About the author
images:
  - url: https://example.com/hero.jpg
    alt: Hero image alt text
  - url: https://example.com/diagram.png
    alt: Architecture diagram
---

# My Post Title

Opening paragraph of the article body, converted to GitHub-Flavored Markdown...

## A Section Heading

- List item one
- List item two

\`\`\`python
def example():
    return "code blocks are fenced with language hints"
\`\`\`

[A link in the body](https://example.com/linked-page)

![An image in the body](https://example.com/inline-image.png)
```

### Campos del frontmatter

| Field | Type | Description |
|-------|------|-------------|
| `url` | `string` | La URL final después de redirecciones (no siempre es la URL que enviaste). |
| `title` | `string` | El título de la página desde `<title>`, con reserva al título corto detectado por Readability. |
| `description` | `string` | El valor de `<meta name="description">`, con reserva a `<meta property="og:description">`. |
| `links` | `array` | Todos los `<a href>` encontrados en la página, con URLs absolutas y el texto visible del enlace. |
| `images` | `array` | Todas las `<img src>` encontradas en la página, con URLs absolutas y texto `alt`. |

### Convenciones de Markdown

- **Estilo de encabezado:** ATX (`#`, `##`, `###`)
- **Viñetas de lista:** `-`
- **Énfasis:** `*bold*`, `*italic*` con `*` y `_` escapados en texto literal
- **Saltos de línea suaves:** dos espacios al final (se conservan en la salida)
- **Bloques de código:** delimitados (` ``` `) con pistas de lenguaje detectadas a partir de `class="language-xxx"`, `class="lang-xxx"`, `class="highlight-source-xxx"`, `data-lang` y `data-language`
- **Enlaces:** `[text](url)` cuando hay texto de anclaje, forma de autolink `<url>` cuando el anclaje está vacío; los enlaces solo de anclaje (`#foo`) y `javascript:` se convierten en texto plano
- **Imágenes:** `![alt](src)`, conservando `title` cuando está presente, con reserva a `data-src` cuando falta `src` (imágenes de carga diferida)
- **Reglas horizontales:** `---`

---

## Funcionalidades

### Extracción de contenido limpio

EnConvert usa el algoritmo Readability (la misma biblioteca que impulsa Firefox Reader View) para aislar el contenido principal del artículo del resto de la página, y luego aplica una segunda pasada de posprocesamiento para producir Markdown limpio.

**Eliminado antes de la conversión:**

- Navegación (`<nav>`), pies de página (`<footer>`), barras laterales (`<aside>`)
- Scripts (`<script>`, `<noscript>`), estilos (`<style>`), iframes, formularios, botones
- SVG en línea, canvas y elementos template
- Los atributos `style`, `class`, `id` y todos los manejadores de eventos `on*`

**Conservado:**

- Encabezados, párrafos, listas, tablas, citas en bloque, bloques de código
- Enlaces con su `href` y texto de anclaje (URLs absolutas)
- Imágenes con `alt`, `title` y `src` absoluto
- Figures y figcaptions (las imágenes en línea se mantienen dentro de estos)

### Modo de captura limpia

Antes de la extracción, la página se renderiza en un navegador real y se limpia de la misma forma que en [url-to-pdf](/es/docs/endpoints/convert/web-pages/url-to-pdf.md):

- **Banners de consentimiento de cookies** -- Se cierran automáticamente en la página principal y en los iframes (OneTrust, Cookiebot, Didomi, Usercentrics y banners genéricos).
- **Cierre de modales y popups** -- Los overlays se cierran mediante la tecla Escape, botones de cierre ARIA, botones de cierre basados en clases y botones de diálogo basados en roles.
- **Revelado de animaciones de scroll** -- Fuerza la visibilidad de elementos ocultos por WOW.js, AOS, ScrollReveal, GSAP ScrollTrigger y clases de animación genéricas.
- **Gestión de encabezados sticky** -- Se detectan los encabezados sticky/fijos y la página se desplaza de nuevo hasta arriba para preservar el orden del contenido.

### Resolución de URLs absolutas

Todos los `href` y `src` relativos del artículo extraído se resuelven contra la URL final de la página (después de redirecciones), de modo que la salida Markdown siempre contiene enlaces absolutos y clicables, algo útil para los pipelines de ingesta LLM que, de lo contrario, verían rutas relativas rotas.

Los enlaces solo de anclaje (`#section`), los enlaces `javascript:`, `mailto:` y `tel:` no se reescriben. Los enlaces solo de anclaje y `javascript:` se convierten en texto plano porque no tienen significado fuera de la página original.

### Detección del lenguaje de bloques de código

Los bloques de código se delimitan con una pista de lenguaje detectada cuando es posible:

```
<pre><code class="language-python">...</code></pre>    →   ```python
<pre data-lang="js">...</pre>                          →   ```js
<pre><code class="highlight-source-shell">...</code></pre> →   ```shell
```

Se reconocen las clases que coinciden con `language-*`, `lang-*`, `highlight-source-*` y `brush:*`, además de los atributos `data-lang` y `data-language` tanto en `<pre>` como en su `<code>` anidado. Si no se encuentra ninguna pista, el bloque se delimita sin etiqueta de lenguaje.

### Autenticación básica HTTP

Pasa `auth` con `username` y `password` para convertir páginas protegidas con HTTP Basic Authentication.

```json
{
    "url": "https://staging.example.com/docs/article",
    "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 de artículos exclusivas para miembros o específicas de una configuración regional (locale).

```json
{
    "url": "https://example.com/members/post",
    "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.

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

### Carga diferida de imágenes

Cuando `load_media` y `enable_scroll` están habilitados (ambos con valor predeterminado `true`), el conversor desplaza la página lentamente para activar los cargadores de carga diferida y luego espera a que todas las imágenes terminen de cargar antes de capturar el HTML final. Esto garantiza que los valores `data-src` se hayan promovido a valores `src` reales y que la lista `images` del frontmatter esté completa.

Configura `load_media=false` para una extracción más rápida cuando solo necesitas el cuerpo de texto. En la salida pueden quedar valores `src` de marcador de posición.

### Funcionalidades adicionales de renderizado

- **Normalización de unidades de viewport** -- Las unidades de viewport de CSS (`vh`, `svh`, `lvh`, `dvh`) se convierten en valores de píxeles fijos antes de la extracción.
- **Modo sigiloso (stealth)** -- Enmascaramiento de la 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 nueva del navegador o popup activado por la página.
- **Bypass de CSP** -- Gestiona las restricciones de Content Security Policy y Trusted Types que, de lo contrario, bloquearían la manipulación de la página.

---

## Restricciones según el plan de suscripción

| Feature | Founding | Indie | Studio | Enterprise |
|---------|------|---------|-----|------------|
| Conversión básica (una sola URL, síncrona) | 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 cuando se convierten varias URLs.

### Cómo funciona

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

### Notificación por correo

De forma predeterminada, se envía un correo de finalización a la dirección de correo del propietario del proyecto. Anúlalo 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` para recibir una notificación POST automática al finalizar:

```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 que la entrega fue exitosa con los códigos HTTP 200, 201, 202 y 204.

---

## Procesamiento por lotes y en masa

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

### Salida individual (predeterminada)

Cada URL genera un archivo Markdown independiente:

```json
{
    "url": [
        "https://example.com/post-1",
        "https://example.com/post-2",
        "https://example.com/post-3"
    ],
    "async_mode": true
}
```

### Salida en paquete ZIP

Agrupa todos los archivos Markdown en un único archivo ZIP:

```json
{
    "url": [
        "https://example.com/post-1",
        "https://example.com/post-2",
        "https://example.com/post-3"
    ],
    "async_mode": true,
    "output_format": true,
    "output_filename": "blog-archive"
}
```

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-markdown",
    headers={"X-API-Key": "sk_your_private_key"},
    json={
        "url": "https://example.com/articles/my-post",
        "direct_download": True
    }
)

response.raise_for_status()
markdown_text = response.content.decode("utf-8")
print(markdown_text)
```

### PHP (clave privada)

```php
$ch = curl_init("https://api.enconvert.com/v1/convert/url-to-markdown");
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/articles/my-post"
    ])
]);

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

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

### Go (clave privada)

```go
package main

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

func main() {
    body, _ := json.Marshal(map[string]interface{}{
        "url": "https://example.com/articles/my-post",
    })

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

const markdown = await convertRes.text();
console.log(markdown);
```

### React (clave pública)

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

function UrlToMarkdown() {
    const [loading, setLoading] = useState(false);
    const [markdown, setMarkdown] = useState("");

    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-markdown", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${token}`
                },
                body: JSON.stringify({ url: "https://example.com/articles/my-post" })
            });

            setMarkdown(await convertRes.text());
        } finally {
            setLoading(false);
        }
    }

    return (
        <div>
            <button onClick={convertUrl} disabled={loading}>
                {loading ? "Converting..." : "Convert to Markdown"}
            </button>
            {markdown && <pre>{markdown}</pre>}
        </div>
    );
}

export default UrlToMarkdown;
```

---

## Respuestas de error

| Status | Condition |
|--------|-----------|
| `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 strings) |
| `400 Bad Request` | Conflicto entre `auth` y el encabezado personalizado `Authorization` |
| `400 Bad Request` | Clave pública intentando usar varias URLs |
| `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` | No se encontró el ID de job (al consultar el estado) |
| `500 Internal Server Error` | La conversión falló (fallo del navegador, error de navegación, fallo en la extracción) |

---

## Límites

| Limit | Value |
|-------|-------|
| 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 |
| 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 Markdown con una API REST?

Envía una solicitud `POST` a `/v1/convert/url-to-markdown` con un `url` en el cuerpo JSON y tu clave en el encabezado `X-API-Key`. Recibes una respuesta JSON con un `presigned_url` al archivo Markdown, o los bytes Markdown UTF-8 sin procesar cuando defines `direct_download=true`.

### ¿Puedo convertir páginas web a Markdown para pipelines de LLM y RAG?

Sí. La salida está diseñada para la ingesta de LLM. El algoritmo Readability (la misma biblioteca detrás de Firefox Reader View) aísla el artículo principal, se elimina el contenido superfluo como `<nav>`, `<footer>`, scripts y formularios, todos los enlaces e imágenes relativos se resuelven a URLs absolutas, y un bloque YAML frontmatter incluye `url`, `title`, `description`, `links` e `images` de la página.

### ¿Puedo convertir varias URLs a Markdown en una sola solicitud a la API?

Sí. Pasa un array de URLs en `url` con `async_mode=true` (requiere una clave privada y un plan con acceso a lotes); la API devuelve HTTP `202` con un `batch_id` que consultas mediante `GET /v1/convert/batch/{batch_id}`. Define `output_format=true` para agrupar todos los archivos Markdown en un único archivo ZIP.

### ¿Por qué algunas imágenes en mi salida Markdown tienen valores src de marcador de posición?

Esto ocurre cuando `load_media=false`. La extracción es más rápida, pero las imágenes con carga diferida pueden conservar valores `src` de marcador de posición. Mantén `load_media` y `enable_scroll` en su valor predeterminado `true` para que la página se desplace y active los cargadores de carga diferida, y para que todas las imágenes terminen de cargar (timeout de 5 segundos por imagen) antes de la captura.

### ¿Funciona la API de URL a Markdown en páginas que requieren inicio de sesión?

Sí, en planes con acceso a autenticación básica: pasa `auth` con `username` y `password` para HTTP Basic Auth, inyecta hasta 50 `cookies` de sesión, o envía hasta 20 `headers` personalizados, algo útil para páginas de artículos exclusivas para miembros o de staging.
