Kotlin SDK für Dateikonvertierung#

com.enconvert:enconvert-kotlin ist der offizielle EnConvert-Client für Kotlin und die JVM, gebaut gegen JDK 17. Er konvertiert Dateien über 43 implementierte Formatpaare hinweg (DOCX zu PDF, HEIC zu WebP, JSON zu YAML, URL zu PDF, Anything to Markdown), und über den Namespace client.v2 liest er live Webseiten in agentenfertiges Markdown, JSON, Screenshots und RAG-fertiges JSONL ein. Optionen und Antworten sind idiomatische Kotlin-Datenklassen mit benannten Argumenten und sinnvollen Standardwerten, HTTP läuft über den JDK-eigenen java.net.http.HttpClient, und langsame Konvertierungen fangen sich nach Reverse-Proxy-Timeouts wieder, indem sie den Job-Status abfragen.

Maven Central: com.enconvert:enconvert-kotlin:0.0.1 · Quelle: conversionapi/kotlin-sdk · Voraussetzung: JDK 17+ · Lizenz: MIT

Installation#

// Gradle, Kotlin DSL
dependencies {
    implementation("com.enconvert:enconvert-kotlin:0.0.1")
}
// Gradle, Groovy DSL
dependencies {
    implementation 'com.enconvert:enconvert-kotlin:0.0.1'
}
<dependency>
  <groupId>com.enconvert</groupId>
  <artifactId>enconvert-kotlin</artifactId>
  <version>0.0.1</version>
</dependency>

Die einzige Laufzeitabhängigkeit ist org.jetbrains.kotlinx:kotlinx-serialization-json. Alles Übrige kommt aus dem JDK: Anfragen gehen über java.net.http.HttpClient raus, und Multipart-Bodies baut das SDK selbst zusammen. Die Toolchain zielt auf JVM 17, es funktioniert also jede Laufzeitumgebung ab JDK 17.


Schnellstart#

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

    // Eine Live-Seite in ein PDF konvertieren und direkt auf die Platte streamen.
    val pdf = client.convertUrlToPdf("https://example.com", UrlToPdfOptions(saveTo = "page.pdf"))
    println(pdf.presignedUrl)

    // Dieselbe Seite so lesen, wie es dein Agent tun sollte, mit angehängtem Qualitätswert.
    val op = client.v2.perceive(
        "https://example.com",
        PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN, PerceiveOutputName.STRUCTURED)),
    )
    println("${op.outputs["markdown"]?.url} ${op.renderQuality}") // z. B. 0.93
}

Jeder EnConvert-Typ liegt im Paket com.enconvert, und die Beispiele unten lassen die Imports weg; JDK-Typen wie java.nio.file.Files und java.nio.file.Path erscheinen aus demselben Grund unqualifiziert. Jede Methode blockiert, denn es gibt keine suspend-Funktionen. Aus einer Coroutine heraus verpackst du den Aufruf also in withContext(Dispatchers.IO). Der Client authentifiziert sich mit einem privaten API-Key, der als Header X-API-Key gesendet wird. Damit ist er ausschließlich serverseitig: Liefere den Key niemals in einer Android-App oder etwas anderem aus, das du verteilst. Siehe Authentifizierung für die Key-Typen.


Was der Client bereitstellt#

Enconvert ist die gesamte Oberfläche. Die Datei-Konvertierung liegt am Client selbst; die Web-Intelligence liegt im v2-Namespace, erreichbar über client.v2.

Bereich Erreichbar über Deckt ab
Datei-Konvertierung client.<method>() URL zu PDF, Screenshot, Markdown; Bild- und Dokumentpaare; Anything-to-PDF und Anything-to-Markdown; Batches für ganze Websites; Job- und Batch-Status
Web-Intelligence client.v2.<method>() Perceive, Discover, Lookup, Distill, Ingest, Watch: 23 Methoden über 21 REST-Endpunkte

Zwölf Konvertierungsmethoden bilden die REST-API ab, die in der Endpunkt-Übersicht beschrieben ist:

Methode Endpunkt Rückgabe
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} (mit Polling) BatchStatus

Die vier Datei-Upload-Methoden haben jeweils vier Überladungen. Das erste Argument darf ein Pfad-String sein, ein java.nio.file.Path, ein bloßes ByteArray (Dateiname ist dann upload.bin) oder ein FileInput(data, filename, contentType?), wenn du Rohbytes hast und sie selbst benennen willst.


Datei-Konvertierung#

convertUrlToPdf#

Rendere jede öffentliche URL zu 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}")
Option Typ Standard Beschreibung
render UrlRenderOptions UrlRenderOptions() Viewport, Medien, Scrollen, Dateiname, Browser-Zugriff.
saveTo String? -- Lokaler Pfad, auf den das PDF gestreamt wird. Übergeordnete Verzeichnisse werden für dich angelegt.
singlePage Boolean true true erzeugt eine einzige durchgehende Seite. false paginiert anhand von pdfOptions.pageSize.
pdfOptions PdfOptions? -- Seitengeometrie. Siehe PDF-Optionen.

UrlRenderOptions wird von jeder URL-basierten Konvertierung geteilt. Es trägt viewportWidth und viewportHeight (Standard 1920 x 1080), loadMedia und enableScroll (beide standardmäßig true, warten also auf Medien und scrollen von oben nach unten, damit Lazy Loader auslösen), outputFilename sowie drei Felder für den Browser-Zugriff: auth (ein HttpBasicAuth), cookies (eine List<BrowserCookie>, max. 50) und headers (max. 20, Hop-by-Hop-Header werden abgelehnt).

Kombiniere auth nicht mit einem Authorization-Header. Die API weist den Konflikt zurück, statt zu raten, was du gemeint hast.

convertUrlToScreenshot#

Nimm ein PNG von einer beliebigen URL auf. UrlToScreenshotOptions trägt nur render und saveTo.

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

convertUrlToMarkdown#

Extrahiere sauberes GitHub-Flavored Markdown aus einer URL. Der Konverter entfernt Navigation, Fußzeilen, Werbung und Skripte, behält den eigentlichen Artikeltext und stellt ein YAML-Frontmatter mit Titel, Beschreibung, URL, Links und Bildern voran.

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

Wenn du zusätzlich einen Qualitätswert, ein Artefaktbündel oder eine strukturierte Extraktion aus demselben Rendering möchtest, nimm stattdessen client.v2.perceive.

convertImage#

Konvertiere zwischen jpeg, png, svg, heic und webp in jeder Richtung, oder rastere ein PDF nach JPEG. ConvertImageOptions verlangt ein outputFormat und akzeptiert optional saveTo und 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"),
)

Das Eingabeformat ergibt sich aus der Dateiendung. Das Ausgabeformat wird für dich normalisiert, "jpg" löst also zu jpeg auf. Nicht unterstützte Paare werfen EnconvertException, bevor irgendein Netzwerkaufruf stattfindet, mit den gültigen Ausgaben in der Meldung. Du kannst die Formattabelle auch direkt befragen:

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

convertDocument#

Konvertiere Dokumente und Datenformate. outputFormat steht standardmäßig auf "pdf".

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",
    ),
)

Unterstützte Eingaben: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.

Die 43 implementierten Paare, genau so, wie das SDK sie prüft:

Eingabe Ausgaben
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 jeweils untereinander, alle 20 Paare
pdf jpeg

EPUB hat kein eigenes Dokumentpaar. Schicke .epub-Dateien durch convertToPdf oder convertToMarkdown. Die Optionen sind outputFormat, saveTo, outputFilename und pdfOptions (nur berücksichtigt, wenn die Ausgabe ein PDF ist).

convertToMarkdown#

Konvertiere eine hochgeladene Datei nahezu beliebigen Dokumentformats in sauberes Markdown. Das Format wird serverseitig automatisch erkannt, das SDK führt also keine Endungsprüfung durch und lädt die Datei unverändert hoch.

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

Akzeptiert: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere Office- und ODF-Formate. Bilder werden hier nicht unterstützt.

Die Ausgabe ist eine einzige überschriftenbewusste .md-Datei, was sie zur natürlichen ersten Stufe einer RAG-Pipeline macht: Ein semantischer Chunker kann anhand der Überschriftenhierarchie des Dokuments selbst trennen statt an willkürlichen Zeichenzahlen. Dieser Endpunkt kennt keine PDF-Optionen; saveTo und outputFilename sind die einzigen Optionen.

convertToPdf#

Konvertiere eine hochgeladene Datei nahezu beliebigen Formats nach PDF. Akzeptiert werden Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, reiner Text, Rasterbilder, SVG, EPUB und ein bestehendes PDF zum Durchreichen.

client.convertToPdf("slides.pptx", ConvertToPdfOptions(saveTo = "slides.pdf"))
client.convertToPdf("scan.pdf", ConvertToPdfOptions(pdfOptions = PdfOptions(grayscale = true), saveTo = "gray.pdf"))
An diesem Endpunkt wird ausschließlich pdfOptions.grayscale berücksichtigt. Seitengröße, Ausrichtung, Ränder, Skalierung, Kopf- und Fußzeile werden hier ignoriert. Wenn du die volle Seitengeometrie brauchst, gehe stattdessen über convertDocument oder convertUrlToPdf.

convertWebsiteToPdf und convertWebsiteToScreenshot#

Ermittle jede Seite einer Website, konvertiere jede einzelne im Hintergrund und sammle alles in einem ZIP. Beide arbeiten asynchron: Sie liefern sofort ein BatchSubmission zurück, und du fragst mit getBatchStatus ab oder blockierst mit 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 funktioniert identisch und erzeugt ein ZIP mit PNGs. WebsiteConversionOptions trägt render, crawlMode (AUTO, SITEMAP, FULL), includePatterns, excludePatterns, notificationEmail und callbackUrl. waitForBatch fragt alle 5 Sekunden ab und gibt nach 30 Minuten auf, beides überschreibbar über WaitForBatchOptions(intervalMs, timeoutMs, saveTo); bei Timeout wirft es ApiException mit Status 504.

getJobStatus#

Frage einen einzelnen asynchronen oder wiederhergestellten Konvertierungsjob ab.

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")
}
Du musst das selten selbst aufrufen. Das SDK fragt den Status bereits ab, wenn eine synchrone Anfrage mit 5xx antwortet. Siehe Timeout-Recovery.

Web-Intelligence (V2)#

Jeder V2-Lesevorgang trägt renderQuality, einen Wert von 0.0 bis 1.0, der beschreibt, wie sauber die Seite tatsächlich gerendert hat. Eine Challenge-Seite, eine Cookie-Wall, ein Login-Gate oder eine leere SPA-Hülle kommt mit niedrigem Wert zurück, dazu eine gefüllte deductions-Map, die benennt, welche Prüfungen angeschlagen haben, und warnings, während der Inhalt selbst trotzdem geliefert wird. Ein schlechter Lesevorgang wird markiert, statt unbemerkt in den Kontext deines Agenten zu rutschen. statusCode meldet den HTTP-Status der finalen Hauptdokument-Antwort, und contentHash verrät dir, dass sich seit dem letzten Lesevorgang nichts geändert hat. Beginne mit der V2-Übersicht für die Konzepte hinter den sechs Fähigkeiten.

Fähigkeit Methoden auf 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#

Rendere eine URL in genau die Artefakte, die du anforderst. Synchron: Der Aufruf liefert eine abgeschlossene Operation zurück, deren Artefakt-URLs 15 Minuten lang signiert sind. Vollständige Referenz unter 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)
Option Typ Standard Beschreibung
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?>? -- JSON-Schema für die strukturierte Extraktion.
waitFor / waitTimeoutMs String? / Int? -- / 30000 Ein CSS-Selektor (optional mit Präfix css:) oder js:<expr>, auf den gewartet wird, und dessen Budget, 0 bis 60000.
jsCode String? -- JavaScript, das nach der Navigation ausgeführt wird, max. 20000 Zeichen.
viewport PerceiveViewport? 1920 x 1080 width 320-3840, height 240-2160.
headers / cookies / auth -- -- Zusätzliche Header, eingeschleuste Cookies, HTTP-Basic-Zugangsdaten.
cacheMode PerceiveCacheMode? ENABLED ENABLED (1 Stunde Cache), BYPASS, REFRESH.
pdfOptions PdfOptions? -- Nur relevant, wenn outputs PDF enthält.
blockResources List<PerceiveResourceType>? -- Ressourcentypen, die der Browser nicht laden soll.
respectRobots / mobile Boolean? -- robots.txt beachten; mit einem Mobilprofil rendern.
onlyMainContent Boolean? true Entfernt Navigation, Header, Footer und Cookie-Banner aus dem Markdown-Artefakt und dem main_content-Extrakt.
directDownload Boolean? -- Liefert Artefakt-Bytes statt eines JSON-Envelopes. Bevorzuge perceiveDirect.

proxyUrl, geolocation und actionChain existieren zwar auf PerceiveOptions, sind serverseitig aber noch nicht verfügbar und werden derzeit mit 422 abgelehnt.

Bündle bis zu 1000 URLs hinter einem gemeinsamen Optionsblock. Kleine Batches laufen inline durch; größere kommen als QUEUED zurück, frage dann die Job-ID ab. getPerceiveOperation signiert die Artefakt-URLs jeder früheren Operation neu.

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) // frisch signierte URLs

Wenn du nur die Bytes willst und sonst nichts, streamt perceiveDirect das Artefakt in derselben Anfrage zurück und spart den Umweg über die signierte URL. Es braucht genau eine artefakterzeugende Ausgabe, also alles außer STRUCTURED, und wirft lokal EnconvertException, wenn du keine oder mehr als eine anforderst.

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

// Ein gespeichertes Artefakt einer früheren Operation erneut herunterladen.
val markdown = client.v2.downloadPerceiveArtifact(op.operationId, PerceiveOutputName.MARKDOWN)
println(markdown.content.toString(Charsets.UTF_8))

downloadPerceiveArtifact akzeptiert ein output von null, wenn die Operation genau ein Artefakt erzeugt hat, und liefert 410, sobald das gespeicherte Artefakt seine Aufbewahrungsfrist überschritten hat.

Discover#

Zähle die URLs einer Website auf, ganz ohne Browser-Rendering. Vollständige Referenz unter 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}")
Option Typ Standard Beschreibung
mode DiscoverMode? HYBRID SITEMAP, CRAWL oder HYBRID (Sitemap plus HTTP-Crawl).
maxUrls / maxDepth Int? 100 / 2 1-1000 und 1-5.
includePatterns / excludePatterns List<String>? -- Regex-Positivliste und -Sperrliste, jeweils max. 50 Einträge. Die Sperrliste wird als Zweites angewendet.
sameDomainOnly Boolean? true Auf der Startdomain bleiben.
respectRobots Boolean? -- robots.txt beachten.

DiscoverResult.sources meldet die rohen Zähler je Quelle vor der Deduplizierung, zum Beispiel {sitemap=42, crawl=30}.

Lookup#

Führe eine kategorisierte Websuche aus und lass im selben Aufruf optional die besten Treffer per Perceive lesen. Vollständige Referenz unter 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}") }
}
Option Typ Standard Beschreibung
category LookupCategory? WEB WEB, NEWS, IMAGES, SCHOLAR, PATENTS, MAPS.
country / locale String? -- Google-Ländercode gl und Oberflächensprache hl.
timeFilter LookupTimeFilter? -- HOUR, DAY, WEEK, MONTH, YEAR.
numResults / page Int? 10 / 1 1-100 und 1-10.
location String? -- Ort als freier Text, zum Beispiel "Austin, Texas".
autocorrect Boolean? true Lässt den Anbieter Tippfehler korrigieren.
perceiveTop Int? 0 Liest die obersten N Ergebnis-URLs automatisch per Perceive, 0-10. Jede davon startet ein vollständiges Browser-Rendering.

Das Ergebnis trägt außerdem answerBox, knowledgeGraph, perceiveOperationIds und total.

Distill#

Richte ein Schema auf einige Seiten und bekomme strukturierte Daten zurück. Ein optionaler CSS-Durchgang beantwortet alles, was er kann, bevor irgendetwas an die LLM-Stufe eskaliert. Vollständige Referenz unter 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)
}

Oder ermittle zuerst die URLs und destilliere anschließend jede einzelne:

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"),
    ),
)

Gib genau eines von beiden an: urls oder discoverFrom. Beides oder keines von beidem wirft EnconvertException, bevor die Anfrage deinen Prozess verlässt.

Option Typ Standard Beschreibung
urls List<String>? -- Explizite URLs zum Destillieren, max. 50.
discoverFrom DistillDiscoverFrom? -- Ermittelt zuerst die URLs einer Website. maxPages liegt zwischen 1 und 50, Standard 10.
schema Map<String, Any?> erforderlich Ein JSON-Schema-Objekt oder eine flache {field to description}-Map.
cssSchema CssSchema? -- CSS-Durchgang, der vor jeder LLM-Eskalation läuft.
waitFor / waitTimeoutMs String? / Int? -- / 30000 Selektor oder js:-Ausdruck, auf den gewartet wird, und dessen Budget.
headers / cookies / respectRobots -- -- Dieselben Render-Steuerungen wie bei Perceive.

CssField.type ist eines von TEXT, ATTRIBUTE, HTML, REGEX, NESTED, LIST, NESTED_LIST. ATTRIBUTE verlangt attribute, REGEX verlangt pattern, und die drei verschachtelten Varianten verlangen eine nicht leere fields-Liste, bis zu fünf Ebenen tief.

Ingest#

Verwandle eine ganze Website oder einen Stapel hochgeladener Dokumente über eine einzige Pipeline in gechunktes, RAG-fertiges JSONL. Ingest arbeitet immer asynchron. Vollständige Referenz unter 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 steht standardmäßig auf URLS, was eine nicht leere urls-Liste verlangt und url ablehnt. SITEMAP und CRAWL verlangen eine Start-url und lehnen urls ab. Das SDK erzwingt beide Regeln lokal und wirft EnconvertException, statt eine Anfrage zu senden, die nicht gelingen kann.

Hochgeladene Dateien laufen über ingestFiles, das denselben Job-Lebenszyklus unter dem Modus FILES nutzt und PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere Office- und ODF-Dokumente akzeptiert.

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)
Option Typ Standard Beschreibung
mode IngestMode? URLS URLS, SITEMAP, CRAWL, FILES.
url / urls String? / List<String>? -- Start-URL für SITEMAP und CRAWL; explizite URLs (max. 1000) für URLS.
maxPages / maxDepth Int? 50 / 2 Obergrenzen für die Discovery, 1-1000 und 1-5.
sameDomainOnly Boolean? true Auf der Startdomain bleiben.
includePatterns / excludePatterns / respectRobots -- -- Dieselben Discovery-Steuerungen wie bei discover.
waitFor / waitTimeoutMs String? / Int? -- / 30000 Render-Wartezeit pro Seite.
chunk IngestChunkOptions? -- maxWords 32-4000 (Standard 512), sentenceOverlap 0-10 (Standard 1).
webhookUrl String? -- Abschluss-Webhook, HMAC-signiert.

Job-Verwaltung und Webhook-Infrastruktur:

client.v2.listIngestJobs(V2ListOptions(limit = 20))   // neueste zuerst
client.v2.cancelIngestJob(job.jobId)                  // idempotent

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

client.v2.rotateWebhookSecret()         // alte Signaturen gelten sofort nicht mehr
client.v2.retryIngestWebhook(job.jobId) // Webhook eines fertigen Jobs erneut zustellen

Watch#

Rendere eine URL in festem Takt erneut und lass dich benachrichtigen, wenn sie sich ändert. Vollständige Referenz unter 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 = "")) // löscht den Webhook
client.v2.deleteWatcher(watcher.watcherId)                                 // Soft Delete, idempotent
Option Typ Standard Beschreibung
frequencyMinutes Int? 60 Minuten zwischen zwei Prüfungen, 60-43200. Die stündliche Untergrenze ist hart.
diffMode WatchDiffMode? AUTO AUTO, TEXT, STRUCTURED, TABLES, METADATA.
trackFields Map<String, Any?>? -- Teilmenge von Feldern oder Selektoren, die die Diff-Engine beobachten soll.
webhookUrl String? -- Änderungs-Webhook, HMAC-signiert mit demselben Secret wie bei Ingest.
notifyEmail Boolean? true Schickt dem Projektinhaber bei Änderungen eine E-Mail.

updateWatcher verlangt mindestens ein Feld und wirft EnconvertException bei einem leeren WatcherUpdate. Sein status akzeptiert nur ACTIVE oder PAUSED; das Löschen läuft über deleteWatcher, das den auf tot gesetzten Watcher mit Status DELETED zurückgibt. getWatcher auf einen gelöschten Watcher liefert 404.

Snapshot-Diffs enthalten nicht vertrauenswürdige Seiteninhalte. WatcherSnapshot.changes ist Rohtext, der von der beobachteten Seite stammt. Escape ihn, bevor du ihn in HTML, ein Dashboard oder eine Chat-Nachricht renderst.

PDF-Optionen#

PdfOptions wird von convertUrlToPdf, convertDocument, convertToPdf (nur Graustufen), convertWebsiteToPdf und PerceiveOptions.pdfOptions gemeinsam genutzt.

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",
    ),
)
Feld Typ Beschreibung
pageSize String? "A4", "A3", "Letter", "Legal" und so weiter.
pageWidth / pageHeight Double? Eigene Geometrie. Gemeinsam gesetzt überschreiben sie pageSize.
orientation PdfOrientation? PORTRAIT oder LANDSCAPE. Standard ist Hochformat.
margins PdfMargins? top, bottom, left, right, alle optional als Double in mm.
scale Double? Render-Skalierung, zum Beispiel 0.9 für 90 Prozent.
grayscale Boolean? Wandelt das PDF nachträglich in Graustufen um.
header / footer PdfHeaderFooter? content (max. 2000 Zeichen) und height.

Nur die Felder, die du tatsächlich setzt, gehen über die Leitung, ein teilweise gefülltes PdfOptions überschreibt also nie einen Server-Standard, den du gar nicht angefasst hast. Die vollständige Parametermatrix steht unter Parameter und Optionen.


Fehlerbehandlung#

Jeder Fehler ist eine EnconvertException oder eine Unterklasse davon, ein einziges catch kann also deine Auffanglinie sein, während spezifische Unterklassen die Fälle behandeln, die dich interessieren.

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}")
}
Klasse Ausgelöst bei Statuscode
AuthenticationException Ungültiger, fehlender oder widerrufener API-Key 401, 403
QuotaException Wird bei HTTP 402 ausgelöst 402
RateLimitException Zu viele Anfragen 429
ApiException Jede andere 4xx- oder 5xx-Antwort der tatsächliche Code
EnconvertException Basisklasse, dazu clientseitige Validierung wie ein nicht unterstütztes Konvertierungspaar oder ein fehlerhaftes Optionsobjekt --

ApiException stellt die rohe Eigenschaft statusCode bereit, und ihre message wird als [<statusCode>] <server message> gerendert, wobei das Feld detail oder error des Servers aus dem JSON-Body herausgezogen wird. Die Reihenfolge der Catch-Blöcke zählt: Die drei engen Klassen erweitern alle ApiException, die wiederum EnconvertException erweitert, führe sie also zuerst auf. Die Zuordnung der Meldungen ist unter Fehlercodes dokumentiert.


Timeout-Recovery#

Lange URL-zu-PDF-Renderings und große Dokumentkonvertierungen können ein Reverse-Proxy-Timeout von 60 bis 120 Sekunden überdauern, selbst wenn die Konvertierung auf dem Server gelingt. Das SDK fängt das bei den V1-Konvertierungsmethoden ab:

  1. Vor jeder Anfrage erzeugt es eine 32 Zeichen lange Hex-Job-ID und sendet sie als job_id im JSON-Body oder als Multipart-Feld.
  2. Antwortet die Anfrage mit 5xx, vertraut das SDK der Antwort nicht mehr und fragt GET /v1/convert/status/{job_id} alle 3 Sekunden ab. Ein 404, während die Job-Zeile noch geschrieben wird, heißt "weiter warten".
  3. Bei success bildet das SDK die Nutzdaten auf ein normales ConversionResult ab. Bei failed wirft es ApiException mit der Fehlermeldung des Servers.
  4. Die Frist beträgt 5 Minuten, danach wirft es ApiException(504, "Conversion timed out").

Erfolgreiche Antworten ohne job_id (der synchrone URL-Pfad macht das) bekommen die clientseitig erzeugte ID nachgetragen, result.jobId ist also immer etwas, das du an getJobStatus übergeben kannst. Zwei bewusste Ausnahmen: convertWebsiteToPdf und convertWebsiteToScreenshot überspringen den Fallback, denn eine Website-Einreichung hat keine Job-Zeile, und ein 5xx bedeutet dort, dass die Einreichung selbst fehlgeschlagen ist. V2-Methoden überspringen ihn ebenfalls, da jeder V2-Endpunkt sein eigenes Polling- oder Webhook-Konzept hat.


Konfiguration#

val client = Enconvert(
    apiKey = System.getenv("ENCONVERT_API_KEY"),
    timeout = 300_000,                      // ms, 5 Minuten
    baseUrl = "https://api.enconvert.com",  // für ein selbst gehostetes Gateway überschreiben
)
Parameter Typ Standard Beschreibung
apiKey String erforderlich Privater API-Key. Ein leerer Wert wirft IllegalArgumentException aus dem Konstruktor.
timeout Long 300_000 Timeout pro Anfrage in Millisekunden, angewendet auf den zugrunde liegenden HttpRequest.
baseUrl String https://api.enconvert.com Basis-URL der API. Abschließende Schrägstriche werden entfernt.
Schreibe den API-Key niemals fest in den Code. Lies ihn aus einer Umgebungsvariable, einer Gradle-Property oder deinem Secret-Manager und halte ihn aus jedem Artefakt heraus, das du auf das Gerät eines Nutzers ausliefert. Wer deinen privaten Key besitzt, kann Anfragen gegen dein Projekt ausführen.

Ergebnisform#

Jede Konvertierungsmethode liefert ein 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,
)

Die vorsignierte URL ist ein temporärer signierter Link. Übergib saveTo, wenn du die Bytes sofort auf der Platte haben willst, oder lade die URL selbst herunter und speichere die Datei für dauerhaften Zugriff in deinem eigenen Bucket.

V2-Lesevorgänge liefern stattdessen ein PerceiveResult, und darin stecken die Ehrlichkeitssignale:

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

op.operationId    // "per_..."
op.status         // PerceiveStatus.COMPLETED
op.url            // angefragte URL; op.urlFinal nach Weiterleitungen
op.renderQuality  // Double?, 0.0 bis 1.0
op.statusCode     // Int?, HTTP-Status des Hauptdokuments
op.deductions     // Map<String, Double>, z. B. {http_error=0.7}. Leer bei sauberem Rendering.
op.warnings       // List<String>
op.cacheHit       // Boolean; op.contentHash ist der SHA-256 des gerenderten Inhalts
op.outputs        // Map<String, V2OutputArtifact>, geschlüsselt nach Ausgabename
op.structured     // Map<String, Any?>?, vorhanden, wenn extract oder schema genutzt wurde
op.extractionTier // HEURISTIC, CSS oder LLM
op.tokens         // V2Tokens(input, output); daneben op.costCents und op.durationMs

Jedes V2OutputArtifact trägt url, objectKey, sizeBytes, contentType und expiresIn (900 Sekunden). Artefakt-URLs werden bei jedem getPerceiveOperation-Aufruf neu signiert, speichere also die operationId, nicht die URL. Untypisierte Nutzdaten (Extraktionsschemata, extrahierte Daten, verfolgte Felder, Diff-Einträge, Such-Extras) überqueren die Grenze als Map<String, Any?> und werden in beide Richtungen verlustfrei umgewandelt, nichts, was du in ein Schema steckst, wird also auf dem Rückweg umgeformt.


Quelle und Issues#


Häufig gestellte Fragen#

Wie konvertiere ich Dateien in Kotlin?#

Füge com.enconvert:enconvert-kotlin:0.0.1 deinem Gradle- oder Maven-Build hinzu, konstruiere Enconvert(apiKey = System.getenv("ENCONVERT_API_KEY")) und rufe eine typisierte Methode wie convertDocument, convertImage oder convertUrlToPdf auf. Übergib saveTo im Optionsobjekt, damit die Ausgabe direkt in eine lokale Datei gestreamt wird, statt dass du die vorsignierte URL selbst herunterlädst.

Wie konvertiere ich DOCX in Kotlin nach PDF?#

Rufe client.convertDocument("report.docx", ConvertDocumentOptions(saveTo = "report.pdf")) auf. Das Ausgabeformat ist standardmäßig pdf, du kannst outputFormat also weglassen. Das Eingabeformat ergibt sich aus der Dateiendung, und .doc und .docx führen beide zur selben Konvertierung. Für die Seitengeometrie übergibst du ein PdfOptions über ConvertDocumentOptions.pdfOptions.

Wie konvertiere ich eine URL in Kotlin in ein PDF?#

Rufe client.convertUrlToPdf(url, UrlToPdfOptions(saveTo = "page.pdf")) auf. Setze singlePage = false, um mit pdfOptions.pageSize zu paginieren, und nutze UrlRenderOptions, um den Viewport zu ändern, das Laden von Medien abzuschalten oder den Scroll-Durchgang zu deaktivieren, der Lazy Loader auslöst.

Wie konvertiere ich HEIC auf der JVM nach WebP?#

Rufe client.convertImage("photo.heic", ConvertImageOptions(outputFormat = "webp", saveTo = "photo.webp")) auf. Alle 20 Paare unter jpeg, png, svg, heic und webp sind implementiert, dazu die Rasterung von pdf nach jpeg. Ein nicht unterstütztes Paar wirft EnconvertException, bevor irgendein Netzwerkaufruf stattfindet, und validOutputsFor("heic") listet die gültigen Ziele vorab auf.

Wie hole ich eine Webseite aus Kotlin als Markdown?#

Zwei Möglichkeiten. client.convertUrlToMarkdown(url, UrlToMarkdownOptions(saveTo = "page.md")) liefert dir eine Markdown-Datei mit YAML-Frontmatter. client.v2.perceive(url, PerceiveOptions(outputs = listOf(PerceiveOutputName.MARKDOWN))) liefert denselben Inhalt plus renderQuality, deductions, warnings und statusCode, und genau das willst du, wenn ein Agent das Ergebnis unbeaufsichtigt liest.

Was bedeutet renderQuality, und wann sollte ich eine Seite verwerfen?#

renderQuality reicht von 0.0 bis 1.0 und beschreibt, wie sauber die Seite gerendert hat, nicht wie gut der Inhalt ist. Challenge-Seiten, Login-Walls, HTTP-Fehler und leere SPA-Hüllen drücken den Wert, und deductions benennt jede Prüfung, die angeschlagen hat, zum Beispiel {http_error=0.7}. Der Inhalt wird immer zurückgegeben, damit du ihn prüfen kannst. Ein verbreitetes Muster ist, alles unter 0.5 als verdächtig zu behandeln und entweder mit cacheMode = PerceiveCacheMode.REFRESH neu anzufragen oder es an einen Menschen weiterzureichen.

Wie verwandle ich eine Doku-Website aus Kotlin in RAG-Chunks?#

Rufe client.v2.ingest(IngestOptions(mode = IngestMode.SITEMAP, url = "https://docs.example.com", chunk = IngestChunkOptions(maxWords = 512, sentenceOverlap = 1))) auf. Ingest arbeitet immer asynchron: Frage getIngestJob(jobId) ab, bis der Status COMPLETED lautet, und lies outputUrl für das JSONL, oder setze webhookUrl und lass dich vom Abschluss-Webhook finden. Lokale Dokumente laufen mit denselben Chunk-Einstellungen über ingestFiles.

Blockiert das SDK den aufrufenden Thread?#

Ja. Jede Methode ruft HttpClient.send synchron auf, und im SDK gibt es keine suspend-Funktionen oder Coroutine-Builder. waitForBatch und der interne Poller für die Timeout-Recovery legen den aktuellen Thread zwischen den Versuchen schlafen. Aus einer Coroutine heraus verpackst du Aufrufe in withContext(Dispatchers.IO); in einem Server-Framework hältst du sie vom Thread-Pool für die Anfragebearbeitung fern.

Kann ich dieses SDK aus Java aufrufen?#

Du kannst, denn es sind ganz gewöhnliche JVM-Klassen. Allerdings werden Kotlin-Standardargumente Java nicht als Überladungen bereitgestellt, ein Java-Aufrufer muss also jedes Konstruktorargument einer Options-Datenklasse übergeben. Ist deine Codebasis Java, nutze stattdessen das separate Java SDK, das auf der SDK-Seite aufgeführt ist.

Wo bekomme ich einen API-Key?#

Erstelle einen privaten Key in deinem Dashboard. Er wird bei jeder Anfrage als Header X-API-Key gesendet, halte ihn also serverseitig. Key-Typen und Geltungsbereiche behandelt die Authentifizierung, und die Preise decken die kommerzielle Seite ab.