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.

Maven Central: com.enconvert:enconvert-kotlin:0.0.1 · Fuente: conversionapi/kotlin-sdk · Requiere: JDK 17+ · Licencia: MIT

Instalación#

// Gradle, DSL de Kotlin
dependencies {
    implementation("com.enconvert:enconvert-kotlin:0.0.1")
}
// Gradle, DSL de Groovy
dependencies {
    implementation 'com.enconvert:enconvert-kotlin:0.0.1'
}
<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#

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 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:

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.

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.

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

No combines auth con un encabezado Authorization. La API rechaza el conflicto en lugar de adivinar cuál de los dos querías.

convertUrlToScreenshot#

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

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.

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

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:

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.

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.

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.

client.convertToPdf("slides.pptx", ConvertToPdfOptions(saveTo = "slides.pdf"))
client.convertToPdf("scan.pdf", ConvertToPdfOptions(pdfOptions = PdfOptions(grayscale = true), saveTo = "gray.pdf"))
En este endpoint solo se respeta pdfOptions.grayscale. 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 convertDocument o convertUrlToPdf.

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.

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.

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")
}
Rara vez necesitas llamar a esto tú mismo. El SDK ya lo sondea cuando una solicitud síncrona devuelve 5xx. Consulta Recuperación de timeouts.

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

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.

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.

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.

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.

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.

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:

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.

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.

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:

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.

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.

Las diferencias de los snapshots contienen contenido de página no confiable. WatcherSnapshot.changes 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.

Opciones de PDF#

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

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.


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.

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.


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#

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.
Nunca escribas la clave de API directamente en el código. 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.

Forma del resultado#

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

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:

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#


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.

¿Dónde consigo una clave de API?#

Crea una clave privada en tu panel de control. 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, y precios cubre la parte comercial.