---
seo_title: SDK Swift de Conversión de Archivos: async await | EnConvert
meta_desc: SDK oficial de EnConvert para Swift en macOS, iOS, tvOS y watchOS. Métodos async await para convertir archivos y para perceive, discover y distill.
keywords: sdk de conversión de archivos para swift, convertir archivos en swift, url a pdf en swift, api de web scraping con swift, docx a pdf en swift, enconvert swift sdk, heic a webp en swift, cliente api async await en swift, api de conversión de archivos para ios, librería pdf con swift package manager, captura de pantalla de una web en swift, extracción de datos estructurados en swift
---

# SDK de Conversión de Archivos para Swift

`Enconvert` es el cliente oficial de EnConvert para Swift, distribuido a través de Swift Package Manager. Está construido sobre `URLSession` con `async`/`await`, no lleva ninguna dependencia externa y apunta a Swift 5.9 y posteriores en macOS 12, iOS 15, tvOS 15 y watchOS 8. Doce métodos del cliente cubren la conversión de archivos y el renderizado de URL (DOCX a PDF, HEIC a WebP, URL a PDF, URL a Markdown, lotes de sitios completos), y el espacio de nombres `client.v2` añade veintitrés métodos de inteligencia web para percibir, descubrir, buscar, destilar, ingerir y vigilar páginas.

<div class="alert alert-info">
<strong>Paquete:</strong> <code>Enconvert</code> · <strong>Fuente:</strong> <a href="https://github.com/conversionapi/swift-sdk">conversionapi/swift-sdk</a> · <strong>Swift:</strong> 5.9+ · <strong>Plataformas:</strong> macOS 12+, iOS 15+, tvOS 15+, watchOS 8+ · <strong>Dependencias:</strong> ninguna
</div>

---

## Instalación

Añade el paquete y luego lista el producto en el target que lo usa:

```swift
dependencies: [
    .package(url: "https://github.com/conversionapi/swift-sdk.git", from: "0.0.1")
],
targets: [
    .target(name: "MyApp", dependencies: [
        .product(name: "Enconvert", package: "swift-sdk")
    ])
]
```

En Xcode, usa **File > Add Package Dependencies** y pega `https://github.com/conversionapi/swift-sdk.git`. En Linux el SDK importa `FoundationNetworking` de forma condicional, así que no necesitas nada más por tu parte.

---

## Inicio rápido

```swift
import Enconvert

let apiKey = ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? ""
let client = try Enconvert(apiKey: apiKey)

let result = try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf"))
print(result.filename, result.presignedUrl)
```

`Enconvert.init` lanza errores, no es fallible: un `apiKey` vacío provoca `EnconvertError.invalidArgument` antes de que nada toque la red. Todos los métodos de solicitud son `async throws`, y los métodos de conversión están marcados con `@discardableResult`, así que una llamada hecha solo por su efecto secundario `saveTo` no genera avisos.

---

## Qué expone el cliente

Doce métodos cuelgan de `Enconvert` y se corresponden 1:1 con endpoints REST:

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

`client.v2` es un espacio de nombres `EnconvertV2` que contiene veintitrés métodos más repartidos entre seis grupos de capacidades:

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

Las opciones se pasan como un struct con parámetros de inicializador que tienen valores por defecto, así que `UrlToPdfOptions()` significa "todo por defecto" y solo nombras los campos que te importan. Swift exige argumentos etiquetados en el orden de declaración, así que mantén `saveTo:` por delante de `singlePage:` y `pdfOptions:` cuando establezcas varios a la vez.

---

## Conversión de archivos

Las subidas aceptan un `FileInput`:

| Caso | Para qué sirve |
|------|----------------|
| `.path("report.docx")` | Un archivo en disco. El nombre base decide el formato de entrada y el tipo MIME. |
| `.data(bytes)` | Bytes crudos sin nombre. Se suben como `upload.bin`, `application/octet-stream`. |
| `.wrapped(data: bytes, filename: "report.docx", contentType: nil)` | Bytes crudos más un nombre de archivo explícito. Un `contentType` nulo se infiere de la extensión. |

### convertUrlToPdf

Renderiza cualquier URL pública a PDF.

```swift
let result = try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(
    viewportWidth: 1440,
    saveTo: "report.pdf",
    singlePage: false,
    pdfOptions: PdfOptions(pageSize: "A4", orientation: .landscape)
))
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `viewportWidth`, `viewportHeight` | `Int?` | `1920`, `1080` | Tamaño del viewport del navegador en píxeles. |
| `loadMedia`, `enableScroll` | `Bool?` | `true` | Espera a imágenes y vídeo; desplaza de arriba abajo para disparar los cargadores diferidos. |
| `outputFilename` | `String?` | automático | Sobrescribe el nombre de archivo generado. |
| `auth`, `cookies`, `headers` | `HttpBasicAuth?`, `[BrowserCookie]?`, `[String: String]?` | ninguno | Credenciales HTTP Basic, cookies inyectadas (máximo 50), encabezados de solicitud adicionales (máximo 20, con los de salto a salto rechazados). |
| `saveTo` | `String?` | ninguno | Ruta local donde escribir el PDF. Los directorios padre se crean. |
| `singlePage` | `Bool?` | `true` | `true` produce una única página continua. `false` pagina usando `pdfOptions.pageSize`. |
| `pdfOptions` | `PdfOptions?` | ninguno | Geometría de página. Consulta [Opciones de PDF](#opciones-de-pdf). |

Las páginas tras un inicio de sesión reciben credenciales, cookies o encabezados:

```swift
_ = try await client.convertUrlToPdf("https://internal.example.com/report", options: UrlToPdfOptions(
    auth: HttpBasicAuth(username: "user", password: "pass"),
    cookies: [BrowserCookie(name: "session", value: "abc123", domain: "internal.example.com")],
    headers: ["X-Tenant": "acme"],
    saveTo: "report.pdf"
))
```

No combines `auth` con una entrada `Authorization` en `headers`. La API rechaza el conflicto.

### convertUrlToScreenshot

Captura un PNG de cualquier URL.

```swift
let shot = try await client.convertUrlToScreenshot(
    "https://example.com",
    options: UrlToScreenshotOptions(viewportWidth: 1440, saveTo: "shot.png")
)
```

`UrlToScreenshotOptions` acepta los mismos campos de viewport, medios, desplazamiento, nombre de archivo y acceso del navegador que `UrlToPdfOptions`, sin `singlePage` ni `pdfOptions`.

### 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. Útil para pipelines de RAG, para importar contenido de terceros a un CMS o para generar datos de entrenamiento.

```swift
_ = try await client.convertUrlToMarkdown("https://example.com/article", options: UrlToMarkdownOptions(saveTo: "article.md"))
```

### convertImage

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp`, o rasteriza un PDF a JPEG.

```swift
let result = try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp"))
```

El formato de entrada procede de la extensión del nombre de archivo (`.jpg`, `.jpeg`, `.png`, `.svg`, `.heic`, `.webp` y `.pdf` para rasterizar). `outputFormat` es obligatorio y acepta los alias `jpg`, `yml`, `htm` y `md`.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|-------------|-------------|
| `outputFormat` | `String` | Sí | Formato de destino, por ejemplo `"webp"`. |
| `saveTo` | `String?` | no | Ruta local donde escribir el resultado. |
| `outputFilename` | `String?` | no | Sobrescribe el nombre de archivo generado. |

### convertDocument

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

```swift
// docx a pdf
_ = try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf"))

// json a yaml
_ = try await client.convertDocument(.path("data.json"), options: ConvertDocumentOptions(outputFormat: "yaml", saveTo: "data.yaml"))

// markdown a pdf con configuración de página personalizada
_ = try await client.convertDocument(.path("README.md"), options: ConvertDocumentOptions(
    saveTo: "readme.pdf",
    pdfOptions: PdfOptions(pageSize: "A4", margins: PdfMargins(top: 20, bottom: 20))
))
```

**Entradas admitidas:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`. EPUB no tiene un par de documento dedicado, así que envía los archivos `.epub` a través de `convertToPdf` o `convertToMarkdown`.

El SDK incluye la tabla de conversión completa del gateway y valida localmente cada par `{input}-to-{output}`, así que un par no admitido lanza `EnconvertError.invalidArgument` con la lista de salidas válidas en lugar de pagar un viaje de ida y vuelta condenado a fallar. Hay 43 pares implementados:

| Entrada | Salidas |
|---------|---------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `yaml` | `json` |
| `csv` | `json`, `xml` |
| `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | cada uno a los otros cuatro (20 pares ordenados) |
| `pdf` | `jpeg` |

Puedes consultar esa tabla tú mismo sin realizar ninguna solicitud:

```swift
validOutputsFor("json")                           // ["csv", "toml", "xml", "yaml"]
IMPLEMENTED_CONVERSIONS.contains("heic-to-webp")  // true
```

### convertToMarkdown

Convierte a Markdown limpio un documento subido de casi cualquier formato, con el formato detectado automáticamente en el servidor. Una buena primera etapa para un pipeline de RAG.

```swift
_ = try await client.convertToMarkdown(.path("handbook.docx"), options: ConvertToMarkdownOptions(saveTo: "handbook.md"))
```

Se aceptan PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, y archivos de ofimática antiguos o en ODF. Las imágenes no. Este endpoint no tiene opciones de PDF.

### convertToPdf

Convierte a PDF un archivo subido de casi cualquier formato: ofimática, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB o un PDF existente que pasa de largo y se normaliza.

```swift
_ = try await client.convertToPdf(.path("slides.pptx"), options: ConvertToPdfOptions(saveTo: "slides.pdf"))

// PDF que pasa de largo, convertido a escala de grises
_ = try await client.convertToPdf(
    .path("scan.pdf"),
    options: ConvertToPdfOptions(saveTo: "scan-gray.pdf", pdfOptions: PdfOptions(grayscale: true))
)
```

<div class="alert alert-warning">
<strong>Aquí solo se respeta <code>grayscale</code>.</strong> <code>convertToPdf</code> reenvía <code>pdfOptions</code>, pero el endpoint anything-to-pdf lee <code>grayscale</code> e ignora el resto. Usa <code>convertDocument</code> o <code>convertUrlToPdf</code> cuando necesites tamaño de página, orientación, márgenes, escala, encabezados o pies.
</div>

### convertWebsiteToPdf y convertWebsiteToScreenshot

Descubre todas las páginas de un sitio, convierte cada una en segundo plano y recoge un único ZIP. Ambos métodos son solo asíncronos y requieren una clave de API privada con acceso de rastreo.

```swift
let batch = try await client.convertWebsiteToPdf("https://example.com", options: WebsiteToPdfOptions(
    crawlMode: .sitemap,
    excludePatterns: ["/blog/tag/"]
))
print(batch.batchId, batch.urlCount, batch.discoveryMethod ?? "")

// Bloquea hasta que el lote se resuelva y guarda el ZIP
let status = try await client.waitForBatch(batch.batchId, options: WaitForBatchOptions(saveTo: "site.zip"))
print("\(status.completed) of \(status.total) pages converted")

// O sondéalo tú mismo
let snapshot = try await client.getBatchStatus(batch.batchId)
if snapshot.status != .processing {
    print(snapshot.zipDownloadUrl ?? "")
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `crawlMode` | `CrawlMode?` | `.auto` | `.auto`, `.sitemap` (solo sitemap.xml) o `.full` (sitemap más rastreo BFS). |
| `includePatterns`, `excludePatterns` | `[String]?` | ninguno | Lista de permitidos y después lista de bloqueados para las URL descubiertas. Solo en modo de rastreo completo. |
| `notificationEmail` | `String?` | propietario del proyecto | Correo al que se avisa cuando el lote termina. |
| `callbackUrl` | `String?` | ninguno | Webhook al que se hace POST cuando el lote termina. |
| `singlePage`, `pdfOptions` | `Bool?`, `PdfOptions?` | ver arriba | Solo en lotes de PDF. |

Ambos métodos aceptan también los campos de viewport, medios, desplazamiento y acceso del navegador listados en `convertUrlToPdf`, aplicados a cada página. `waitForBatch` sondea cada 5 segundos con un plazo de 30 minutos por defecto; puedes cambiarlo con `WaitForBatchOptions(intervalMs:timeoutMs:saveTo:)`. Superar el plazo lanza `EnconvertError.api(statusCode: 504, ...)`. `convertWebsiteToScreenshot` se comporta de forma idéntica y produce un ZIP de PNG.

---

## Inteligencia web (V2)

Cada lectura V2 lleva una puntuación `renderQuality` de 0.0 a 1.0, expuesta como un `Double?` en `PerceiveResult`, `PerceiveDirectResult`, `DistillItem` y `WatcherSnapshot`. Una puntuación baja significa que la página no se renderizó de forma honesta: un desafío antibots, una barrera de inicio de sesión, un banner de cookies sobre el armazón vacío de una SPA, un estado de error HTTP. El contenido se sigue devolviendo, marcado, junto a un diccionario `deductions` que nombra cada penalización activada y un array `warnings`, de modo que una mala lectura nunca entra en silencio en el contexto de tu agente. Ponle una condición antes de fiarte de nada:

```swift
if let quality = op.renderQuality, quality < 0.6 {
    print("low quality read of \(op.url): \(op.deductions)")
}
```

### Perceive

Renderiza una URL en los artefactos que pidas. Es síncrono, con URL de artefacto firmadas válidas durante 15 minutos. Todos los métodos V2 necesitan una clave de API privada; las claves públicas se rechazan.

```swift
let op = try await client.v2.perceive("https://example.com", options: PerceiveOptions(
    outputs: [.markdown, .screenshot, .structured],
    extract: [.tables, .metadata],
    viewport: PerceiveViewport(width: 1440)
))
print(op.operationId, op.renderQuality ?? 0, op.outputs["markdown"]?.url ?? "")
print(op.structured ?? [:], op.extractionTier ?? .heuristic)

// Vuelve a firmar más tarde las URL de artefacto
let again = try await client.v2.getPerceiveOperation(op.operationId)
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `outputs` | `[PerceiveOutputName]?` | `[.markdown, .structured]` | `.markdown`, `.htmlCleaned`, `.htmlRaw`, `.screenshot`, `.screenshotFullPage`, `.pdf`, `.links`, `.images`, `.structured`. |
| `extract` | `[PerceiveExtractName]?` | ninguno | `.tables`, `.prices`, `.contacts`, `.metadata`, `.mainContent`, `.headings`, `.structuredData`, `.technologies`, `.all`. |
| `schema` | `JSONObject?` | ninguno | Schema JSON para la extracción estructurada a través del nivel LLM. |
| `waitFor`, `waitTimeoutMs` | `String?`, `Int?` | ninguno, `30000` | Un selector CSS (opcionalmente con el prefijo `css:`) o `js:<expr>` que esperar, y su presupuesto en ms (de 0 a 60000). |
| `jsCode` | `String?` | ninguno | JavaScript ejecutado tras la navegación, máximo 20000 caracteres. |
| `viewport` | `PerceiveViewport?` | 1920 por 1080 | `width` de 320 a 3840, `height` de 240 a 2160. |
| `headers`, `cookies`, `auth` | `[String: String]?`, `[BrowserCookie]?`, `HttpBasicAuth?` | ninguno | Encabezados de solicitud, cookies inyectadas, credenciales HTTP Basic. |
| `cacheMode` | `PerceiveCacheMode?` | `.enabled` | `.enabled` (caché de 1 hora), `.bypass`, `.refresh`. |
| `pdfOptions` | `PdfOptions?` | ninguno | Solo tiene sentido cuando `outputs` incluye `.pdf`. |
| `blockResources` | `[PerceiveResourceType]?` | ninguno | `.image`, `.media`, `.font`, `.stylesheet`, `.script`, `.xhr`, `.fetch`, `.websocket`, `.manifest`, `.other`. |
| `respectRobots`, `mobile` | `Bool?` | por defecto del servidor | Respetar `robots.txt`; emular un dispositivo móvil. |
| `onlyMainContent` | `Bool?` | `true` | Elimina la navegación, el encabezado, el pie y los banners de cookies del artefacto markdown y del extract `main_content`. Ponlo en `false` para la página completa. |
| `directDownload` | `Bool?` | `false` | Transmite bytes crudos en lugar de un sobre JSON. Es preferible `perceiveDirect`. |

<div class="alert alert-warning">
<strong>Hay tres opciones declaradas que todavía no están activas.</strong> <code>proxyUrl</code>, <code>geolocation</code> y <code>actionChain</code> existen en <code>PerceiveOptions</code> y se serializan al cable, pero el servidor responde actualmente <code>422</code> para las tres. Déjalas en <code>nil</code>.
</div>

Transmitir un único artefacto directamente a disco se salta el sobre JSON y el viaje de ida y vuelta de la URL firmada. `perceiveDirect` comprueba localmente que has pedido exactamente una salida que produzca artefacto, así que un error no cuesta nada:

```swift
let direct = try await client.v2.perceiveDirect("https://example.com", options: PerceiveOptions(outputs: [.pdf]))
try direct.content.write(to: URL(fileURLWithPath: direct.filename ?? "page.pdf"))

// Vuelve a descargar más tarde un artefacto almacenado. Pasa nil cuando la operación produjo solo uno.
let saved = try await client.v2.downloadPerceiveArtifact(direct.operationId, output: .pdf)
```

`PerceiveDirectResult` lleva `content`, `contentType`, `filename`, `operationId`, `objectKey`, `cacheHit`, `renderQuality`, `sourceStatusCode`, `contentHash` y `warningsCount`, todos leídos de los encabezados de la respuesta. Un `410` desde `downloadPerceiveArtifact` significa que el artefacto ha superado su ventana de retención.

Los lotes aceptan hasta 1000 URL con un único bloque de opciones compartido. Los lotes pequeños terminan en línea; los más grandes vuelven en cola y tú sondeas:

```swift
let batch = try await client.v2.perceiveBatch(
    ["https://a.example", "https://b.example"],
    options: PerceiveBatchOptions(outputs: [.markdown], outputMode: .zip)
)
let done = try await client.v2.getPerceiveBatch(batch.jobId)
if done.status == .completed, let zip = done.zip {
    print(zip.url ?? "")
}
```

`outputMode` es `.manifest` (por defecto) o `.zip`. `directDownload` se rechaza con `422` en los lotes.

### Discover

Enumera las URL de un sitio sin renderizado de navegador. Es rápido y nunca ejecuta un renderizado.

```swift
let opts = DiscoverOptions(mode: .hybrid, maxUrls: 200, excludePatterns: ["/tag/"])
let found = try await client.v2.discover("https://example.com", options: opts)
print(found.total, found.truncated, found.sources, found.urls)
```

| 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 URL; profundidad de rastreo de 1 a 5. |
| `includePatterns`, `excludePatterns` | `[String]?` | ninguno | Lista de permitidos por regex y después lista de bloqueados aplicada tras ella. Máximo 50 patrones cada una. |
| `sameDomainOnly` | `Bool?` | `true` | Permanece en el dominio de la URL semilla. |
| `respectRobots` | `Bool?` | por defecto del servidor | Respetar `robots.txt`. |

`DiscoverResult` informa también de `pagesCrawled`, `robotsRespected` y `warnings`, y `sources` guarda los recuentos crudos por fuente tomados antes de la deduplicación.

### Lookup

Ejecuta una búsqueda web categorizada y, opcionalmente, renderiza los primeros aciertos en la misma llamada.

```swift
let search = try await client.v2.lookup(
    "best static site generators",
    options: LookupOptions(category: .web, numResults: 10, perceiveTop: 3)
)
for hit in search.results {
    print(hit.position ?? 0, hit.title ?? "", hit.url ?? "", hit.perceive?.renderQuality ?? 0)
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `category` | `LookupCategory?` | `.web` | `.web`, `.news`, `.images`, `.scholar`, `.patents`, `.maps`. |
| `country`, `locale` | `String?` | ninguno | Código de país `gl` de Google (`"us"`, `"in"`) e idioma de interfaz `hl` (`"en"`). |
| `timeFilter` | `LookupTimeFilter?` | ninguno | `.hour`, `.day`, `.week`, `.month`, `.year`. |
| `numResults`, `page` | `Int?` | `10`, `1` | De 1 a 100 resultados; página de 1 a 10. |
| `location`, `autocorrect` | `String?`, `Bool?` | ninguno, `true` | Ubicación en texto libre como `"Austin, Texas"`; deja que el proveedor corrija la consulta. |
| `perceiveTop` | `Int?` | `0` | Renderiza automáticamente las N primeras URL de resultado, de 0 a 10. Cada una ejecuta un renderizado completo de navegador. |

`LookupResult` expone además `answerBox`, `knowledgeGraph`, `perceiveOperationIds` y `credits`.

### Distill

Extrae datos estructurados de las páginas contra un schema que tú defines.

```swift
let extraction = try await client.v2.distill(DistillOptions(
    urls: ["https://example.com/pricing"],
    schema: ["plans": .string("list of plan names with monthly prices")],
    cssSchema: CssSchema(baseSelector: ".plan-card", fields: [
        CssField(name: "name", type: .text, selector: "h3"),
        CssField(name: "price", type: .text, selector: ".price")
    ])
))

let first = extraction.results[0]
print(first.data ?? [:], first.extractionTier, first.fieldsFromCss, first.fieldsFromLlm)
```

`schema` es un `JSONObject`, que es `[String: JSONValue]`, así que tanto un mapa plano `{field: description}` como un objeto JSON-Schema completo funcionan. El `cssSchema` opcional se ejecuta primero y responde todo lo que los selectores simples alcanzan; solo los campos que se le escapan escalan al nivel LLM, y `extractionTier` informa de qué niveles respondieron realmente (`.css`, `.llm`, `.mixed` o `.none`). `CssField.type` es uno de `.text`, `.attribute`, `.html`, `.regex`, `.nested`, `.list` o `.nestedList`, anidado hasta 5 niveles de profundidad.

Cambia `urls` por `discoverFrom` para descubrir y destilar en una sola llamada. `DistillDiscoverFrom` recibe `url`, `mode` (`.hybrid` por defecto) y `maxPages` (de 1 a 50, 10 por defecto, limitando tanto el descubrimiento como la destilación):

```swift
_ = try await client.v2.distill(DistillOptions(
    discoverFrom: DistillDiscoverFrom(url: "https://example.com", mode: .sitemap, maxPages: 10),
    schema: ["title": .string("page title"), "summary": .string("one-line summary")]
))
```

Pasar a la vez `urls` y `discoverFrom`, o ninguno de los dos, lanza `EnconvertError.invalidArgument` antes de enviar ninguna solicitud.

### Ingest

Convierte un sitio, una lista de URL o una pila de documentos subidos en JSONL troceado y listo para RAG. Siempre asíncrono.

```swift
let job = try await client.v2.ingest(IngestOptions(
    mode: .sitemap,
    url: "https://docs.example.com",
    maxPages: 100,
    chunk: IngestChunkOptions(maxWords: 512, sentenceOverlap: 1),
    webhookUrl: "https://my.app/hooks/enconvert"
))

let status = try await client.v2.getIngestJob(job.jobId)
if status.status == .completed {
    print(status.totalChunks, status.outputUrl ?? "")
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `mode` | `IngestMode?` | `.urls` | `.urls`, `.sitemap` o `.crawl`. El cuarto caso, `.files`, es lo que `ingestFiles` informa en su job; no lo pases aquí. |
| `url` | `String?` | ninguno | URL semilla. Obligatoria para `.sitemap` y `.crawl`, prohibida para `.urls`. |
| `urls` | `[String]?` | ninguno | URL explícitas, máximo 1000. Obligatorias para `.urls`, prohibidas en el resto de casos. |
| `maxPages`, `maxDepth` | `Int?` | `50`, `2` | Límite de descubrimiento para `.sitemap` y `.crawl`, de 1 a 1000; profundidad de 1 a 5. |
| `sameDomainOnly` | `Bool?` | `true` | Permanece en el dominio de la URL semilla. |
| `includePatterns`, `excludePatterns` | `[String]?` | ninguno | Lista de permitidos por regex y después lista de bloqueados. |
| `respectRobots` | `Bool?` | por defecto del servidor | Respetar `robots.txt`. |
| `waitFor`, `waitTimeoutMs` | `String?`, `Int?` | `30000` ms | Selector o expresión `js:` esperada por página, y su presupuesto (de 0 a 60000). |
| `chunk` | `IngestChunkOptions?` | ninguno | `maxWords` de 32 a 4000, 512 por defecto. `sentenceOverlap` de 0 a 10, 1 por defecto. |
| `webhookUrl` | `String?` | ninguno | Webhook de finalización, firmado con HMAC. |

Las reglas de modo y URL de arriba se aplican en el cliente: `ingest` lanza `EnconvertError.invalidArgument` en lugar de hacer una solicitud condenada si pasas `urls` con `mode: .sitemap`. Los archivos subidos recorren el mismo pipeline y el mismo ciclo de vida de job:

```swift
let files: [FileInput] = [.path("handbook.pdf"), .path("notes.docx")]
let fileJob = try await client.v2.ingestFiles(files, options: IngestFilesOptions(chunk: IngestChunkOptions(maxWords: 512)))
```

Se aceptan PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, y archivos de ofimática antiguos o en ODF, y hace falta al menos un archivo. Gestión de jobs y fontanería de webhooks:

```swift
let list = try await client.v2.listIngestJobs(V2ListOptions(limit: 20))
let canceled = try await client.v2.cancelIngestJob(job.jobId)   // idempotente

let secret = try await client.v2.getWebhookSecret()
print(secret.signatureHeader, secret.signatureScheme, secret.replayToleranceSeconds)
_ = try await client.v2.rotateWebhookSecret()                   // las firmas antiguas dejan de verificarse al instante

let retry = try await client.v2.retryIngestWebhook(job.jobId)
print(retry.delivered, retry.attempts, retry.detail)
```

`retryIngestWebhook` responde `409` cuando el job no está completado y `400` cuando no tiene ningún webhook configurado. `V2ListOptions` recibe `skip` y `limit` (de 1 a 100, 20 por defecto).

### Watch

Vuelve a renderizar una página con una cadencia fija y recibe un aviso cuando cambie.

```swift
let watcher = try await client.v2.createWatcher("https://example.com/pricing", options: WatchCreateOptions(
    frequencyMinutes: 60,
    diffMode: .auto,
    webhookUrl: "https://my.app/hooks/changes",
    notifyEmail: true
))

let history = try await client.v2.getWatcherSnapshots(watcher.watcherId, options: SnapshotListOptions(limit: 10))
for snapshot in history.snapshots where snapshot.hasChanges {
    print(snapshot.checkedAt, snapshot.changeCount, snapshot.similarity ?? 0)
}
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `frequencyMinutes` | `Int?` | `60` | De 60 a 43200. El suelo horario es rígido. |
| `diffMode` | `WatchDiffMode?` | `.auto` | `.auto`, `.text`, `.structured`, `.tables`, `.metadata`. |
| `trackFields` | `JSONObject?` | ninguno | Subconjunto de campos o selectores entregado al motor de diferencias. |
| `webhookUrl` | `String?` | ninguno | Webhook de notificación de cambios, firmado con HMAC. |
| `notifyEmail` | `Bool?` | `true` | Avisa por correo al propietario del proyecto cuando hay cambios. |

```swift
// Una cadena vacía borra el webhook; nil lo deja como está.
_ = try await client.v2.updateWatcher(watcher.watcherId, updates: WatcherUpdate(webhookUrl: "", status: .paused))

_ = try await client.v2.listWatchers()
_ = try await client.v2.getWatcher(watcher.watcherId)
_ = try await client.v2.deleteWatcher(watcher.watcherId)   // borrado lógico, idempotente
```

`updateWatcher` exige al menos un campo y lanza `EnconvertError.invalidArgument` ante un `WatcherUpdate` vacío. `WatchUpdateStatus` acepta únicamente `.active` o `.paused`; el borrado pasa por `deleteWatcher`, que devuelve el watcher marcado como eliminado con estado `.deleted`.

<div class="alert alert-warning">
<strong>Las diferencias de los snapshots contienen contenido de página no confiable.</strong> <code>WatcherSnapshot.changes</code> es un array de objetos JSON crudos extraídos de la página vigilada. Escapa los valores antes de renderizarlos en cualquier sitio.
</div>

---

## Opciones de PDF

`PdfOptions` es compartido por `convertUrlToPdf`, `convertDocument`, `convertWebsiteToPdf`, `PerceiveOptions` y (solo para `grayscale`) `convertToPdf`. Solo se envían los campos que estableces.

```swift
let pdf = PdfOptions(
    pageSize: "A4",
    orientation: .landscape,
    margins: PdfMargins(top: 10, bottom: 10, left: 15, right: 15),
    scale: 0.9,
    grayscale: false,
    header: PdfHeaderFooter(content: "Quarterly Report", height: 15),
    footer: PdfHeaderFooter(content: "Confidential", height: 12)
)
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `pageSize` | `String?` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` y compañía. |
| `pageWidth`, `pageHeight` | `Double?` | Prevalecen sobre `pageSize` cuando se establecen juntos. |
| `orientation` | `PdfOrientation?` | `.portrait` o `.landscape`. Por defecto, vertical. |
| `margins` | `PdfMargins?` | `top`, `bottom`, `left`, `right`, cada uno un `Double?`. Los cuatro opcionales. |
| `scale` | `Double?` | Escala de renderizado, por ejemplo `0.9` para el 90%. |
| `grayscale` | `Bool?` | Posprocesa el PDF a escala de grises. |
| `header` | `PdfHeaderFooter?` | `content` (máximo 2000 caracteres) y `height`. |
| `footer` | `PdfHeaderFooter?` | La misma forma que `header`. |

---

## Manejo de errores

Swift recibe un único tipo de error, `EnconvertError`, modelado como un enum en lugar de como una jerarquía de clases. Cázalo con patrones de `catch`:

```swift
do {
    _ = try await client.v2.perceive("https://example.com")
} catch EnconvertError.authentication(let message) {
    print("invalid or missing API key: \(message)")
} catch EnconvertError.rateLimit(let message) {
    print("too many requests, back off and retry: \(message)")
} catch let error as EnconvertError {
    print("api error: \(error)")   // se renderiza como "[<status>] <message>"
}
```

| Caso | Se lanza en | Código de estado |
|------|-------------|------------------|
| `.authentication(message:)` | Clave inválida, ausente o revocada | `401`, `403` (ambos informan `401`) |
| `.quota(message:)` | Cualquier respuesta que la API conteste con `402` | `402` |
| `.rateLimit(message:)` | Límite de tasa superado | `429` |
| `.api(statusCode:message:)` | Cualquier otra 4xx o 5xx | el código real |
| `.invalidArgument(_:)` | Validación del lado del cliente, antes de cualquier solicitud | ninguno |

`EnconvertError` cumple `CustomStringConvertible` y `LocalizedError`, así que `String(describing:)`, `localizedDescription` y la interpolación de cadenas se renderizan todos como `"[<status>] <message>"`. Dos propiedades de conveniencia leen los mismos valores sin coincidencia de patrones: `error.statusCode` (`Int?`, `nil` para `.invalidArgument`) y `error.message` (el texto sin el prefijo entre corchetes). Una respuesta 2xx bien formada a la que le falte un campo que el SDK necesita aparece como `.api(statusCode: 0, ...)`, lo que separa un payload mal formado de un fallo HTTP real.

Los pares de conversión no admitidos, una llamada a `distill` con `urls` y `discoverFrom` a la vez, una llamada a `perceiveDirect` que pide dos artefactos y un `WatcherUpdate` vacío lanzan todos `.invalidArgument` antes de tocar la red. Los códigos de respuesta están catalogados en la [referencia de códigos de error](/es/docs/error-codes).

---

## Recuperación de timeouts

Los renderizados largos de URL y las conversiones de documentos grandes pueden sobrevivir al techo de 60 a 120 segundos de un proxy inverso incluso cuando el job termina bien en el servidor. El SDK sondea para salir de ahí, sin que escribas ni una línea:

1. Antes de cada conversión de un solo archivo y de una sola URL, el cliente genera un UUIDv4, le quita los guiones y lo envía como `job_id`.
2. Si esa solicitud vuelve con un estado de 500 o superior, el cliente pasa en silencio a `GET /v1/convert/status/{job_id}`, sondeando cada 3 segundos. Un `404` ahí significa "todavía no registrado" y mantiene el bucle en marcha.
3. Ante `success` devuelve el resultado. Ante `failed` lanza `.api(statusCode: 500, message:)` con el mensaje del servidor. El plazo de sondeo es de 5 minutos, tras el cual obtienes `.api(statusCode: 504, message: "Conversion timed out")`.

El cliente rellena `ConversionResult.jobId` incluso cuando la ruta síncrona tuvo éxito y la respuesta lo omitió, así que puedes pasárselo tú mismo a `getJobStatus`:

```swift
let status = try await client.getJobStatus(result.jobId ?? "")
if status.status == .success {
    print(status.presignedUrl ?? "")
} else if status.status == .failed {
    print(status.error ?? "conversion failed")
}
```

<div class="alert alert-info">
<strong>Los lotes de sitios web quedan excluidos a propósito.</strong> <code>convertWebsiteToPdf</code> y <code>convertWebsiteToScreenshot</code> no tienen una fila por job que sondear, así que un 5xx ahí se expone de inmediato en lugar de reintentarse. Los métodos V2 tampoco usan el respaldo por job.
</div>

---

## Configuración

```swift
let client = try Enconvert(
    apiKey: ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? "",
    baseURL: "https://api.enconvert.com",
    timeout: 300
)
```

| Parámetro | Tipo | Por defecto | Descripción |
|-----------|------|-------------|-------------|
| `apiKey` | `String` | obligatorio | Clave de API privada. Una cadena vacía lanza `EnconvertError.invalidArgument`. |
| `baseURL` | `String` | `https://api.enconvert.com` | Sobrescritura para un gateway autoalojado. Las barras finales se eliminan. |
| `timeout` | `TimeInterval` | `300` | Segundos. Establece tanto `timeoutIntervalForRequest` como `timeoutIntervalForResource` en la `URLSession` interna. |

La clave viaja en un encabezado `X-API-Key` en cada llamada a la API. Las descargas prefirmadas salen deliberadamente sin ella, ya que una URL de almacenamiento firmada se autentica sola y reenviar la clave a un host de almacenamiento la filtraría. Para cancelar una llamada concreta, envuélvela en una `Task` y cancélala: cada método es una función `async throws` corriente. `Enconvert` guarda solo propiedades `let` sobre una `URLSession`, así que construye un único cliente al arrancar y reutilízalo; `client.v2` es un espacio de nombres fino sobre el mismo transporte.

<div class="alert alert-warning">
<strong>Nunca escribas la clave de API en el código, y nunca la incluyas en el bundle de una app.</strong> Léela desde el entorno o desde tu gestor de secretos y mantén el cliente en un servidor que controles. El paquete compila para iOS, tvOS y watchOS para que puedas compartir código de modelo entre targets, pero el binario de una app es un artefacto público: cualquiera que extraiga tu clave privada puede ejecutar conversiones contra tu proyecto. Haz que la app llame a tu propio backend, y que el backend llame a EnConvert. Consulta <a href="/es/docs/authentication">autenticación</a> para los tipos de clave y su rotación.
</div>

---

## Forma del resultado

Las conversiones de un solo archivo y de una sola URL devuelven un `ConversionResult`:

```swift
public struct ConversionResult: Codable, Equatable, Sendable {
    public let presignedUrl: String
    public let objectKey: String
    public let filename: String
    public let fileSize: Int?
    public let conversionTimeSeconds: Double?
    public let jobId: String?
}
```

Las URL prefirmadas duran poco. Pasa `saveTo` para que el SDK transmita los bytes a disco por ti, creando los directorios padre que hagan falta, o descarga la URL tú mismo y guarda el archivo en tu propio bucket para tener acceso a largo plazo.

Los artefactos V2 llegan como valores `V2OutputArtifact` indexados por nombre de salida, cada uno con `url` (`String?`, prefirmada durante 15 minutos y vuelta a firmar en cada GET de estado), `objectKey`, `sizeBytes`, `contentType` y `expiresIn` (en segundos, 900 por defecto). `PerceiveResult` los envuelve con los metadatos de honestidad: `renderQuality`, `statusCode`, `deductions`, `cacheHit`, `warnings`, `contentHash`, `urlFinal`, `structured`, `extractionTier`, `tokens`, `costCents`, `durationMs` y `optionsEcho`, que devuelve el eco de las opciones que el servidor respetó realmente, con los secretos reducidos a booleanos. Los payloads definidos por quien llama (schemas de extracción, `data` destilado, `trackFields` de un watcher, `changes` de una diferencia, `extra` de una búsqueda) van y vuelven a través de `JSONValue`, un enum con los casos `.null`, `.bool`, `.number`, `.string`, `.array` y `.object`, más el alias `JSONObject` de `[String: JSONValue]`. Todos los tipos de resultado son `Codable`, `Equatable` y `Sendable`, así que cachear un resultado parseado en disco y recargarlo más tarde funciona sin más.

---

## Código fuente e incidencias

- **Paquete:** `Enconvert`, a través de Swift Package Manager. La versión se expone en tiempo de ejecución como la constante `VERSION` a nivel de módulo
- **GitHub:** [conversionapi/swift-sdk](https://github.com/conversionapi/swift-sdk)
- **Licencia:** MIT. Dependencias: ninguna, solo `URLSession` y Foundation

Lectura relacionada: [todos los SDK](/es/docs/sdks), [visión general de V2](/es/docs/v2-overview), [perceive](/es/docs/v2-perceive), [discover](/es/docs/v2-discover), [lookup](/es/docs/v2-lookup), [distill](/es/docs/v2-distill), [ingest](/es/docs/v2-ingest), [watch](/es/docs/v2-watch), [visión general de endpoints](/es/docs/endpoints-overview), [parámetros y opciones](/es/docs/parameters-options) y tu [panel de control](/es/dashboard) para las claves.

---

## Preguntas frecuentes

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

Añade `https://github.com/conversionapi/swift-sdk.git` a las dependencias de tu `Package.swift`, construye un cliente con `try Enconvert(apiKey:)` y luego llama a un método tipado como `convertDocument`, `convertImage` o `convertUrlToPdf`. Pasa `saveTo` en el struct de opciones y el SDK transmite el archivo terminado directamente a esa ruta, creando los directorios padre que hagan falta.

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

Llama a `try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf"))`. Establece `singlePage: false` para paginar en lugar de producir una única página continua, y pasa `pdfOptions:` para el tamaño de página, la orientación, los márgenes, la escala, la escala de grises, los encabezados y los pies. Recuerda que Swift quiere las etiquetas en el orden de declaración, así que `saveTo:` va antes que `singlePage:` y `pdfOptions:`.

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

`try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf"))`. El formato de salida es `"pdf"` por defecto, así que `outputFormat` se puede omitir. El mismo método se encarga de entradas XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML y TOML.

### ¿Cómo convierto HEIC a WebP en Swift?

`try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp"))`. El formato de entrada se lee de la extensión del nombre de archivo, y los 20 pares ordenados entre `jpeg`, `png`, `svg`, `heic` y `webp` funcionan de la misma manera. Los pares no admitidos lanzan `EnconvertError.invalidArgument` localmente, antes de enviar ninguna solicitud.

### ¿El SDK de Swift arrastra alguna dependencia de terceros?

No. `Package.swift` declara un array `dependencies` vacío. Todo se ejecuta sobre `URLSession`, `JSONSerialization` y `Foundation`, con `FoundationNetworking` importado de forma condicional para que el paquete compile tanto en Linux como en las plataformas de Apple.

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

Dos opciones. `client.convertUrlToMarkdown` devuelve Markdown con sabor GitHub y frontmatter YAML, y es el camino más sencillo. `client.v2.perceive` con `outputs: [.markdown]` te da el mismo Markdown más una puntuación `renderQuality`, un mapa `deductions`, `warnings` y la opción de añadir capturas de pantalla, enlaces o extracción estructurada en el mismo renderizado.

### ¿Qué significa la calidad de renderizado y por qué debería comprobarla?

`renderQuality` es un `Double?` de 0.0 a 1.0 adjunto a cada lectura V2. Baja cuando la página no se renderizó de forma honesta: un desafío antibots, una barrera de inicio de sesión, un banner de cookies sobre un armazón vacío o un estado de error HTTP. El contenido se sigue devolviendo en lugar de descartarse, así que comprueba la puntuación y el diccionario `deductions` que nombra cada penalización antes de darle el texto a un modelo.

### ¿Puedo usar el SDK de Swift dentro de una app de iOS o macOS?

Solo por detrás de tu propio backend. El paquete compila para iOS 15, tvOS 15, watchOS 8 y macOS 12 para que puedas compartir código de modelo entre targets, pero se autentica con una clave de API privada y los endpoints V2 rechazan de plano las claves públicas. Meter esa clave en el binario de una app se la entrega a cualquiera que descomprima el bundle. Llama a tu propio servidor desde la app, y llama a EnConvert desde el servidor.

### ¿Qué pasa cuando una conversión larga choca con el timeout del proxy?

El SDK envía un `job_id` generado por el cliente con cada conversión de un solo archivo y de una sola URL. Si la solicitud devuelve 500 o más, sondea `GET /v1/convert/status/{job_id}` cada 3 segundos durante hasta 5 minutos, devolviendo el resultado ante `success` y lanzando `.api(statusCode: 500, ...)` ante `failed`. Superar el plazo produce `.api(statusCode: 504, message: "Conversion timed out")`. Los envíos de lotes de sitios web se saltan este respaldo deliberadamente.

### ¿Cómo sé qué conversiones están admitidas antes de enviar una solicitud?

Llama a `validOutputsFor("json")` para conocer las salidas que admite un formato de entrada dado, o comprueba la pertenencia a `IMPLEMENTED_CONVERSIONS`, el conjunto de los 43 endpoints `{input}-to-{output}` implementados. `convertImage` y `convertDocument` ejecutan la misma comprobación internamente y lanzan `EnconvertError.invalidArgument` con las salidas válidas para esa entrada, antes de enviar ninguna solicitud.
