---
seo_title: SDK Kotlin de Conversión de Archivos: Cliente JVM | EnConvert
meta_desc: SDK oficial de EnConvert para Kotlin y JDK 17+. Data classes idiomáticas para convertir archivos y para perceive, discover, distill, ingest y watch.
keywords: sdk de conversión de archivos para kotlin, convertir archivos en kotlin, url a pdf en kotlin, api de web scraping con kotlin, docx a pdf en kotlin, enconvert kotlin sdk, cliente api de conversión de archivos para jvm, heic a webp en kotlin, página web a markdown en kotlin, pipeline de ingesta rag en kotlin, monitorizar cambios de una web con kotlin, librería de conversión en maven central
---

# SDK de Conversión de Archivos para Kotlin

`com.enconvert:enconvert-kotlin` es el cliente oficial de EnConvert para Kotlin y la JVM, compilado contra JDK 17. Convierte archivos entre 43 pares de formatos implementados (DOCX a PDF, HEIC a WebP, JSON a YAML, URL a PDF, cualquier cosa a Markdown) y lee páginas web en vivo para transformarlas en Markdown, JSON, capturas de pantalla y JSONL listo para RAG a través del espacio de nombres `client.v2`. Las opciones y las respuestas son data classes idiomáticas de Kotlin con argumentos con nombre y valores por defecto sensatos, el HTTP viaja sobre el propio `java.net.http.HttpClient` del JDK, y las conversiones lentas se recuperan de los timeouts del proxy inverso sondeando el estado del job.

<div class="alert alert-info">
<strong>Maven Central:</strong> <code>com.enconvert:enconvert-kotlin:0.0.1</code> · <strong>Fuente:</strong> <a href="https://github.com/conversionapi/kotlin-sdk">conversionapi/kotlin-sdk</a> · <strong>Requiere:</strong> JDK 17+ · <strong>Licencia:</strong> MIT
</div>

---

## Instalación

```kotlin
// Gradle, DSL de Kotlin
dependencies {
    implementation("com.enconvert:enconvert-kotlin:0.0.1")
}
```

```groovy
// Gradle, DSL de Groovy
dependencies {
    implementation 'com.enconvert:enconvert-kotlin:0.0.1'
}
```

```xml
<dependency>
  <groupId>com.enconvert</groupId>
  <artifactId>enconvert-kotlin</artifactId>
  <version>0.0.1</version>
</dependency>
```

La única dependencia en tiempo de ejecución es `org.jetbrains.kotlinx:kotlinx-serialization-json`. Todo lo demás viene del JDK: las solicitudes salen por `java.net.http.HttpClient` y los cuerpos multipart los ensambla el propio SDK. La toolchain apunta a JVM 17, así que funciona cualquier runtime JDK 17 o posterior.

---

## Inicio rápido

```kotlin
import com.enconvert.Enconvert
import com.enconvert.PerceiveOptions
import com.enconvert.PerceiveOutputName
import com.enconvert.UrlToPdfOptions

fun main() {
    val client = Enconvert(apiKey = System.getenv("ENCONVERT_API_KEY"))

    // Convierte una página en vivo a PDF y transmítela directamente a disco.
    val pdf = client.convertUrlToPdf("https://example.com", UrlToPdfOptions(saveTo = "page.pdf"))
    println(pdf.presignedUrl)

    // Lee la misma página como debería hacerlo tu agente, con una puntuación de calidad adjunta.
    val op = client.v2.perceive(
        "https://example.com",
        PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN, PerceiveOutputName.STRUCTURED)),
    )
    println("${op.outputs["markdown"]?.url} ${op.renderQuality}") // p. ej. 0.93
}
```

Todos los tipos de EnConvert viven en el paquete `com.enconvert`, y los fragmentos siguientes omiten los imports; los tipos del JDK como `java.nio.file.Files` y `java.nio.file.Path` aparecen sin cualificar por la misma razón. Todos los métodos bloquean, porque no hay funciones `suspend`, así que desde una corrutina envuelve la llamada en `withContext(Dispatchers.IO)`. El cliente se autentica con una clave de API privada enviada en el encabezado `X-API-Key`, lo que lo hace de uso exclusivo del lado del servidor: nunca incluyas la clave dentro de una app de Android ni de nada más que distribuyas. Consulta [Autenticación](/es/docs/authentication) para conocer los tipos de clave.

---

## Qué expone el cliente

`Enconvert` es toda la superficie. La conversión de archivos vive en el propio cliente; la inteligencia web vive en el espacio de nombres `v2`, al que se accede como `client.v2`.

| Superficie | Cómo se accede | Cubre |
|------------|----------------|-------|
| Conversión de archivos | `client.<method>()` | URL a PDF, captura de pantalla, Markdown; pares de imagen y de documento; cualquier cosa a PDF y cualquier cosa a Markdown; lotes de sitios completos; estado de job y de lote |
| Inteligencia web | `client.v2.<method>()` | Perceive, discover, lookup, distill, ingest, watch: 23 métodos sobre 21 endpoints REST |

Doce métodos de conversión se corresponden con la API REST descrita en la [visión general de endpoints](/es/docs/endpoints-overview):

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

Los cuatro métodos de subida de archivos tienen cuatro sobrecargas cada uno. El primer argumento puede ser una ruta `String`, un `java.nio.file.Path`, un `ByteArray` a secas (el nombre de archivo será `upload.bin` por defecto) o un `FileInput(data, filename, contentType?)` cuando tienes bytes crudos y quieres nombrarlos tú mismo.

---

## Conversión de archivos

### convertUrlToPdf

Renderiza cualquier URL pública a PDF.

```kotlin
val result = client.convertUrlToPdf(
    "https://example.com/report",
    UrlToPdfOptions(
        render = UrlRenderOptions(viewportWidth = 1440),
        singlePage = false,
        pdfOptions = PdfOptions(pageSize = "A4", orientation = PdfOrientation.LANDSCAPE, margins = PdfMargins(top = 10.0)),
        saveTo = "report.pdf",
    ),
)
println("${result.filename} ${result.fileSize}")
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `render` | `UrlRenderOptions` | `UrlRenderOptions()` | Viewport, medios, desplazamiento, nombre de archivo, acceso del navegador. |
| `saveTo` | `String?` | -- | Ruta local a la que transmitir el PDF. Los directorios padre se crean por ti. |
| `singlePage` | `Boolean` | `true` | `true` produce una única página continua. `false` pagina usando `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions?` | -- | Geometría de página. Consulta [Opciones de PDF](#opciones-de-pdf). |

`UrlRenderOptions` es compartido por todas las conversiones basadas en URL. Lleva `viewportWidth` y `viewportHeight` (1920 x 1080 por defecto), `loadMedia` y `enableScroll` (ambos `true` por defecto, esperando a los medios y desplazando de arriba abajo para que se disparen los cargadores diferidos), `outputFilename` y tres campos de acceso del navegador: `auth` (un `HttpBasicAuth`), `cookies` (una `List<BrowserCookie>`, máximo 50) y `headers` (máximo 20, con los encabezados salto a salto rechazados).

<div class="alert alert-warning">
<strong>No combines <code>auth</code> con un encabezado <code>Authorization</code>.</strong> La API rechaza el conflicto en lugar de adivinar cuál de los dos querías.
</div>

### convertUrlToScreenshot

Captura un PNG de cualquier URL. `UrlToScreenshotOptions` lleva únicamente `render` y `saveTo`.

```kotlin
client.convertUrlToScreenshot(
    "https://example.com",
    UrlToScreenshotOptions(render = UrlRenderOptions(viewportWidth = 1440), saveTo = "shot.png"),
)
```

### convertUrlToMarkdown

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

```kotlin
val article = client.convertUrlToMarkdown("https://example.com/post", UrlToMarkdownOptions(saveTo = "article.md"))
println(article.presignedUrl)
```

Cuando además quieras una puntuación de calidad, un paquete de artefactos o extracción estructurada del mismo renderizado, usa [`client.v2.perceive`](#perceive) en su lugar.

### convertImage

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp` en cualquier dirección, o rasteriza un PDF a JPEG. `ConvertImageOptions` recibe un `outputFormat` obligatorio más los opcionales `saveTo` y `outputFilename`.

```kotlin
client.convertImage("photo.heic", ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp"))
client.convertImage(Path.of("logo.svg"), ConvertImageOptions(outputFormat = "png", saveTo = "logo.png"))

val bytes = Files.readAllBytes(Path.of("photo.heic"))
client.convertImage(
    FileInput(data = bytes, filename = "photo.heic"),
    ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp"),
)
```

El formato de entrada se resuelve a partir de la extensión del nombre de archivo. El formato de salida se normaliza por ti, así que `"jpg"` se resuelve como `jpeg`. Los pares no admitidos lanzan `EnconvertException` antes de cualquier llamada de red, con las salidas válidas enumeradas en el mensaje. Puedes preguntarle directamente a la tabla de formatos:

```kotlin
validOutputsFor("pdf")  // [jpeg]
validOutputsFor("json") // [csv, toml, xml, yaml]
validOutputsFor("heic") // [jpeg, png, svg, webp]
```

### convertDocument

Convierte documentos y formatos de datos. `outputFormat` es `"pdf"` por defecto.

```kotlin
client.convertDocument("report.docx", ConvertDocumentOptions(saveTo = "report.pdf"))
client.convertDocument("data.json", ConvertDocumentOptions(outputFormat = "yaml", saveTo = "data.yaml"))
client.convertDocument(
    "README.md",
    ConvertDocumentOptions(
        outputFormat = "pdf",
        pdfOptions = PdfOptions(pageSize = "A4", margins = PdfMargins(top = 20.0, bottom = 20.0)),
        saveTo = "readme.pdf",
    ),
)
```

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

Los 43 pares implementados, exactamente como los filtra el SDK:

| Entrada | Salidas |
|---------|---------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `csv` | `json`, `xml` |
| `yaml` | `json` |
| `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | entre sí, los 20 pares |
| `pdf` | `jpeg` |

EPUB no tiene un par de documento dedicado. Envía los archivos `.epub` a través de `convertToPdf` o `convertToMarkdown`. Las opciones son `outputFormat`, `saveTo`, `outputFilename` y `pdfOptions` (respetada solo cuando la salida es PDF).

### convertToMarkdown

Convierte un archivo subido de casi cualquier formato de documento a Markdown limpio. El formato se detecta automáticamente en el servidor, así que el SDK no ejecuta ninguna comprobación de extensión y sube el archivo tal cual.

```kotlin
client.convertToMarkdown("handbook.docx", ConvertToMarkdownOptions(saveTo = "handbook.md"))
```

**Se aceptan:** PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, además de los formatos antiguos de Office y de ODF. Aquí las imágenes no están admitidas.

La salida es un único archivo `.md` consciente de los encabezados, lo que lo convierte en una primera etapa natural para un pipeline de RAG: un chunker semántico puede dividir por la propia jerarquía de encabezados del documento en lugar de por recuentos arbitrarios de caracteres. Este endpoint no tiene opciones de PDF; `saveTo` y `outputFilename` son las únicas opciones.

### convertToPdf

Convierte a PDF un archivo subido de casi cualquier formato. La entrada aceptada cubre Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB y un PDF existente que pasa de largo.

```kotlin
client.convertToPdf("slides.pptx", ConvertToPdfOptions(saveTo = "slides.pdf"))
client.convertToPdf("scan.pdf", ConvertToPdfOptions(pdfOptions = PdfOptions(grayscale = true), saveTo = "gray.pdf"))
```

<div class="alert alert-warning">
<strong>En este endpoint solo se respeta <code>pdfOptions.grayscale</code>.</strong> El tamaño de página, la orientación, los márgenes, la escala, el encabezado y el pie se ignoran aquí. Cuando necesites la geometría de página completa, pasa por <code>convertDocument</code> o <code>convertUrlToPdf</code>.
</div>

### convertWebsiteToPdf y convertWebsiteToScreenshot

Descubre todas las páginas de un sitio web, convierte cada una en segundo plano y recoge un único ZIP. Ambos son asíncronos: devuelven un `BatchSubmission` de inmediato, y tú sondeas con `getBatchStatus` o bloqueas con `waitForBatch`.

```kotlin
val batch = client.convertWebsiteToPdf(
    "https://example.com",
    WebsiteToPdfOptions(
        website = WebsiteConversionOptions(crawlMode = CrawlMode.SITEMAP, excludePatterns = listOf("/tag/")),
    ),
)
val status = client.waitForBatch(batch.batchId, WaitForBatchOptions(saveTo = "site.zip"))
println("${status.completed} of ${status.total} converted, ${status.failed} failed")
```

`convertWebsiteToScreenshot` funciona de forma idéntica y produce un ZIP de PNG. `WebsiteConversionOptions` lleva `render`, `crawlMode` (`AUTO`, `SITEMAP`, `FULL`), `includePatterns`, `excludePatterns`, `notificationEmail` y `callbackUrl`. `waitForBatch` sondea cada 5 segundos y se rinde a los 30 minutos, ambos valores modificables mediante `WaitForBatchOptions(intervalMs, timeoutMs, saveTo)`; al vencer el plazo lanza `ApiException` con estado `504`.

### getJobStatus

Sondea un único job de conversión asíncrono o recuperado.

```kotlin
val status = client.getJobStatus("job_abc123")
when (status.status) {
    JobStatusValue.SUCCESS -> println(status.presignedUrl)
    JobStatusValue.FAILED -> System.err.println(status.error)
    JobStatusValue.PROCESSING -> println("still running")
}
```

<div class="alert alert-info">
<strong>Rara vez necesitas llamar a esto tú mismo.</strong> El SDK ya lo sondea cuando una solicitud síncrona devuelve 5xx. Consulta <a href="#timeout-recovery">Recuperación de timeouts</a>.
</div>

---

## Inteligencia web (V2)

Cada lectura V2 lleva `renderQuality`, una puntuación de 0.0 a 1.0 que describe con qué limpieza se renderizó realmente la página. Una página de desafío, un muro de cookies, una barrera de inicio de sesión o el armazón vacío de una SPA vuelven con una puntuación baja, con un mapa `deductions` relleno que nombra qué comprobaciones se activaron y con `warnings`, mientras que el contenido en sí se sigue devolviendo. Una mala lectura queda marcada en lugar de entrar en silencio en el contexto de tu agente. `statusCode` informa del estado HTTP de la respuesta final del documento principal, y `contentHash` te permite saber que nada ha cambiado desde la última lectura. Empieza por la [visión general de V2](/es/docs/v2-overview) para conocer los conceptos detrás de las seis capacidades.

| Capacidad | Métodos en `client.v2` |
|-----------|------------------------|
| Perceive | `perceive`, `getPerceiveOperation`, `perceiveBatch`, `getPerceiveBatch`, `perceiveDirect`, `downloadPerceiveArtifact` |
| Discover | `discover` |
| Lookup | `lookup` |
| Distill | `distill` |
| Ingest | `ingest`, `ingestFiles`, `listIngestJobs`, `getIngestJob`, `cancelIngestJob`, `retryIngestWebhook`, `getWebhookSecret`, `rotateWebhookSecret` |
| Watch | `createWatcher`, `listWatchers`, `getWatcher`, `getWatcherSnapshots`, `updateWatcher`, `deleteWatcher` |

### Perceive

Renderiza una URL en los artefactos que pidas. Es síncrono: la llamada devuelve una operación completada cuyas URL de artefacto están firmadas durante 15 minutos. Referencia completa en [Perceive](/es/docs/v2-perceive).

```kotlin
val op = client.v2.perceive(
    "https://example.com/pricing",
    PerceiveOptions(
        outputs = listOf(PerceiveOutputName.MARKDOWN, PerceiveOutputName.SCREENSHOT_FULL_PAGE, PerceiveOutputName.STRUCTURED),
        extract = listOf(PerceiveExtractName.TABLES, PerceiveExtractName.METADATA),
        viewport = PerceiveViewport(width = 1440),
        waitFor = "css:.pricing-table",
    ),
)

if ((op.renderQuality ?: 0.0) < 0.5) System.err.println("Low quality read: ${op.deductions} ${op.warnings}")
println(op.outputs["markdown"]?.url)
println(op.structured)
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `outputs` | `List<PerceiveOutputName>?` | `[MARKDOWN, STRUCTURED]` | `MARKDOWN`, `HTML_CLEANED`, `HTML_RAW`, `SCREENSHOT`, `SCREENSHOT_FULL_PAGE`, `PDF`, `LINKS`, `IMAGES`, `STRUCTURED`. |
| `extract` | `List<PerceiveExtractName>?` | -- | `TABLES`, `PRICES`, `CONTACTS`, `METADATA`, `MAIN_CONTENT`, `HEADINGS`, `STRUCTURED_DATA`, `TECHNOLOGIES`, `ALL`. |
| `schema` | `Map<String, Any?>?` | -- | Schema JSON para la extracción estructurada. |
| `waitFor` / `waitTimeoutMs` | `String?` / `Int?` | -- / `30000` | Un selector CSS (opcionalmente con el prefijo `css:`) o `js:<expr>` que esperar, y su presupuesto, de 0 a 60000. |
| `jsCode` | `String?` | -- | JavaScript ejecutado tras la navegación, máximo 20000 caracteres. |
| `viewport` | `PerceiveViewport?` | 1920 x 1080 | `width` de 320 a 3840, `height` de 240 a 2160. |
| `headers` / `cookies` / `auth` | -- | -- | Encabezados adicionales, cookies inyectadas, credenciales HTTP Basic. |
| `cacheMode` | `PerceiveCacheMode?` | `ENABLED` | `ENABLED` (caché de 1 hora), `BYPASS`, `REFRESH`. |
| `pdfOptions` | `PdfOptions?` | -- | Solo tiene sentido cuando `outputs` incluye `PDF`. |
| `blockResources` | `List<PerceiveResourceType>?` | -- | Tipos de recurso que el navegador no debería cargar. |
| `respectRobots` / `mobile` | `Boolean?` | -- | Respetar `robots.txt`; renderizar con un perfil móvil. |
| `onlyMainContent` | `Boolean?` | `true` | Elimina la navegación, el encabezado, el pie y los banners de cookies del artefacto Markdown y del extract `main_content`. |
| `directDownload` | `Boolean?` | -- | Devuelve los bytes del artefacto en lugar de un sobre JSON. Es preferible `perceiveDirect`. |

`proxyUrl`, `geolocation` y `actionChain` existen en `PerceiveOptions` pero todavía no están disponibles en el servidor, y actualmente se rechazan con `422`.

Agrupa hasta 1000 URL detrás de un único bloque de opciones compartido. Los lotes pequeños se completan en línea; los más grandes vuelven en `QUEUED`, así que sondea el id del job. `getPerceiveOperation` vuelve a firmar las URL de artefacto de cualquier operación anterior.

```kotlin
val batch = client.v2.perceiveBatch(
    listOf("https://a.example.com", "https://b.example.com"),
    PerceiveBatchOptions(
        options = PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN)),
        outputMode = PerceiveBatchOutputMode.ZIP,
    ),
)

var job = client.v2.getPerceiveBatch(batch.jobId)
while (job.status == PerceiveBatchStatus.QUEUED || job.status == PerceiveBatchStatus.PROCESSING) {
    Thread.sleep(5_000)
    job = client.v2.getPerceiveBatch(batch.jobId)
}
println("${job.completed}/${job.total} done, zip at ${job.zip?.url}")

val again = client.v2.getPerceiveOperation(op.operationId) // URL recién firmadas
```

Cuando quieres los bytes y nada más, `perceiveDirect` devuelve el artefacto en la misma solicitud y se salta el viaje de ida y vuelta de la URL firmada. Necesita exactamente una salida que produzca artefacto, es decir, cualquiera excepto `STRUCTURED`, y lanza `EnconvertException` localmente si pides cero o más de una.

```kotlin
val direct = client.v2.perceiveDirect("https://example.com", PerceiveOptions(outputs = listOf(PerceiveOutputName.PDF)))
Files.write(Path.of(direct.filename ?: "page.pdf"), direct.content)
println("${direct.renderQuality} ${direct.sourceStatusCode} ${direct.warningsCount}")

// Vuelve a descargar un artefacto almacenado de una operación anterior.
val markdown = client.v2.downloadPerceiveArtifact(op.operationId, PerceiveOutputName.MARKDOWN)
println(markdown.content.toString(Charsets.UTF_8))
```

`downloadPerceiveArtifact` acepta un `output` nulo cuando la operación produjo exactamente un artefacto, y devuelve `410` una vez que el artefacto almacenado supera su ventana de retención.

### Discover

Enumera las URL de un sitio sin renderizado de navegador alguno. Referencia completa en [Discover](/es/docs/v2-discover).

```kotlin
val found = client.v2.discover(
    "https://example.com",
    DiscoverOptions(mode = DiscoverMode.HYBRID, maxUrls = 200, maxDepth = 3, excludePatterns = listOf("/tag/")),
)
println("${found.total} urls, truncated=${found.truncated}, sources=${found.sources}")
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `mode` | `DiscoverMode?` | `HYBRID` | `SITEMAP`, `CRAWL` o `HYBRID` (sitemap más rastreo HTTP). |
| `maxUrls` / `maxDepth` | `Int?` | `100` / `2` | De 1 a 1000 y de 1 a 5. |
| `includePatterns` / `excludePatterns` | `List<String>?` | -- | Lista de permitidos y lista de bloqueados por regex, máximo 50 entradas cada una. La lista de bloqueados se aplica en segundo lugar. |
| `sameDomainOnly` | `Boolean?` | `true` | Permanece en el dominio semilla. |
| `respectRobots` | `Boolean?` | -- | Respetar `robots.txt`. |

`DiscoverResult.sources` informa de los recuentos crudos por fuente antes de la deduplicación, por ejemplo `{sitemap=42, crawl=30}`.

### Lookup

Ejecuta una búsqueda web categorizada y, opcionalmente, aplica perceive a los primeros resultados en la misma llamada. Referencia completa en [Lookup](/es/docs/v2-lookup).

```kotlin
val search = client.v2.lookup(
    "best static site generators",
    LookupOptions(category = LookupCategory.WEB, numResults = 10, country = "us", timeFilter = LookupTimeFilter.MONTH, perceiveTop = 3),
)

for (hit in search.results) {
    println("${hit.position}. ${hit.title} ${hit.url}")
    hit.perceive?.let { println("   rendered at quality ${it.renderQuality}") }
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `category` | `LookupCategory?` | `WEB` | `WEB`, `NEWS`, `IMAGES`, `SCHOLAR`, `PATENTS`, `MAPS`. |
| `country` / `locale` | `String?` | -- | Código de país `gl` de Google e idioma de interfaz `hl`. |
| `timeFilter` | `LookupTimeFilter?` | -- | `HOUR`, `DAY`, `WEEK`, `MONTH`, `YEAR`. |
| `numResults` / `page` | `Int?` | `10` / `1` | De 1 a 100 y de 1 a 10. |
| `location` | `String?` | -- | Ubicación en texto libre, por ejemplo `"Austin, Texas"`. |
| `autocorrect` | `Boolean?` | `true` | Deja que el proveedor corrija las erratas. |
| `perceiveTop` | `Int?` | `0` | Aplica perceive automáticamente a las N primeras URL de resultado, de 0 a 10. Cada una ejecuta un renderizado completo de navegador. |

El resultado también lleva `answerBox`, `knowledgeGraph`, `perceiveOperationIds` y `total`.

### Distill

Apunta un schema hacia unas páginas y recibe datos estructurados. Una pasada CSS opcional responde todo lo que puede antes de que nada escale al nivel LLM. Referencia completa en [Distill](/es/docs/v2-distill).

```kotlin
val extraction = client.v2.distill(
    DistillOptions(
        urls = listOf("https://example.com/pricing"),
        schema = mapOf("plans" to "list of plan names with monthly prices"),
        cssSchema = CssSchema(
            baseSelector = ".plan-card",
            fields = listOf(
                CssField(name = "name", type = CssFieldType.TEXT, selector = "h3"),
                CssField(name = "price", type = CssFieldType.TEXT, selector = ".price"),
            ),
            targetField = "plans",
        ),
    ),
)

for (item in extraction.results) {
    println("${item.url} tier=${item.extractionTier} css=${item.fieldsFromCss} llm=${item.fieldsFromLlm}")
    println(item.data)
}
```

O descubre primero las URL y destila cada una:

```kotlin
client.v2.distill(
    DistillOptions(
        discoverFrom = DistillDiscoverFrom(url = "https://example.com", mode = DiscoverMode.SITEMAP, maxPages = 10),
        schema = mapOf("title" to "page title", "summary" to "one-line summary"),
    ),
)
```

Proporciona exactamente uno de `urls` o `discoverFrom`. Pasar ambos, o ninguno, lanza `EnconvertException` antes de que la solicitud salga de tu proceso.

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `urls` | `List<String>?` | -- | URL explícitas que destilar, máximo 50. |
| `discoverFrom` | `DistillDiscoverFrom?` | -- | Descubre primero las URL de un sitio. `maxPages` va de 1 a 50, con 10 por defecto. |
| `schema` | `Map<String, Any?>` | obligatorio | Un objeto JSON Schema, o un mapa plano `{field to description}`. |
| `cssSchema` | `CssSchema?` | -- | Pasada CSS ejecutada antes de cualquier escalada al LLM. |
| `waitFor` / `waitTimeoutMs` | `String?` / `Int?` | -- / `30000` | Selector o expresión `js:` que esperar, y su presupuesto. |
| `headers` / `cookies` / `respectRobots` | -- | -- | Los mismos controles de renderizado que perceive. |

`CssField.type` es uno de `TEXT`, `ATTRIBUTE`, `HTML`, `REGEX`, `NESTED`, `LIST`, `NESTED_LIST`. `ATTRIBUTE` requiere `attribute`, `REGEX` requiere `pattern`, y los tres tipos anidados requieren una lista `fields` no vacía, hasta cinco niveles de profundidad.

### Ingest

Convierte un sitio entero, o un montón de documentos subidos, en JSONL troceado y listo para RAG a través de un único pipeline. Ingest es siempre asíncrono. Referencia completa en [Ingest](/es/docs/v2-ingest).

```kotlin
val job = client.v2.ingest(
    IngestOptions(
        mode = IngestMode.SITEMAP,
        url = "https://docs.example.com",
        maxPages = 100,
        chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1),
        webhookUrl = "https://my.app/hooks/enconvert",
    ),
)

var state = client.v2.getIngestJob(job.jobId)
while (state.status !in setOf(IngestStatus.COMPLETED, IngestStatus.FAILED, IngestStatus.CANCELED)) {
    Thread.sleep(10_000)
    state = client.v2.getIngestJob(job.jobId)
}
if (state.status == IngestStatus.COMPLETED) println("${state.totalChunks} chunks at ${state.outputUrl}")
```

`mode` es `URLS` por defecto, lo que exige una lista `urls` no vacía y rechaza `url`. `SITEMAP` y `CRAWL` exigen una `url` semilla y rechazan `urls`. El SDK aplica ambas reglas localmente y lanza `EnconvertException` en lugar de enviar una solicitud que no puede tener éxito.

Los archivos subidos pasan por `ingestFiles`, que comparte el mismo ciclo de vida de job bajo el modo `FILES` y acepta PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, y documentos antiguos de Office y de ODF.

```kotlin
val paths = listOf(Path.of("handbook.pdf"), Path.of("notes.docx"))
val fileJob = client.v2.ingestFiles(
    paths.map { FileInput(data = Files.readAllBytes(it), filename = it.fileName.toString()) },
    IngestFilesOptions(chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1)),
)
println(fileJob.jobId)
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `mode` | `IngestMode?` | `URLS` | `URLS`, `SITEMAP`, `CRAWL`, `FILES`. |
| `url` / `urls` | `String?` / `List<String>?` | -- | URL semilla para `SITEMAP` y `CRAWL`; URL explícitas (máximo 1000) para `URLS`. |
| `maxPages` / `maxDepth` | `Int?` | `50` / `2` | Límites de descubrimiento, de 1 a 1000 y de 1 a 5. |
| `sameDomainOnly` | `Boolean?` | `true` | Permanece en el dominio semilla. |
| `includePatterns` / `excludePatterns` / `respectRobots` | -- | -- | Los mismos controles de descubrimiento que `discover`. |
| `waitFor` / `waitTimeoutMs` | `String?` / `Int?` | -- / `30000` | Espera de renderizado por página. |
| `chunk` | `IngestChunkOptions?` | -- | `maxWords` de 32 a 4000 (512 por defecto), `sentenceOverlap` de 0 a 10 (1 por defecto). |
| `webhookUrl` | `String?` | -- | Webhook de finalización, firmado con HMAC. |

Gestión de jobs y fontanería de webhooks:

```kotlin
client.v2.listIngestJobs(V2ListOptions(limit = 20))   // de los más recientes a los más antiguos
client.v2.cancelIngestJob(job.jobId)                  // idempotente

val secret = client.v2.getWebhookSecret()
println("${secret.signatureHeader} ${secret.signatureScheme} ${secret.replayToleranceSeconds}s")

client.v2.rotateWebhookSecret()         // las firmas antiguas dejan de verificarse de inmediato
client.v2.retryIngestWebhook(job.jobId) // reenvía el webhook de un job completado
```

### Watch

Vuelve a renderizar una URL con una cadencia fija y recibe un aviso cuando cambie. Referencia completa en [Watch](/es/docs/v2-watch).

```kotlin
val watcher = client.v2.createWatcher(
    "https://example.com/pricing",
    WatchCreateOptions(
        frequencyMinutes = 60,
        diffMode = WatchDiffMode.AUTO,
        webhookUrl = "https://my.app/hooks/changes",
        notifyEmail = true,
    ),
)

client.v2.listWatchers(V2ListOptions(limit = 20))
for (snap in client.v2.getWatcherSnapshots(watcher.watcherId, SnapshotListOptions(limit = 10)).snapshots) {
    println("${snap.checkedAt} changed=${snap.hasChanges} similarity=${snap.similarity}")
}

client.v2.updateWatcher(watcher.watcherId, WatcherUpdate(status = WatcherUpdateStatus.PAUSED))
client.v2.updateWatcher(watcher.watcherId, WatcherUpdate(webhookUrl = "")) // borra el webhook
client.v2.deleteWatcher(watcher.watcherId)                                 // borrado lógico, idempotente
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `frequencyMinutes` | `Int?` | `60` | Minutos entre comprobaciones, de 60 a 43200. El suelo horario es rígido. |
| `diffMode` | `WatchDiffMode?` | `AUTO` | `AUTO`, `TEXT`, `STRUCTURED`, `TABLES`, `METADATA`. |
| `trackFields` | `Map<String, Any?>?` | -- | Subconjunto de campos o selectores que el motor de diferencias debería vigilar. |
| `webhookUrl` | `String?` | -- | Webhook de cambios, firmado con HMAC usando el mismo secreto que ingest. |
| `notifyEmail` | `Boolean?` | `true` | Avisa por correo al propietario del proyecto cuando hay cambios. |

`updateWatcher` exige al menos un campo y lanza `EnconvertException` ante un `WatcherUpdate` vacío. Su `status` acepta únicamente `ACTIVE` o `PAUSED`; el borrado pasa por `deleteWatcher`, que devuelve el watcher marcado como eliminado con estado `DELETED`. `getWatcher` sobre un watcher eliminado responde `404`.

<div class="alert alert-warning">
<strong>Las diferencias de los snapshots contienen contenido de página no confiable.</strong> <code>WatcherSnapshot.changes</code> es texto crudo extraído de la página vigilada. Escápalo antes de renderizarlo en HTML, en un panel de control o en un mensaje de chat.
</div>

---

## Opciones de PDF

`PdfOptions` es compartido por `convertUrlToPdf`, `convertDocument`, `convertToPdf` (solo la escala de grises), `convertWebsiteToPdf` y `PerceiveOptions.pdfOptions`.

```kotlin
client.convertUrlToPdf(
    "https://example.com",
    UrlToPdfOptions(
        pdfOptions = PdfOptions(
            pageSize = "A4",
            orientation = PdfOrientation.LANDSCAPE,
            margins = PdfMargins(top = 10.0, bottom = 10.0, left = 15.0, right = 15.0),
            scale = 0.9,
            header = PdfHeaderFooter(content = "Quarterly report", height = 12.0),
        ),
        saveTo = "report.pdf",
    ),
)
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `pageSize` | `String?` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` y similares. |
| `pageWidth` / `pageHeight` | `Double?` | Geometría personalizada. Juntos prevalecen sobre `pageSize`. |
| `orientation` | `PdfOrientation?` | `PORTRAIT` o `LANDSCAPE`. Por defecto, vertical. |
| `margins` | `PdfMargins?` | `top`, `bottom`, `left`, `right`, todos ellos dobles opcionales en mm. |
| `scale` | `Double?` | Escala de renderizado, por ejemplo `0.9` para el 90 por ciento. |
| `grayscale` | `Boolean?` | Posprocesa el PDF a escala de grises. |
| `header` / `footer` | `PdfHeaderFooter?` | `content` (máximo 2000 caracteres) y `height`. |

Solo se serializan al cable los campos que realmente estableces, así que un `PdfOptions` parcialmente relleno nunca prevalece sobre un valor por defecto del servidor que no tocaste. La matriz completa de parámetros está en [Parámetros y opciones](/es/docs/parameters-options).

---

## Manejo de errores

Todo fallo es una `EnconvertException` o una subclase, así que un único `catch` puede servirte de red de seguridad mientras las subclases específicas se encargan de los casos que te importan.

```kotlin
try {
    client.v2.perceive("https://example.com")
} catch (e: AuthenticationException) {
    System.err.println("Invalid or missing API key")
} catch (e: QuotaException) {
    System.err.println("Request rejected with 402")
} catch (e: RateLimitException) {
    System.err.println("Too many requests, back off and retry")
} catch (e: ApiException) {
    System.err.println("API error [${e.statusCode}]: ${e.message}")
} catch (e: EnconvertException) {
    System.err.println("Client-side validation failed: ${e.message}")
}
```

| Clase | Se lanza en | Código de estado |
|-------|-------------|------------------|
| `AuthenticationException` | Clave de API inválida, ausente o revocada | `401`, `403` |
| `QuotaException` | Se lanza ante un HTTP 402 | `402` |
| `RateLimitException` | Demasiadas solicitudes | `429` |
| `ApiException` | Cualquier otra respuesta 4xx o 5xx | el código real |
| `EnconvertException` | Clase base, más la validación del lado del cliente, como un par de conversión no admitido o un objeto de opciones mal formado | -- |

`ApiException` expone la propiedad `statusCode` en crudo, y su `message` se renderiza como `[<statusCode>] <server message>`, con el campo `detail` o `error` del servidor extraído del cuerpo JSON. El orden de captura importa: las tres clases específicas extienden `ApiException`, que extiende `EnconvertException`, así que ponlas primero. El mapa de mensajes está documentado en [Códigos de error](/es/docs/error-codes).

---

## Recuperación de timeouts

Los renderizados largos de URL a PDF y las conversiones de documentos grandes pueden sobrevivir a un timeout de proxy inverso de 60 a 120 segundos incluso cuando la conversión tiene éxito en el servidor. El SDK se ocupa de eso en los métodos de conversión V1:

1. Antes de cada solicitud genera un id de job hexadecimal de 32 caracteres y lo envía como `job_id` en el cuerpo JSON o como campo multipart.
2. Si la solicitud vuelve con 5xx, el SDK deja de confiar en la respuesta y sondea `GET /v1/convert/status/{job_id}` cada 3 segundos. Un `404` mientras la fila del job todavía se está escribiendo significa "sigue esperando".
3. Ante `success`, el SDK mapea el payload a un `ConversionResult` normal. Ante `failed`, lanza `ApiException` con el mensaje de error del servidor.
4. El plazo es de 5 minutos, tras el cual lanza `ApiException(504, "Conversion timed out")`.

A las respuestas correctas que omiten `job_id` (la ruta síncrona de URL hace esto) se les rellena el id generado por el cliente, así que `result.jobId` siempre es algo que puedes pasarle a `getJobStatus`. Dos excepciones deliberadas: `convertWebsiteToPdf` y `convertWebsiteToScreenshot` se saltan el respaldo, porque un envío de sitio web no tiene una fila por job y un 5xx ahí significa que falló el propio envío. Los métodos V2 también se lo saltan, ya que cada endpoint V2 tiene su propia historia de sondeo o de webhook.

---

## Configuración

```kotlin
val client = Enconvert(
    apiKey = System.getenv("ENCONVERT_API_KEY"),
    timeout = 300_000,                      // ms, 5 minutos
    baseUrl = "https://api.enconvert.com",  // sobrescribe para un gateway autoalojado
)
```

| Parámetro | Tipo | Por defecto | Descripción |
|-----------|------|-------------|-------------|
| `apiKey` | `String` | obligatorio | Clave de API privada. Un valor en blanco lanza `IllegalArgumentException` desde el constructor. |
| `timeout` | `Long` | `300_000` | Timeout por solicitud en milisegundos, aplicado al `HttpRequest` subyacente. |
| `baseUrl` | `String` | `https://api.enconvert.com` | URL base de la API. Las barras finales se eliminan. |

<div class="alert alert-warning">
<strong>Nunca escribas la clave de API directamente en el código.</strong> Léela desde una variable de entorno, una propiedad de Gradle o tu gestor de secretos, y mantenla fuera de cualquier artefacto que envíes al dispositivo de un usuario. Cualquiera que tenga tu clave privada puede ejecutar solicitudes contra tu proyecto.
</div>

---

## Forma del resultado

Todos los métodos de conversión devuelven un `ConversionResult`:

```kotlin
public data class ConversionResult(
    val presignedUrl: String,
    val objectKey: String,
    val filename: String,
    val fileSize: Long? = null,
    val conversionTimeSeconds: Double? = null,
    val jobId: String? = null,
)
```

La URL prefirmada es un enlace firmado temporal. Pasa `saveTo` si quieres los bytes en disco de inmediato, o descarga la URL tú mismo y guarda el archivo en tu propio bucket para tener acceso permanente.

Las lecturas V2 devuelven en su lugar un `PerceiveResult`, que es donde viven las señales de honestidad:

```kotlin
val op = client.v2.perceive("https://example.com")

op.operationId    // "per_..."
op.status         // PerceiveStatus.COMPLETED
op.url            // URL solicitada; op.urlFinal tras las redirecciones
op.renderQuality  // Double?, de 0.0 a 1.0
op.statusCode     // Int?, estado HTTP del documento principal
op.deductions     // Map<String, Double>, p. ej. {http_error=0.7}. Vacío en un renderizado limpio.
op.warnings       // List<String>
op.cacheHit       // Boolean; op.contentHash es el SHA-256 del contenido renderizado
op.outputs        // Map<String, V2OutputArtifact> indexado por nombre de salida
op.structured     // Map<String, Any?>?, presente cuando se usó extract o schema
op.extractionTier // HEURISTIC, CSS o LLM
op.tokens         // V2Tokens(input, output); op.costCents y op.durationMs a su lado
```

Cada `V2OutputArtifact` lleva `url`, `objectKey`, `sizeBytes`, `contentType` y `expiresIn` (900 segundos). Las URL de artefacto se vuelven a firmar en cada llamada a `getPerceiveOperation`, así que guarda el `operationId`, no la URL. Los payloads sin tipar (schemas de extracción, datos extraídos, campos vigilados, entradas de diferencias, extras de búsqueda) cruzan la frontera como `Map<String, Any?>` y se convierten sin pérdidas en ambas direcciones, así que nada de lo que pones en un schema cambia de forma al salir.

---

## Código fuente e incidencias

- **Maven Central:** `com.enconvert:enconvert-kotlin:0.0.1`
- **GitHub:** [conversionapi/kotlin-sdk](https://github.com/conversionapi/kotlin-sdk)
- **Licencia:** MIT
- **Otros lenguajes:** consulta la [lista completa de SDK](/es/docs/sdks)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en Kotlin?

Añade `com.enconvert:enconvert-kotlin:0.0.1` a tu build de Gradle o Maven, construye `Enconvert(apiKey = System.getenv("ENCONVERT_API_KEY"))` y llama a un método tipado como `convertDocument`, `convertImage` o `convertUrlToPdf`. Pasa `saveTo` en el objeto de opciones para transmitir la salida directamente a un archivo local en lugar de descargar tú mismo la URL prefirmada.

### ¿Cómo convierto DOCX a PDF en Kotlin?

Llama a `client.convertDocument("report.docx", ConvertDocumentOptions(saveTo = "report.pdf"))`. El formato de salida es `pdf` por defecto, así que puedes dejar `outputFormat` sin establecer. El formato de entrada se resuelve a partir de la extensión del archivo, y `.doc` y `.docx` se corresponden con la misma conversión. Para la geometría de página, pasa un `PdfOptions` a través de `ConvertDocumentOptions.pdfOptions`.

### ¿Cómo convierto una URL a PDF en Kotlin?

Llama a `client.convertUrlToPdf(url, UrlToPdfOptions(saveTo = "page.pdf"))`. Establece `singlePage = false` para paginar con `pdfOptions.pageSize`, y usa `UrlRenderOptions` para cambiar el viewport, desactivar la carga de medios o apagar la pasada de desplazamiento que dispara los cargadores diferidos.

### ¿Cómo convierto HEIC a WebP en la JVM?

Llama a `client.convertImage("photo.heic", ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp"))`. Están implementados los 20 pares entre `jpeg`, `png`, `svg`, `heic` y `webp`, más la rasterización de `pdf` a `jpeg`. Un par no admitido lanza `EnconvertException` antes de cualquier llamada de red, y `validOutputsFor("heic")` enumera de antemano los destinos válidos.

### ¿Cómo extraigo una página web a Markdown desde Kotlin?

Dos opciones. `client.convertUrlToMarkdown(url, UrlToMarkdownOptions(saveTo = "page.md"))` te da un archivo Markdown con frontmatter YAML. `client.v2.perceive(url, PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN)))` te da el mismo contenido más `renderQuality`, `deductions`, `warnings` y `statusCode`, que es lo que quieres cuando un agente va a leer el resultado sin supervisión.

### ¿Qué significa renderQuality y cuándo debería rechazar una página?

`renderQuality` va de 0.0 a 1.0 y describe con qué limpieza se renderizó la página, no lo bueno que es el contenido. Las páginas de desafío, las barreras de inicio de sesión, los errores HTTP y los armazones vacíos de SPA la empujan hacia abajo, y `deductions` nombra cada comprobación que se activó, por ejemplo `{http_error=0.7}`. El contenido siempre se devuelve para que puedas inspeccionarlo. Un patrón habitual es tratar cualquier valor por debajo de 0.5 como sospechoso y o bien volver a pedir la página con `cacheMode = PerceiveCacheMode.REFRESH` o bien enviarla a una persona.

### ¿Cómo convierto un sitio de documentación en fragmentos para RAG desde Kotlin?

Llama a `client.v2.ingest(IngestOptions(mode = IngestMode.SITEMAP, url = "https://docs.example.com", chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1)))`. Ingest es siempre asíncrono: sondea `getIngestJob(jobId)` hasta que el estado sea `COMPLETED` y lee `outputUrl` para obtener el JSONL, o establece `webhookUrl` y deja que el webhook de finalización te encuentre. Los documentos locales pasan por `ingestFiles` con los mismos ajustes de troceado.

### ¿El SDK bloquea el hilo que llama?

Sí. Todos los métodos llaman a `HttpClient.send` de forma síncrona, y no hay funciones `suspend` ni constructores de corrutinas en el SDK. `waitForBatch` y el sondeador interno de recuperación de timeouts duermen el hilo actual entre intentos. Desde una corrutina, envuelve las llamadas en `withContext(Dispatchers.IO)`; desde un framework de servidor, mantenlas fuera del pool de hilos que atiende las solicitudes.

### ¿Puedo llamar a este SDK desde Java?

Puedes, porque son clases JVM corrientes, pero los argumentos por defecto de Kotlin no se exponen a Java como sobrecargas, así que quien llame desde Java tiene que pasar todos los argumentos del constructor de una data class de opciones. Si tu base de código es Java, usa mejor el SDK de Java independiente que aparece en la [página de SDK](/es/docs/sdks).

### ¿Dónde consigo una clave de API?

Crea una clave privada en tu [panel de control](/es/dashboard). Se envía en el encabezado `X-API-Key` en cada solicitud, así que mantenla del lado del servidor. Los tipos de clave y sus alcances se explican en [Autenticación](/es/docs/authentication), y [precios](/es/pricing) cubre la parte comercial.
