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.
Enconvert · Fuente: conversionapi/swift-sdk · Swift: 5.9+ · Plataformas: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+ · Dependencias: ninguna
Instalación#
Añade el paquete y luego lista el producto en el target que lo usa:
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#
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.
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. |
Las páginas tras un inicio de sesión reciben credenciales, cookies o encabezados:
_ = 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.
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.
_ = 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.
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.
// 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:
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.
_ = 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.
_ = 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))
)
grayscale. convertToPdf reenvía pdfOptions, pero el endpoint anything-to-pdf lee grayscale e ignora el resto. Usa convertDocument o convertUrlToPdf cuando necesites tamaño de página, orientación, márgenes, escala, encabezados o pies.
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.
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:
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.
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. |
proxyUrl, geolocation y actionChain existen en PerceiveOptions y se serializan al cable, pero el servidor responde actualmente 422 para las tres. Déjalas en nil.
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:
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:
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.
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.
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.
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):
_ = 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.
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:
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:
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.
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. |
// 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.
WatcherSnapshot.changes es un array de objetos JSON crudos extraídos de la página vigilada. Escapa los valores antes de renderizarlos en cualquier sitio.
Opciones de PDF#
PdfOptions es compartido por convertUrlToPdf, convertDocument, convertWebsiteToPdf, PerceiveOptions y (solo para grayscale) convertToPdf. Solo se envían los campos que estableces.
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:
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.
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:
- 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. - 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. Un404ahí significa "todavía no registrado" y mantiene el bucle en marcha. - Ante
successdevuelve el resultado. Antefailedlanza.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:
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")
}
convertWebsiteToPdf y convertWebsiteToScreenshot 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.
Configuración#
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.
Forma del resultado#
Las conversiones de un solo archivo y de una sola URL devuelven un ConversionResult:
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 constanteVERSIONa nivel de módulo - GitHub: conversionapi/swift-sdk
- Licencia: MIT. Dependencias: ninguna, solo
URLSessiony Foundation
Lectura relacionada: todos los SDK, visión general de V2, perceive, discover, lookup, distill, ingest, watch, visión general de endpoints, parámetros y opciones y tu panel de control 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.