---
seo_title: Kotlin SDK für Dateikonvertierung: JVM-Client | EnConvert
meta_desc: Offizielles EnConvert Kotlin SDK für JDK 17+. Idiomatische Kotlin-Datenklassen für Dateikonvertierung und zum Perceive, Discover, Distill, Ingest und Watch von Seiten.
keywords: kotlin sdk dateikonvertierung, dateien konvertieren kotlin, url zu pdf kotlin, kotlin web scraping api, docx in pdf konvertieren kotlin, enconvert kotlin sdk, jvm api client dateikonvertierung, heic in webp konvertieren kotlin, webseite in markdown umwandeln kotlin, rag pipeline kotlin, website änderungen überwachen kotlin, maven central bibliothek dateikonvertierung
---

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

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

---

## Installation

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

```groovy
// Gradle, Groovy DSL
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>
```

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

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

    // 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](/de/docs/authentication) 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](/de/docs/endpoints-overview) 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.

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

| 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](#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).

<div class="alert alert-warning">
<strong>Kombiniere <code>auth</code> nicht mit einem <code>Authorization</code>-Header.</strong> Die API weist den Konflikt zurück, statt zu raten, was du gemeint hast.
</div>

### convertUrlToScreenshot

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

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

```kotlin
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`](#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`.

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

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:

```kotlin
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"`.

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

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

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

```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>An diesem Endpunkt wird ausschließlich <code>pdfOptions.grayscale</code> berücksichtigt.</strong> Seitengröße, Ausrichtung, Ränder, Skalierung, Kopf- und Fußzeile werden hier ignoriert. Wenn du die volle Seitengeometrie brauchst, gehe stattdessen über <code>convertDocument</code> oder <code>convertUrlToPdf</code>.
</div>

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

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

```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>Du musst das selten selbst aufrufen.</strong> Das SDK fragt den Status bereits ab, wenn eine synchrone Anfrage mit 5xx antwortet. Siehe <a href="#timeout-recovery">Timeout-Recovery</a>.
</div>

---

## 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](/de/docs/v2-overview) 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](/de/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)
```

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

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

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

// 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](/de/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}")
```

| 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](/de/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}") }
}
```

| 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](/de/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)
}
```

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

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

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](/de/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` 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.

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

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

```kotlin
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](/de/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 = "")) // 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`.

<div class="alert alert-warning">
<strong>Snapshot-Diffs enthalten nicht vertrauenswürdige Seiteninhalte.</strong> <code>WatcherSnapshot.changes</code> ist Rohtext, der von der beobachteten Seite stammt. Escape ihn, bevor du ihn in HTML, ein Dashboard oder eine Chat-Nachricht renderst.
</div>

---

## PDF-Optionen

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

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

| 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](/de/docs/parameters-options).

---

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

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

| 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](/de/docs/error-codes) 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

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

<div class="alert alert-warning">
<strong>Schreibe den API-Key niemals fest in den Code.</strong> 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.
</div>

---

## Ergebnisform

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

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:

```kotlin
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

- **Maven Central:** `com.enconvert:enconvert-kotlin:0.0.1`
- **GitHub:** [conversionapi/kotlin-sdk](https://github.com/conversionapi/kotlin-sdk)
- **Lizenz:** MIT
- **Weitere Sprachen:** siehe die vollständige [SDK-Liste](/de/docs/sdks)

---

## 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](/de/docs/sdks) aufgeführt ist.

### Wo bekomme ich einen API-Key?

Erstelle einen privaten Key in deinem [Dashboard](/de/dashboard). Er wird bei jeder Anfrage als Header `X-API-Key` gesendet, halte ihn also serverseitig. Key-Typen und Geltungsbereiche behandelt die [Authentifizierung](/de/docs/authentication), und die [Preise](/de/pricing) decken die kommerzielle Seite ab.
