---
seo_title: SDK Node.js de conversión de archivos, npm | EnConvert
meta_desc: Instala @enconvert/node-sdk desde npm para Node.js 18+. Métodos tipados de conversión y un espacio de nombres client.v2 para percibir, extraer y vigilar la web.
keywords: sdk de conversión de archivos nodejs, cliente npm de api de conversión, convertir archivos en nodejs, sdk de url a pdf en nodejs, heic a webp en nodejs, docx a pdf en node js, comprimir imagen en nodejs, convertir cualquier archivo a markdown en nodejs, cliente typescript de api de conversión, sdk de web scraping en nodejs, url a markdown en nodejs, extracción de datos estructurados en nodejs, pipeline de ingesta rag en nodejs, monitorizar cambios de una web en nodejs
---

# SDK de Conversión de Archivos para Node.js

`@enconvert/node-sdk` es el cliente oficial de JavaScript y TypeScript para la API de EnConvert. Trece métodos de conversión tipados se corresponden 1:1 con endpoints REST como `POST /v1/convert/url-to-pdf`, y un segundo espacio de nombres, `client.v2`, añade inteligencia web: percibir una URL y convertirla en artefactos listos para agentes, descubrir las URLs de un sitio, ejecutar una búsqueda web, extraer datos estructurados, ingerir un sitio en JSONL listo para RAG y vigilar páginas en busca de cambios. Está pensado para Node.js 18+ sin dependencias en tiempo de ejecución, construido sobre `fetch`, `FormData` y `node:stream` nativos, y se recupera de forma transparente de los tiempos de espera del proxy inverso sondeando el estado del trabajo. Se publica con builds duales ESM y CJS con declaraciones de TypeScript completas.

<div class="alert alert-info">
<strong>npm:</strong> <code>@enconvert/node-sdk</code> · <strong>Fuente:</strong> <a href="https://github.com/enconvert/node-sdk">enconvert/node-sdk</a> · <strong>Node:</strong> 18+
</div>

---

## Instalación

```bash
npm install @enconvert/node-sdk
```

```bash
pnpm add @enconvert/node-sdk
```

```bash
yarn add @enconvert/node-sdk
```

---

## Inicio rápido

```ts
import { Enconvert } from "@enconvert/node-sdk";

const client = new Enconvert({ apiKey: process.env.ENCONVERT_API_KEY! });

// V1: convierte una URL a PDF y transmítelo al disco.
const result = await client.convertUrlToPdf("https://example.com", {
    saveTo: "page.pdf",
});
console.log(result.presignedUrl);

// V2: lee una página como debería leerla tu agente, con una puntuación de calidad adjunta.
const op = await client.v2.perceive("https://example.com", {
    outputs: ["markdown", "structured"],
});
console.log(op.outputs.markdown.url, op.renderQuality); // p. ej. 0.93
```

El SDK funciona en todo runtime moderno de Node (Node 18+, Bun y Deno vía el especificador de npm). Es **solo del lado del servidor**, así que no incluyas tu clave de API privada en una aplicación de navegador.

---

## Qué expone el cliente

Un cliente, dos superficies. A ambas se llega desde la misma instancia de `Enconvert` y comparten una única clave de API.

| Superficie | Cómo se accede | Qué cubre |
|---------|-----------|----------------|
| Conversión de archivos | `client.convertUrlToPdf(...)`, `client.convertImage(...)`, etc. | Trece métodos tipados para renderizado de URLs, conversión de imágenes, compresión de imágenes y conversión de documentos, más el sondeo de trabajos y de lotes de sitios completos. Consulta [Conversión de archivos](#conversion-de-archivos). |
| Inteligencia web (V2) | `client.v2.perceive(...)`, `client.v2.distill(...)`, etc. | Veintitrés métodos repartidos en seis capacidades: perceive, discover, lookup, distill, ingest y watch. Consulta [Inteligencia web (V2)](#inteligencia-web-v2). |

Los endpoints de V2 requieren una clave de API privada (`sk_...`); las claves públicas se rechazan. Consulta [Autenticación](/es/docs/authentication.md) para ver en qué se diferencian los dos tipos de clave, y el [V1 y V2](/es/docs/concepts/v1-and-v2.md) para la superficie REST que hay detrás de `client.v2`.

---

## Conversión de archivos

La superficie de conversión expone trece métodos que se corresponden 1:1 con la API REST:

| Método | Endpoint | Devuelve |
|--------|----------|---------|
| `convertUrlToPdf(url, options?)` | `POST /v1/convert/url-to-pdf` | `ConversionResult` |
| `convertUrlToScreenshot(url, options?)` | `POST /v1/convert/url-to-screenshot` | `ConversionResult` |
| `convertUrlToMarkdown(url, options?)` | `POST /v1/convert/url-to-markdown` | `ConversionResult` |
| `convertImage(file, options)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `convertDocument(file, options?)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `compressImage(file, options?)` | `POST /v1/convert/compress-image` | `ConversionResult` |
| `convertToMarkdown(file, options?)` | `POST /v1/convert/anything-to-markdown` | `ConversionResult` |
| `convertToPdf(file, options?)` | `POST /v1/convert/anything-to-pdf` | `ConversionResult` |
| `getJobStatus(jobId)` | `GET /v1/convert/status/{jobId}` | `JobStatus` |
| `convertWebsiteToPdf(url, options?)` | `POST /v1/convert/website-to-pdf` | `BatchSubmission` |
| `convertWebsiteToScreenshot(url, options?)` | `POST /v1/convert/website-to-screenshot` | `BatchSubmission` |
| `getBatchStatus(batchId)` | `GET /v1/convert/batch/{batchId}` | `BatchStatus` |
| `waitForBatch(batchId, options?)` | `GET /v1/convert/batch/{batchId}` (sondeado) | `BatchStatus` |

Cada método devuelve una promesa tipada. Todos los campos de opciones son opcionales salvo que se indique lo contrario. Los cuatro últimos son ayudantes de lotes de sitios completos: envían y sondean trabajos asíncronos, así que devuelven un `BatchSubmission` o un `BatchStatus` en lugar de un `ConversionResult`.

---

### `convertUrlToPdf`

Renderiza cualquier URL pública a PDF.

```ts
const result = await client.convertUrlToPdf("https://example.com", {
    pdfOptions: { pageSize: "A4", orientation: "landscape" },
    singlePage: false,
    viewportWidth: 1440,
    saveTo: "report.pdf",
});
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `saveTo` | `string` | -- | Ruta local a la que transmitir el PDF. Los directorios padre se crean automáticamente. |
| `singlePage` | `boolean` | `true` | `true` produce una única página continua. `false` pagina usando `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions` | -- | Tamaño de página, orientación, márgenes, escala, escala de grises, encabezado y pie. Consulta [Opciones de PDF](#opciones-de-pdf). |
| `viewportWidth` | `number` | `1920` | Ancho del viewport del navegador en píxeles. |
| `viewportHeight` | `number` | `1080` | Alto del viewport del navegador en píxeles. |
| `loadMedia` | `boolean` | `true` | Espera a las imágenes y los vídeos antes de capturar. |
| `enableScroll` | `boolean` | `true` | Hace scroll de arriba abajo para disparar la carga diferida. |
| `outputFilename` | `string` | automático | Sustituye el nombre de archivo generado. Se añade `.pdf` si falta. |

---

### `convertUrlToScreenshot`

Captura un PNG de página completa de cualquier URL.

```ts
const result = await client.convertUrlToScreenshot("https://example.com", {
    viewportWidth: 1440,
    saveTo: "screenshot.png",
});
```

Acepta las mismas opciones de viewport, medios, scroll y nombre de archivo que `convertUrlToPdf` (menos `singlePage` y `pdfOptions`).

---

### `convertUrlToMarkdown`

Extrae Markdown limpio con sabor GitHub de cualquier URL. El conversor elimina navegación, pies de página, anuncios y scripts, conserva el cuerpo principal del artículo y antepone frontmatter YAML (título, descripción, url, enlaces, imágenes).

```ts
const result = await client.convertUrlToMarkdown("https://example.com/article", {
    saveTo: "article.md",
});
```

Útil para construir pipelines de RAG, importar contenido de terceros a un CMS o generar datos de entrenamiento. Si quieres una puntuación de calidad de renderizado junto al Markdown, usa [`client.v2.perceive`](#perceive) en su lugar.

---

### `convertImage`

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp`.

```ts
// Desde una ruta
await client.convertImage("photo.heic", {
    outputFormat: "webp",
    saveTo: "photo.webp",
});

// Desde bytes
import { readFile } from "node:fs/promises";
const buf = await readFile("photo.heic");

await client.convertImage(
    { data: buf, filename: "photo.heic" },
    { outputFormat: "webp", saveTo: "photo.webp" },
);

// Rasteriza un SVG con un ancho fijo
await client.convertImage("logo.svg", { outputFormat: "png", width: 512, saveTo: "logo.png" });
```

El formato de entrada se detecta a partir de la extensión de la ruta o del nombre de archivo. El formato de salida es obligatorio.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|----------|-------------|
| `outputFormat` | `string` | Sí | Formato de destino: `jpeg`, `png`, `svg`, `heic` o `webp` (y `jpeg` para una entrada `.pdf`). Los alias `jpg`, `yml`, `htm` y `md` se normalizan. Los pares no admitidos lanzan un error antes de enviar la solicitud. |
| `saveTo` | `string` | -- | Ruta local a la que transmitir el resultado. |
| `outputFilename` | `string` | -- | Sustituye el nombre de archivo generado. |
| `width` | `number` | -- | Solo para entrada SVG (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), de 1 a 10000. Por sí solo escala proporcionalmente, tomando el alto de la relación de aspecto del SVG. |
| `height` | `number` | -- | Solo para entrada SVG (`svg-to-png`, `svg-to-jpeg`, `svg-to-webp`), de 1 a 10000. Por sí solo escala proporcionalmente, tomando el ancho de la relación de aspecto del SVG. |

Fija `width` y `height` a la vez para clavar un lienzo exacto, lo que puede cambiar la relación de aspecto. Omite ambos y la salida conserva el ancho, el alto o el `viewBox` intrínsecos del SVG. El total de píxeles de salida está limitado a 25.000.000. `svg-to-heic` no acepta ninguna de las dos opciones, y el SDK lanza un error antes de enviar la solicitud si se las pasas a cualquier otra conversión.

---

### `convertDocument`

Convierte documentos y formatos de datos. El `outputFormat` predeterminado es `"pdf"`.

```ts
// docx a pdf
await client.convertDocument("report.docx", { saveTo: "report.pdf" });

// json a yaml
await client.convertDocument("data.json", {
    outputFormat: "yaml",
    saveTo: "data.yaml",
});

// markdown a pdf con configuración de página personalizada
await client.convertDocument("README.md", {
    outputFormat: "pdf",
    pdfOptions: { pageSize: "A4", margins: { top: 20, bottom: 20, left: 25, right: 25 } },
    saveTo: "readme.pdf",
});
```

**Entradas admitidas:** `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`, `html`, `htm`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers`, `markdown` (`.md`, `.markdown`), `csv`, `json`, `xml`, `yaml` (`.yaml`, `.yml`), `toml`.

EPUB no tiene un par de conversión de documentos propio. Envía los archivos `.epub` a través de [`convertToPdf`](#converttopdf) o [`convertToMarkdown`](#converttomarkdown) en su lugar.

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `outputFormat` | `string` | `"pdf"` | Formato de destino. |
| `saveTo` | `string` | -- | Ruta local a la que transmitir el resultado. |
| `outputFilename` | `string` | -- | Sustituye el nombre de archivo generado. |
| `pdfOptions` | `PdfOptions` | -- | Configuración de página. Solo se respeta cuando la salida es PDF. |

---

### `compressImage`

Reduce un PNG, JPEG o WebP sin cambiar su formato.

```ts
// Solo la pasada sin pérdida
const result = await client.compressImage("photo.jpg", { saveTo: "photo-small.jpg" });

// Apunta a un presupuesto de 200 KB
const capped = await client.compressImage("photo.jpg", {
    targetSizeKb: 200,
    saveTo: "photo-capped.jpg",
});

console.log(capped.fileSize);
```

**Entradas admitidas:** `.png`, `.jpg`, `.jpeg`, `.webp`.

La salida conserva el formato y la extensión de la entrada, así que no hay formato de salida que elegir. La primera etapa es sin pérdida: se eliminan los metadatos, se preservan el perfil ICC y la orientación EXIF, y el resultado nunca es mayor que la entrada. Fijar `targetSizeKb` añade una segunda etapa que reduce la escala con la relación de aspecto bloqueada hasta cumplir el presupuesto. Ese objetivo es de mejor esfuerzo: un presupuesto inalcanzable devuelve el archivo más pequeño logrado en lugar de un error, así que revisa `result.fileSize`. Los APNG animados y los WebP animados se rechazan con `400`, y el lienzo decodificado está limitado a 40.000.000 de píxeles.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|----------|-------------|
| `targetSizeKb` | `number` | -- | Presupuesto de tamaño en KB, entero, mínimo `1`. Omítelo para ejecutar solo la pasada sin pérdida. |
| `saveTo` | `string` | -- | Ruta local a la que transmitir el resultado. |
| `outputFilename` | `string` | -- | Sustituye el nombre de archivo generado. Se conserva la extensión de entrada. |

---

### `convertToMarkdown`

Convierte a Markdown cualquier documento, hoja de cálculo, presentación, ebook, archivo web o de texto plano admitido.

```ts
await client.convertToMarkdown("handbook.docx", {
    saveTo: "handbook.md",
});
```

**Entradas admitidas (22):** `.csv`, `.doc`, `.docx`, `.epub`, `.htm`, `.html`, `.markdown`, `.md`, `.mdown`, `.mkd`, `.odp`, `.ods`, `.odt`, `.pdf`, `.ppt`, `.pptx`, `.rtf`, `.text`, `.txt`, `.xhtml`, `.xls`, `.xlsx`.

La salida es un único archivo `.md` consciente de los encabezados y pensado para el chunking de RAG: la jerarquía de encabezados del documento sobrevive a la conversión, así que un chunker semántico puede dividir por encabezados en lugar de por recuentos arbitrarios de caracteres. Este endpoint no tiene opciones de PDF. Cualquier otra extensión lanza un error antes de hacer ninguna solicitud.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Ruta local a la que transmitir el Markdown. |
| `outputFilename` | `string` | -- | Sustituye el nombre de archivo generado. |

Si además quieres que el chunking se haga por ti, entrega esos mismos archivos a [`client.v2.ingestFiles`](#ingest).

---

### `convertToPdf`

Convierte a PDF cualquier documento, imagen, ebook, archivo web o de texto plano admitido.

```ts
// docx a pdf
await client.convertToPdf("contract.docx", { saveTo: "contract.pdf" });

// html a pdf con geometría de página completa
await client.convertToPdf("invoice.html", {
    pdfOptions: { pageSize: "A4", margins: { top: 15, bottom: 15, left: 15, right: 15 } },
    saveTo: "invoice.pdf",
});

// pdf a pdf en escala de grises (paso directo)
await client.convertToPdf("scan.pdf", {
    pdfOptions: { grayscale: true },
    saveTo: "scan-gray.pdf",
});
```

**Entradas admitidas (36):** `.bmp`, `.csv`, `.doc`, `.docx`, `.epub`, `.gif`, `.heic`, `.heif`, `.htm`, `.html`, `.jpeg`, `.jpg`, `.markdown`, `.md`, `.mdown`, `.mkd`, `.numbers`, `.odp`, `.ods`, `.odt`, `.ots`, `.pages`, `.pdf`, `.png`, `.ppt`, `.pptx`, `.rtf`, `.svg`, `.text`, `.tif`, `.tiff`, `.txt`, `.webp`, `.xhtml`, `.xls`, `.xlsx`.

Una entrada `.pdf` se acepta y se pasa directamente, así que con `pdfOptions: { grayscale: true }` este método sirve también como vía de normalización de PDF. EPUB se gestiona aquí también, ya que no tiene un par de conversión de documentos propio. Cualquier otra extensión lanza un error antes de hacer ninguna solicitud.

<div class="alert alert-warning">
<strong>La geometría depende de la entrada.</strong> La geometría de página completa (tamaño de página, ancho y alto de página, orientación, márgenes, escala, encabezado, pie) se respeta para entradas HTML (<code>.html</code>, <code>.htm</code>, <code>.xhtml</code>), Markdown, texto plano, EPUB, imagen y SVG. Las entradas Office, ODF, iWork, RTF y CSV, más el paso directo de PDF, solo admiten <code>grayscale</code> y devuelven <code>400</code> si se fija una opción explícita de geometría. <code>grayscale</code> se respeta para cualquier entrada.
</div>

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|----------|-------------|
| `saveTo` | `string` | -- | Ruta local a la que transmitir el PDF. |
| `outputFilename` | `string` | -- | Sustituye el nombre de archivo generado. Se añade `.pdf` si falta. |
| `pdfOptions` | `PdfOptions` | -- | Configuración de página. Consulta la advertencia anterior sobre qué entradas respetan la geometría. |

---

### `getJobStatus`

Sondea el estado de un trabajo asíncrono o recuperado.

```ts
const status = await client.getJobStatus("job_abc123");

if (status.status === "success") {
    console.log(status.presignedUrl);
} else if (status.status === "failed") {
    console.error(status.error);
}
```

Devuelve `{ status: "processing" | "success" | "failed", presignedUrl?, objectKey?, error? }`.

<div class="alert alert-info">
<strong>Normalmente no necesitas llamar a esto directamente.</strong> El SDK sondea automáticamente cuando una solicitud síncrona devuelve 5xx. Consulta <a href="#timeout-recovery">Recuperación de tiempos de espera</a> más abajo.
</div>

---

### Ayudantes para lotes de sitios completos

`convertWebsiteToPdf` y `convertWebsiteToScreenshot` descubren las páginas de un sitio, las encolan todas y agrupan las salidas en un único ZIP. Ambos devuelven un `BatchSubmission` de inmediato; sondea con `getBatchStatus` o bloquea con `waitForBatch`. Las opciones compartidas son `crawlMode` (`"auto"`, `"sitemap"` o `"full"`), `includePatterns`, `excludePatterns`, `notificationEmail` y `callbackUrl`; `convertWebsiteToPdf` añade `singlePage` y `pdfOptions`.

```ts
const batch = await client.convertWebsiteToPdf("https://example.com", {
    crawlMode: "sitemap",
    excludePatterns: ["/tag/"],
});

const done = await client.waitForBatch(batch.batchId, { saveTo: "site.zip" });
console.log(done.status, done.completed, done.failed, done.zipDownloadUrl);
```

`waitForBatch` acepta `intervalMs` (predeterminado `5_000`), `timeoutMs` (predeterminado `1_800_000`, treinta minutos) y `saveTo`. Lanza `APIError(504, ...)` si se cumple el plazo. Consulta el [resumen de endpoints](/es/docs/endpoints.md) para la superficie REST.

---

## Inteligencia web (V2)

Todo lo que hay bajo `client.v2` devuelve datos en los que un agente puede confiar, porque cada renderizado de V2 lleva una puntuación `renderQuality` de 0.0 a 1.0. Una página bloqueada, un desafío antibot, un muro de inicio de sesión, una página de error HTTP, un 404 blando o el shell vacío de una SPA vuelven con una puntuación baja más `deductions` y `warnings` con nombre, de modo que quedan marcados en lugar de confundirse con contenido real. El contenido se sigue devolviendo; tú decides qué hacer con él. Las puntuaciones por debajo de aproximadamente 0.40 significan que el renderizado no tuvo éxito en ningún sentido útil.

Veintitrés métodos repartidos en seis capacidades:

| Método | Endpoint | Devuelve |
|--------|----------|---------|
| `v2.perceive(url, options?)` | `POST /v2/perceive` | `PerceiveResult` |
| `v2.perceiveDirect(url, options?)` | `POST /v2/perceive` | `PerceiveDirectResult` |
| `v2.getPerceiveOperation(operationId)` | `GET /v2/perceive/{operationId}` | `PerceiveResult` |
| `v2.downloadPerceiveArtifact(operationId, output?)` | `GET /v2/perceive/{operationId}` | `PerceiveDirectResult` |
| `v2.perceiveBatch(urls, options?)` | `POST /v2/perceive/batch` | `PerceiveBatchResult` |
| `v2.getPerceiveBatch(jobId)` | `GET /v2/perceive/batch/{jobId}` | `PerceiveBatchResult` |
| `v2.discover(url, options?)` | `POST /v2/discover` | `DiscoverResult` |
| `v2.lookup(query, options?)` | `POST /v2/lookup` | `LookupResult` |
| `v2.distill(options)` | `POST /v2/distill` | `DistillResult` |
| `v2.ingest(options)` | `POST /v2/ingest` | `IngestJob` |
| `v2.ingestFiles(files, options?)` | `POST /v2/ingest/files` | `IngestJob` |
| `v2.listIngestJobs(options?)` | `GET /v2/ingest` | `IngestJobList` |
| `v2.getIngestJob(jobId)` | `GET /v2/ingest/{jobId}` | `IngestJob` |
| `v2.cancelIngestJob(jobId)` | `DELETE /v2/ingest/{jobId}` | `IngestJob` |
| `v2.retryIngestWebhook(jobId)` | `POST /v2/ingest/{jobId}/retry-webhook` | `WebhookRetryResult` |
| `v2.getWebhookSecret()` | `GET /v2/ingest/webhook-secret` | `WebhookSecret` |
| `v2.rotateWebhookSecret()` | `POST /v2/ingest/webhook-secret/rotate` | `WebhookSecret` |
| `v2.createWatcher(url, options?)` | `POST /v2/watch` | `Watcher` |
| `v2.listWatchers(options?)` | `GET /v2/watch` | `WatcherList` |
| `v2.getWatcher(watcherId)` | `GET /v2/watch/{watcherId}` | `Watcher` |
| `v2.getWatcherSnapshots(watcherId, options?)` | `GET /v2/watch/{watcherId}/snapshots` | `WatcherSnapshotList` |
| `v2.updateWatcher(watcherId, updates)` | `PATCH /v2/watch/{watcherId}` | `Watcher` |
| `v2.deleteWatcher(watcherId)` | `DELETE /v2/watch/{watcherId}` | `Watcher` |

Las opciones son camelCase en la superficie del SDK y se serializan al formato de transmisión snake_case de la API; las respuestas se mapean de vuelta a camelCase. Tus propios payloads (esquemas de extracción, datos extraídos, campos rastreados, entradas de diff) pasan intactos.

---

### Perceive

Renderiza una URL y produce los artefactos que pidas: Markdown, HTML limpio o en bruto, una captura de pantalla del viewport o de página completa, un PDF, una lista de enlaces, una lista de imágenes o datos estructurados. `perceive` es síncrono y devuelve la operación completada con URLs de artefacto firmadas durante 15 minutos. Referencia completa: [Perceive](/es/docs/endpoints/perceive.md).

```ts
const op = await client.v2.perceive("https://example.com", {
    outputs: ["markdown", "screenshot", "structured"],
    extract: ["tables", "metadata"],
    onlyMainContent: true,
    waitFor: "css:.article-body",
    viewport: { width: 1440, height: 900 },
});

console.log(op.renderQuality);        // de 0.0 a 1.0
console.log(op.deductions);           // p. ej. { http_error: 0.7 }
console.log(op.outputs.markdown.url); // URL firmada de 15 minutos
console.log(op.structured);

if ((op.renderQuality ?? 0) < 0.4) {
    console.warn("Bad read, do not feed this to the model:", op.warnings);
}

// Vuelve a firmar las URLs de los artefactos más tarde sin volver a renderizar:
const again = await client.v2.getPerceiveOperation(op.operationId);
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `outputs` | `PerceiveOutputName[]` | `["markdown", "structured"]` | Cualquiera de `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `extract` | `PerceiveExtractName[]` | -- | Objetivos heurísticos: `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `onlyMainContent` | `boolean` | `true` | Elimina navegación, encabezado, pie y avisos de cookies de la salida Markdown, protegido por una salvaguarda de fidelidad. `false` devuelve la página completa intacta. |
| `schema` | `Record<string, unknown>` | -- | Esquema JSON para la extracción estructurada en el nivel de LLM. |
| `waitFor` | `string` | -- | Selector CSS (opcionalmente `"css:..."`) o `"js:<expr>"` que esperar antes de capturar. |
| `waitTimeoutMs` | `number` | `30000` | De 0 a 60000. |
| `jsCode` | `string` | -- | JavaScript ejecutado tras la navegación. Máximo 20000 caracteres. |
| `viewport` | `{ width?, height? }` | `1920 x 1080` | Ancho de 320 a 3840, alto de 240 a 2160. |
| `headers` | `Record<string, string>` | -- | Encabezados de solicitud adicionales. |
| `cookies` | `BrowserCookie[]` | -- | Cookies inyectadas antes de renderizar. Cada una necesita `name`, `value` y `domain` o `url`. |
| `auth` | `{ username, password }` | -- | Autenticación HTTP Basic. |
| `cacheMode` | `"enabled" \| "bypass" \| "refresh"` | `"enabled"` | Caché de una hora. `bypass` la salta, `refresh` fuerza un nuevo renderizado. |
| `pdfOptions` | `PdfOptions` | -- | Solo tiene sentido cuando `outputs` incluye `"pdf"`. Consulta [Opciones de PDF](#opciones-de-pdf). |
| `blockResources` | `PerceiveResourceType[]` | -- | Tipos de recurso que el navegador no debe cargar, por ejemplo `["image", "font", "media"]`. |
| `respectRobots` | `boolean` | -- | Respeta las reglas de robots del sitio. |
| `mobile` | `boolean` | -- | Renderiza con un perfil móvil. |
| `directDownload` | `boolean` | -- | Solo en `perceive`. Es preferible `perceiveDirect`, que lo fija por ti. |

<div class="alert alert-warning">
<strong>Tres opciones están declaradas pero aún no operativas.</strong> <code>proxyUrl</code>, <code>geolocation</code> y <code>actionChain</code> están tipadas en <code>PerceiveOptions</code>, pero el servidor las rechaza actualmente con <code>422</code>. Están reservadas, no son utilizables.
</div>

**Descarga directa.** `perceiveDirect` se salta el viaje de ida y vuelta de la URL firmada: el cuerpo de la respuesta HTTP son los bytes del artefacto y los metadatos viajan en los encabezados. Requiere exactamente una salida que produzca artefacto, y el SDK lanza un error localmente antes de enviar si no es así (`"structured"` puede acompañarla, pero permanece inline en el servidor y no se devuelve).

```ts
import { writeFile } from "node:fs/promises";

const direct = await client.v2.perceiveDirect("https://example.com", { outputs: ["markdown"] });
console.log(direct.contentType, direct.renderQuality, direct.sourceStatusCode);
await writeFile(direct.filename ?? "page.md", direct.content);

// Vuelve a descargar como bytes en bruto un artefacto almacenado de una operación anterior.
// `output` puede omitirse cuando la operación produjo exactamente un artefacto.
const raw = await client.v2.downloadPerceiveArtifact(op.operationId, "markdown");
```

`downloadPerceiveArtifact` devuelve `410` una vez que el artefacto almacenado ha caducado, y `400` (con la lista de salidas disponibles) si la operación produjo más de un artefacto y omitiste `output`.

**Lotes.** `perceiveBatch` acepta hasta 1000 URLs con un único bloque de opciones compartido. Los lotes pequeños se ejecutan inline y vuelven completados; los mayores devuelven el estado `"queued"`, así que sondea `getPerceiveBatch` con el `jobId` devuelto.

```ts
const batch = await client.v2.perceiveBatch(["https://example.com/a", "https://example.com/b"], {
    outputs: ["markdown"],
    outputMode: "zip",
});

let job = await client.v2.getPerceiveBatch(batch.jobId);
while (job.status === "queued" || job.status === "processing") {
    await new Promise((r) => setTimeout(r, 3000));
    job = await client.v2.getPerceiveBatch(batch.jobId);
}
console.log(job.completed, job.failed, job.zip?.url);
```

`outputMode` es `"manifest"` (predeterminado, una entrada por URL en `items`) o `"zip"` (todos los artefactos exitosos agrupados cuando termina el trabajo). El endpoint de lotes rechaza `directDownload`; usa `outputMode: "zip"` en su lugar.

---

### Discover

Lista las URLs de un sitio sin renderizar nada. No interviene ningún navegador, así que es rápido y barato comparado con percibir cada página. Referencia completa: [Discover](/es/docs/coming-soon/discover.md).

```ts
const found = await client.v2.discover("https://example.com", {
    mode: "hybrid",
    maxUrls: 200,
    maxDepth: 3,
    excludePatterns: ["/tag/", "/author/"],
    sameDomainOnly: true,
});

console.log(found.total, found.truncated, found.sources); // p. ej. { sitemap: 42, crawl: 30 }
for (const url of found.urls) console.log(url);
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `mode` | `"sitemap" \| "crawl" \| "hybrid"` | `"hybrid"` | Solo sitemap, solo rastreo HTTP, o ambos. |
| `maxUrls` | `number` | `100` | De 1 a 1000. `truncated` es `true` cuando existían más URLs de las que permitía este tope. |
| `maxDepth` | `number` | `2` | De 1 a 5. Profundidad de rastreo desde la URL semilla. |
| `includePatterns` | `string[]` | -- | Lista de permitidos por regex, máximo 50 entradas. |
| `excludePatterns` | `string[]` | -- | Lista de denegados por regex aplicada después de `includePatterns`, máximo 50 entradas. |
| `sameDomainOnly` | `boolean` | `true` | Mantiene el rastreo en el dominio semilla. |
| `respectRobots` | `boolean` | -- | Respeta las reglas de robots del sitio. `robotsRespected` en el resultado informa de lo que ocurrió. |

---

### Lookup

Ejecuta una búsqueda web categorizada y, opcionalmente, percibe automáticamente los primeros resultados para que cada acierto lleve su propio `PerceiveResult` completo inline. Referencia completa: [Lookup](/es/docs/coming-soon/lookup.md).

```ts
const search = await client.v2.lookup("best static site generators", {
    category: "web",
    numResults: 10,
    country: "us",
    locale: "en",
    timeFilter: "month",
    perceiveTop: 3,
});

for (const hit of search.results) {
    console.log(hit.position, hit.title, hit.url);
    if (hit.perceive) {
        console.log("  quality:", hit.perceive.renderQuality);
        console.log("  markdown:", hit.perceive.outputs.markdown?.url);
    }
}
console.log(search.answerBox, search.knowledgeGraph);
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `category` | `"web" \| "news" \| "images" \| "scholar" \| "patents" \| "maps"` | `"web"` | Vertical de búsqueda. |
| `country` | `string` | -- | Código de país `gl` de Google, por ejemplo `"us"` o `"in"`. |
| `locale` | `string` | -- | Idioma de interfaz `hl` de Google, por ejemplo `"en"`. |
| `timeFilter` | `"hour" \| "day" \| "week" \| "month" \| "year"` | -- | Ventana de actualidad. |
| `numResults` | `number` | `10` | De 1 a 100. |
| `page` | `number` | `1` | De 1 a 10. |
| `location` | `string` | -- | Ubicación en texto libre, por ejemplo `"Austin, Texas"`. |
| `autocorrect` | `boolean` | `true` | Deja que el proveedor corrija erratas evidentes. |
| `perceiveTop` | `number` | `0` | De 0 a 10. Renderiza automáticamente las N primeras URLs de resultados; cada una es un renderizado completo en el navegador. |

`perceiveTop` en el resultado informa de cuántos resultados se percibieron realmente, que puede ser menos de lo que pediste, y `perceiveOperationIds` te da los identificadores de operación para volver a firmar más tarde.

---

### Distill

Extracción estructurada guiada por esquema. Dale una forma y un conjunto de URLs (o un sitio que descubrir primero) y devuelve registros que coinciden con esa forma. Un `cssSchema` opcional responde todo lo que puede desde selectores antes de que algo escale al nivel de LLM. Referencia completa: [Distill](/es/docs/coming-soon/distill.md).

```ts
const extraction = await client.v2.distill({
    urls: ["https://example.com/pricing"],
    schema: { plans: "list of plan names with monthly prices" },
    cssSchema: {
        baseSelector: ".plan-card",
        fields: [
            { name: "name", type: "text", selector: "h3" },
            { name: "price", type: "text", selector: ".price" },
            { name: "url", type: "attribute", selector: "a", attribute: "href" },
        ],
    },
});

const first = extraction.results[0];
console.log(first.data);
console.log(first.extractionTier);  // "css" | "llm" | "mixed" | "none"
console.log(first.fieldsFromCss, first.fieldsFromLlm, first.renderQuality);
```

Pasa exactamente uno de `urls` o `discoverFrom`; el SDK lanza un error localmente si pasas ambos o ninguno, y también lo lanza si falta `schema` o si no es un objeto.

```ts
// Descubre primero un sitio y luego destila cada página encontrada.
await client.v2.distill({
    discoverFrom: { url: "https://example.com", mode: "sitemap", maxPages: 10 },
    schema: { title: "page title", summary: "one-line summary" },
});
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `urls` | `string[]` | -- | URLs explícitas que destilar, máximo 50. Mutuamente excluyente con `discoverFrom`. |
| `discoverFrom` | `{ url, mode?, maxPages? }` | -- | Descubre primero y luego destila. `maxPages` va de 1 a 50, predeterminado 10, y limita tanto el descubrimiento como la destilación. |
| `schema` | `Record<string, unknown>` | obligatorio | Un objeto JSON Schema (`{ type: "object", properties: {...} }`) o un mapa plano `{ campo: descripción }`. |
| `cssSchema` | `CssSchema` | -- | Pasada libre de selectores ejecutada antes de cualquier escalado a LLM. |
| `waitFor` | `string` | -- | Selector CSS o `"js:<expr>"` que esperar. |
| `waitTimeoutMs` | `number` | `30000` | De 0 a 60000. |
| `headers` | `Record<string, string>` | -- | Encabezados de solicitud adicionales. |
| `cookies` | `BrowserCookie[]` | -- | Cookies inyectadas antes de renderizar. |
| `respectRobots` | `boolean` | -- | Respeta las reglas de robots del sitio. |

Un `CssSchema` tiene un `baseSelector` (el contenedor que se repite, un registro por coincidencia), una lista `fields`, un `name` opcional y un `targetField` opcional que nombra la propiedad del esquema de salida que llenan los registros. Cada campo es `{ name, type, selector?, attribute?, pattern?, default?, transform?, fields? }`, donde `type` es uno de `text`, `attribute`, `html`, `regex`, `nested`, `list`, `nested_list`. `attribute` es obligatorio para los campos `attribute`, `pattern` para los campos `regex`, y un array `fields` no vacío para los tipos anidados (profundidad máxima de 5).

---

### Ingest

Convierte un sitio, o un montón de documentos subidos, en JSONL fragmentado y listo para RAG. Ingest siempre es asíncrono: ambos puntos de entrada devuelven un `IngestJob` en cola, y o bien lo sondeas o bien configuras un webhook. Referencia completa: [Ingest](/es/docs/endpoints/ingest.md).

```ts
// Desde un sitio.
const job = await client.v2.ingest({
    mode: "sitemap",
    url: "https://docs.example.com",
    maxPages: 100,
    chunk: { maxWords: 512, sentenceOverlap: 1 },
    webhookUrl: "https://my.app/hooks/enconvert",
});

// Desde archivos subidos: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD,
// y documentos de ofimática heredados o ODF.
const fileJob = await client.v2.ingestFiles(["handbook.pdf", "notes.docx"], {
    chunk: { maxWords: 512, sentenceOverlap: 1 },
});

// Sondea cualquiera de los dos igual. Estados no terminales: queued, discovering, processing.
let status = await client.v2.getIngestJob(job.jobId);
while (!["completed", "failed", "canceled"].includes(status.status)) {
    await new Promise((r) => setTimeout(r, 5000));
    status = await client.v2.getIngestJob(job.jobId);
}
console.log(status.totalChunks, status.outputUrl, status.errorMessage);

const page = await client.v2.listIngestJobs({ limit: 20, skip: 0 });
console.log(page.jobs.length, page.hasMore);

await client.v2.cancelIngestJob(job.jobId); // idempotente
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `mode` | `"urls" \| "sitemap" \| "crawl"` | `"urls"` | `"urls"` necesita `urls` y rechaza `url`. `"sitemap"` y `"crawl"` necesitan una `url` semilla y rechazan `urls`. Ambas reglas se comprueban localmente antes de la solicitud. La unión `IngestMode` también tiene `"files"`, que es lo que `ingestFiles` informa en su trabajo; no lo pases aquí. |
| `url` | `string` | -- | URL semilla para `sitemap` y `crawl`. |
| `urls` | `string[]` | -- | URLs explícitas para el modo `"urls"`, máximo 1000. |
| `maxPages` | `number` | `50` | Tope de descubrimiento para `sitemap` y `crawl`, de 1 a 1000. |
| `maxDepth` | `number` | `2` | De 1 a 5. |
| `sameDomainOnly` | `boolean` | `true` | Mantiene el rastreo en el dominio semilla. |
| `includePatterns` / `excludePatterns` | `string[]` | -- | Lista de permitidos y lista de denegados por regex. |
| `respectRobots` | `boolean` | -- | Respeta las reglas de robots del sitio. |
| `waitFor` / `waitTimeoutMs` | `string` / `number` | -- / `30000` | Espera de renderizado por página. |
| `chunk` | `{ maxWords?, sentenceOverlap? }` | `512` / `1` | `maxWords` va de 32 a 4000, `sentenceOverlap` de 0 a 10. |
| `webhookUrl` | `string` | -- | Webhook de finalización, firmado con HMAC. |

`ingestFiles` acepta un `FileInput[]`, es decir, cadenas de ruta, `Uint8Array` / `Buffer` u objetos `{ data, filename, contentType? }`, mezclados como quieras. Solo toma `chunk` y `webhookUrl`, y lanza un error localmente con una lista vacía.

**Firma de webhooks.** Los webhooks de finalización se firman con HMAC. Obtén el secreto (se crea en la primera llamada) para verificar las entregas, rótalo cuando lo necesites y vuelve a entregar un webhook que tu endpoint se haya perdido.

```ts
const secret = await client.v2.getWebhookSecret();
console.log(secret.signatureHeader, secret.timestampHeader);
console.log(secret.signatureScheme, secret.replayToleranceSeconds);

// Rotarlo invalida de inmediato las firmas hechas con el secreto anterior.
const rotated = await client.v2.rotateWebhookSecret();

// Vuelve a entregar el webhook de un trabajo completado.
const retry = await client.v2.retryIngestWebhook(job.jobId);
console.log(retry.delivered, retry.attempts, retry.statusCode, retry.detail);
```

`retryIngestWebhook` devuelve `409` cuando el trabajo no está completado y `400` cuando el trabajo no tiene webhook configurado.

---

### Watch

Crea un vigilante que vuelve a renderizar una URL con una cadencia fija y te avisa cuando la página cambia, por correo, por webhook o por ambos. Referencia completa: [Watch](/es/docs/coming-soon/watch.md).

```ts
const watcher = await client.v2.createWatcher("https://example.com/pricing", {
    frequencyMinutes: 60,
    diffMode: "auto",
    webhookUrl: "https://my.app/hooks/changes",
    notifyEmail: true,
});
console.log(watcher.watcherId, watcher.nextCheckAt);

const list = await client.v2.listWatchers({ limit: 20 });
const one = await client.v2.getWatcher(watcher.watcherId);

const history = await client.v2.getWatcherSnapshots(watcher.watcherId, { limit: 10 });
for (const snap of history.snapshots) {
    console.log(snap.checkedAt, snap.hasChanges, snap.similarity, snap.changeCount);
}

await client.v2.updateWatcher(watcher.watcherId, { status: "paused" });
await client.v2.updateWatcher(watcher.watcherId, { webhookUrl: "" }); // limpia el webhook
await client.v2.deleteWatcher(watcher.watcherId);                     // borrado suave, idempotente
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `frequencyMinutes` | `number` | `60` | Minutos entre comprobaciones, de 60 a 43200. El mínimo de una hora es estricto. |
| `diffMode` | `"auto" \| "text" \| "structured" \| "tables" \| "metadata"` | `"auto"` | `"auto"` deja que el motor de diff elija según el tipo de contenido. |
| `trackFields` | `Record<string, unknown>` | -- | Subconjunto de campos o selectores para acotar el diff. |
| `webhookUrl` | `string` | -- | Webhook de notificación de cambios, firmado con HMAC. |
| `notifyEmail` | `boolean` | `true` | Envía un correo al propietario del proyecto cuando hay cambios. |

`updateWatcher` toma los mismos campos más `status` (`"active"` o `"paused"`) y requiere al menos uno de ellos; el SDK lanza un error localmente ante una actualización vacía. Pasar `webhookUrl: ""` limpia explícitamente el webhook. Borrar es un borrado suave: `deleteWatcher` devuelve el vigilante marcado como eliminado con estado `"deleted"`, y un vigilante borrado se lee como `404` desde `getWatcher`.

Cada instantánea lleva `checkedAt`, `hasChanges`, `similarity` (de 0.0 a 1.0 frente a la captura anterior), `renderQuality`, `changeCount` y un array `changes`.

<div class="alert alert-warning">
<strong>Los diffs de instantáneas contienen contenido de página no confiable.</strong> Las entradas de <code>snapshot.changes</code> vienen directamente de la página vigilada. Escápalas antes de renderizarlas en HTML o de escribirlas en un visor de logs.
</div>

---

## Opciones de PDF

Se pasan mediante el campo `pdfOptions` en `convertUrlToPdf`, `convertDocument`, `convertToPdf`, `convertWebsiteToPdf` y `client.v2.perceive` (cuando `outputs` incluye `"pdf"`).

```ts
const result = await client.convertUrlToPdf("https://example.com", {
    pdfOptions: {
        pageSize: "A4",
        orientation: "landscape",
        margins: { top: 10, bottom: 10, left: 15, right: 15 },
        scale: 0.9,
        grayscale: false,
    },
    saveTo: "report.pdf",
});
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `pageSize` | `string` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"`, etc. |
| `pageWidth` / `pageHeight` | `number` | Geometría de página explícita, como alternativa a `pageSize`. |
| `orientation` | `"portrait" \| "landscape"` | Por defecto es vertical. |
| `margins` | `{ top, bottom, left, right }` (mm) | Los cuatro son opcionales. |
| `scale` | `number` | Escala de renderizado, p. ej. `0.9` para el 90%. |
| `grayscale` | `boolean` | Posprocesa el PDF con Ghostscript para pasarlo a escala de grises. |
| `header` | `PdfHeaderFooter` | `{ content?, height? }`. `content` está limitado a 2000 caracteres. |
| `footer` | `PdfHeaderFooter` | La misma forma que `header`. |

Cada parámetro se describe por completo en [Trabajos síncronos y asíncronos](/es/docs/concepts/sync-and-async.md).

## Manejo de errores

Los errores son clases de excepción tipadas que puedes comparar con `instanceof`. La misma jerarquía cubre tanto los métodos de conversión como `client.v2`.

```ts
import {
    Enconvert,
    APIError,
    AuthenticationError,
    QuotaError,
    RateLimitError,
} from "@enconvert/node-sdk";

try {
    await client.v2.perceive("https://example.com", { outputs: ["markdown"] });
} catch (e) {
    if (e instanceof AuthenticationError) {
        console.error("Invalid API key. Check ENCONVERT_API_KEY.");
    } else if (e instanceof QuotaError) {
        console.error("Request rejected with 402.");
    } else if (e instanceof RateLimitError) {
        console.error("Too many requests. Back off and retry.");
    } else if (e instanceof APIError) {
        console.error(`API error [${e.statusCode}]: ${e.message}`);
    } else {
        throw e;
    }
}
```

| Clase | Se lanza en | Código de estado |
|-------|-----------|-------------|
| `AuthenticationError` | Clave inválida, ausente o revocada | `401`, `403` (ambos informan `statusCode` `401`) |
| `QuotaError` | Se lanza ante un HTTP 402 | `402` |
| `RateLimitError` | Demasiadas solicitudes | `429` |
| `APIError` | Cualquier otro 4xx / 5xx | el código real |
| `EnconvertError` | Clase base de todas las anteriores | -- |

`QuotaError` y `RateLimitError` extienden ambas `APIError`, que a su vez extiende `EnconvertError`, así que ordena tus comprobaciones `instanceof` de la más específica a la más general. Cada `APIError` lleva un campo `statusCode`.

Algunos fallos nunca llegan a la red: una extensión de archivo no admitida, una llamada a `distill` con `urls` y `discoverFrom` a la vez, una llamada a `ingest` cuyo modo y argumentos no concuerdan, una llamada a `perceiveDirect` con más de una salida de artefacto, o una llamada a `updateWatcher` sin campos. Esos lanzan un `Error` simple de forma local para que encuentres el fallo en desarrollo.

El mapa completo de mensajes de error está en la referencia de [Códigos de error](/es/docs/reference/errors.md).

---

## Recuperación de tiempos de espera

Las conversiones largas de URL a PDF o de documentos grandes pueden superar el límite de 60 a 120 segundos del proxy inverso, incluso cuando la conversión acaba teniendo éxito en el servidor. El SDK lo gestiona de forma transparente en los métodos de conversión de V1:

1. Antes de cada solicitud, el SDK genera un UUID y lo envía como `job_id` en el cuerpo de la solicitud.
2. Si la solicitud original devuelve 5xx, el SDK pasa en silencio a sondear `GET /v1/convert/status/{job_id}` cada 3 segundos.
3. En cuanto el trabajo queda registrado como `success`, el SDK devuelve el resultado. En cuanto queda registrado como `failed`, el SDK lanza `APIError`.
4. El plazo de sondeo es de 5 minutos. Si se supera, el SDK lanza `APIError(504, "Conversion timed out")`.

No necesitas escribir nada de código para esto, simplemente funciona. Fija `timeout` en el constructor si quieres acotar la solicitud inicial.

V2 usa objetos de trabajo explícitos en lugar de recuperación implícita: `perceiveBatch` e `ingest` devuelven un identificador que sondeas con `getPerceiveBatch` y `getIngestJob`, e `ingest` puede llamar a un webhook en su lugar.

---

## Configuración

```ts
const client = new Enconvert({
    apiKey: process.env.ENCONVERT_API_KEY!,
    timeout: 300_000, // ms, 5 min por defecto
    baseUrl: "https://api.enconvert.com", // sustitúyelo para gateways autoalojados
});
```

| Opción | Tipo | Predeterminado | Descripción |
|--------|------|---------|-------------|
| `apiKey` | `string` | -- (obligatorio) | Clave de API privada (`sk_...`). El constructor lanza un error de inmediato si falta. |
| `timeout` | `number` | `300_000` | Tiempo de espera de la solicitud en ms. Aborta el `fetch` subyacente mediante `AbortController`. |
| `baseUrl` | `string` | `https://api.enconvert.com` | URL base de la API. Las barras finales se eliminan. |

La clave viaja como encabezado `X-API-Key` en cada solicitud, tanto de V1 como de V2. `client.v2` se construye por ti y comparte la clave, la URL base y el tiempo de espera del cliente, así que no hay nada más que configurar.

<div class="alert alert-warning">
<strong>Nunca incrustes la clave de API en el código.</strong> Léela de una variable de entorno o de tu gestor de secretos. Cualquiera que consiga tu clave privada puede ejecutar conversiones y operaciones de V2 en tu cuenta. Rota las claves desde el <a href="/es/dashboard">panel de control</a>.
</div>

---

## Forma del resultado

Cada método de conversión devuelve un `ConversionResult`:

```ts
interface ConversionResult {
    presignedUrl: string;          // URL firmada para descargar la salida (1 hora)
    objectKey: string;             // clave del objeto en el almacenamiento
    filename: string;              // nombre de archivo del lado del servidor
    fileSize?: number;             // bytes
    conversionTimeSeconds?: number;
    jobId?: string;                // presente cuando hubo sondeo de recuperación
}
```

La URL prefirmada es válida durante una hora. Si necesitas acceso permanente, descarga el archivo (usa `saveTo` o busca la URL tú mismo) y guárdalo en tu propio bucket.

Los resultados de V2 tienen otra forma. Un `PerceiveResult` lleva `operationId`, `status`, `url`, `urlFinal`, `contentHash`, `renderQuality`, `statusCode`, `deductions`, `cacheHit`, un mapa `outputs` indexado por nombre de salida, `structured`, `extractionTier`, `tokens`, `costCents`, `durationMs`, `optionsEcho`, `error` y `warnings`. Cada entrada de `outputs` es un `V2OutputArtifact` de `{ url?, objectKey, sizeBytes, contentType, expiresIn }`, donde `expiresIn` va en segundos y su valor predeterminado es `900`. Por tanto, las URLs de artefactos de V2 duran 15 minutos en lugar de una hora, y se vuelven a firmar en cada lectura, así que llamar de nuevo a `getPerceiveOperation(operationId)` te da enlaces frescos sin volver a renderizar la página.

---

## TypeScript

Las definiciones de tipos se publican con el paquete, así que no hace falta instalar ningún `@types/...`. El paquete se publica de forma dual (ESM + CJS) con `exports`, `types` y `.d.ts` / `.d.cts` correctos, de modo que funciona en cualquier modo de resolución de módulos de Node.

```ts
import type {
    ClientOptions,
    CompressImageOptions,
    ConversionResult,
    ConvertDocumentOptions,
    ConvertImageOptions,
    ConvertToMarkdownOptions,
    ConvertToPdfOptions,
    FileInput,
    JobStatus,
    PdfOptions,
    UrlToMarkdownOptions,
    UrlToPdfOptions,
    UrlToScreenshotOptions,
    // Los tipos de V2 vienen del mismo punto de entrada.
    DiscoverOptions, DiscoverResult,
    DistillOptions, DistillResult,
    IngestJob, IngestOptions,
    LookupOptions, LookupResult,
    PerceiveOptions, PerceiveResult, PerceiveOutputName,
    PerceiveBatchResult, PerceiveDirectResult,
    Watcher, WatcherSnapshotList,
} from "@enconvert/node-sdk";
```

La propia clase `EnconvertV2` también se exporta, por si quieres tipar el parámetro de una función como el espacio de nombres de V2.

---

## Actualización

El paquete incluye una pequeña CLI, `enconvert-sdk`, para mantenerse al día por sí solo.

```bash
npx enconvert-sdk upgrade
```

```bash
npx enconvert-sdk upgrade --dry-run
```

```bash
npx enconvert-sdk version
```

`upgrade` detecta npm, pnpm, yarn o bun a partir del gestor de paquetes del entorno y siempre imprime el comando de instalación exacto antes de ejecutarlo, así que nada le ocurre a tu lockfile sin que lo veas. `--dry-run` imprime ese comando y se detiene. `version` informa de la versión del SDK instalada.

---

## Código fuente e incidencias

- **npm:** [@enconvert/node-sdk](https://www.npmjs.com/package/@enconvert/node-sdk)
- **GitHub:** [enconvert/node-sdk](https://github.com/enconvert/node-sdk)
- **Licencia:** MIT
- **Otros lenguajes:** [Todos los SDKs](/es/docs/guides/integrations/sdks.md)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en Node.js con un paquete de npm?

Instala `@enconvert/node-sdk`, crea un cliente con tu clave de API (`new Enconvert({ apiKey: process.env.ENCONVERT_API_KEY! })`) y llama a un método tipado como `convertUrlToPdf`, `convertImage` o `convertDocument`. Pasa `saveTo` para transmitir el resultado directamente al disco.

### ¿Cómo extraigo una página web a Markdown limpio en Node.js?

Llama a `client.v2.perceive(url, { outputs: ["markdown"] })`. Obtienes una URL firmada de 15 minutos al Markdown en `op.outputs.markdown.url` más una puntuación `renderQuality` de la lectura. Si prefieres los bytes directamente en lugar de una URL, llama a `client.v2.perceiveDirect(url, { outputs: ["markdown"] })` y lee `result.content`.

### ¿Qué es renderQuality y por qué importa?

`renderQuality` es una puntuación de 0.0 a 1.0 adjunta a cada renderizado de V2. Un desafío antibot, un muro de inicio de sesión, una página de error HTTP, un 404 blando o el shell vacío de una SPA puntúan bajo y vuelven con `deductions` y `warnings` con nombre, de modo que una lectura defectuosa queda marcada en lugar de entrar sin ruido en el contexto de tu agente como si fuera la página real. Las puntuaciones por debajo de aproximadamente 0.40 significan que el renderizado falló en la práctica, aunque la solicitud devolviera 200.

### ¿Cómo convierto HEIC a WebP en Node.js?

Llama a `convertImage` con el archivo HEIC (una ruta o un objeto de búfer `{ data, filename }`) y `outputFormat: "webp"`. El SDK convierte entre `jpeg`, `png`, `svg`, `heic` y `webp`; el formato de entrada se detecta a partir de la extensión del nombre de archivo.

### ¿Cómo comprimo una imagen en Node.js sin cambiar su formato?

Llama a `compressImage` con un archivo `.png`, `.jpg`, `.jpeg` o `.webp`. La salida conserva el formato y la extensión de la entrada, elimina los metadatos preservando el perfil ICC y la orientación EXIF, y nunca es mayor que la entrada. Añade `targetSizeKb` para reducir la escala hacia un presupuesto de tamaño; el objetivo es de mejor esfuerzo, así que lee `result.fileSize` para ver qué se logró realmente.

### ¿Cómo convierto cualquier documento a Markdown para un pipeline de RAG?

Llama a `convertToMarkdown` con el archivo y pasa `saveTo` para escribir el `.md` directamente en disco. Acepta 22 extensiones entre Office, OpenDocument, PDF, EPUB, HTML, CSV y texto plano, y devuelve un único archivo Markdown consciente de los encabezados, de modo que tu chunker puede dividir por los encabezados del propio documento en lugar de por recuentos arbitrarios de caracteres.

### ¿Cómo convierto un sitio web entero en fragmentos listos para RAG en Node.js?

Llama a `client.v2.ingest({ mode: "sitemap", url, maxPages, chunk: { maxWords: 512, sentenceOverlap: 1 } })`. Ingest siempre es asíncrono, así que sondea `client.v2.getIngestJob(job.jobId)` hasta que `status` sea `"completed"` y lee `outputUrl` para el JSONL firmado, o fija `webhookUrl` y deja que el webhook de finalización te avise. Para documentos locales en lugar de un sitio, `client.v2.ingestFiles([...])` ejecuta el mismo pipeline.

### ¿Cómo extraigo JSON estructurado de una página en Node.js?

Llama a `client.v2.distill({ urls, schema })` donde `schema` es o bien un objeto JSON Schema o bien un mapa plano `{ campo: descripción }`. Añade un `cssSchema` y la pasada de selectores responde todo lo que puede antes de que algo escale al nivel de LLM; `result.extractionTier`, `fieldsFromCss` y `fieldsFromLlm` te dicen qué nivel hizo el trabajo.

### ¿Cómo monitorizo los cambios de una página web en Node.js?

Llama a `client.v2.createWatcher(url, { frequencyMinutes: 60, diffMode: "auto", webhookUrl })`. El mínimo de una hora es estricto, así que 60 es la cadencia mínima. Lee el historial con `getWatcherSnapshots`, pausa con `updateWatcher(id, { status: "paused" })` y elimina con `deleteWatcher`, que es un borrado suave e idempotente.

### ¿Cómo gestiona el SDK las conversiones largas que chocan con el tiempo de espera del proxy inverso?

Antes de cada solicitud de conversión de V1, el SDK genera un UUID y lo envía como `job_id`; si la solicitud devuelve 5xx, sondea en silencio `GET /v1/convert/status/{job_id}` cada 3 segundos hasta que el trabajo sea `success` o `failed`. El plazo de sondeo es de 5 minutos, tras los cuales lanza `APIError(504, "Conversion timed out")`. V2 usa identificadores de trabajo explícitos, sondeados con `getPerceiveBatch` o `getIngestJob`.

### ¿Puedo usar el SDK de Node.js en una aplicación de navegador?

No, el SDK es solo del lado del servidor, porque se autentica con una clave de API privada (`sk_...`) que nunca debe empaquetarse en código del lado del cliente. Funciona en Node 18+, Bun y Deno vía el especificador de npm.

### ¿Cuánto tiempo es válida la URL de descarga prefirmada?

El `presignedUrl` de cada `ConversionResult` es válido durante una hora. Las URLs de artefactos de V2 son válidas durante 15 minutos y se vuelven a firmar en cada lectura, así que `getPerceiveOperation(operationId)` te entrega enlaces frescos. Para acceso permanente, descarga el archivo y guárdalo en tu propio bucket.
