Swift File Conversion SDK#
Enconvert is the official EnConvert client for Swift, distributed through Swift Package Manager. It is built on URLSession with async/await, carries zero external dependencies, and targets Swift 5.9 and newer on macOS 12, iOS 15, tvOS 15, and watchOS 8. Twelve methods on the client cover file conversion and URL rendering (DOCX to PDF, HEIC to WebP, URL to PDF, URL to Markdown, whole-site batches), and the client.v2 namespace adds twenty-three web intelligence methods for perceiving, discovering, looking up, distilling, ingesting, and watching pages.
Enconvert · Source: conversionapi/swift-sdk · Swift: 5.9+ · Platforms: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+ · Dependencies: none
Install#
Add the package, then list the product in the target that uses it:
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")
])
]
In Xcode, use File > Add Package Dependencies and paste https://github.com/conversionapi/swift-sdk.git. On Linux the SDK imports FoundationNetworking conditionally, so nothing extra is needed on your side.
Quick start#
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 is throwing, not failable: an empty apiKey raises EnconvertError.invalidArgument before anything touches the network. Every request method is async throws, and the conversion methods are marked @discardableResult so a call made only for its saveTo side effect does not warn.
What the client exposes#
Twelve methods hang off Enconvert and map 1:1 to REST endpoints:
| Method | Endpoint | Returns |
|---|---|---|
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} (polled) |
BatchStatus |
client.v2 is an EnconvertV2 namespace holding twenty-three more methods across six capability groups:
| Group | Methods | Base path |
|---|---|---|
| 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 |
Options are passed as a struct with defaulted initializer parameters, so UrlToPdfOptions() means "all defaults" and you only name the fields you care about. Swift requires labeled arguments in declaration order, so keep saveTo: ahead of singlePage: and pdfOptions: when you set several at once.
File conversion#
Uploads accept a FileInput:
| Case | Use it for |
|---|---|
.path("report.docx") |
A file on disk. The basename decides the input format and the MIME type. |
.data(bytes) |
Raw bytes with no name. Uploaded as upload.bin, application/octet-stream. |
.wrapped(data: bytes, filename: "report.docx", contentType: nil) |
Raw bytes plus an explicit filename. A nil contentType is inferred from the extension. |
convertUrlToPdf#
Render any public URL to 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)
))
| Option | Type | Default | Description |
|---|---|---|---|
viewportWidth, viewportHeight |
Int? |
1920, 1080 |
Browser viewport size in pixels. |
loadMedia, enableScroll |
Bool? |
true |
Wait for images and video; scroll top to bottom to trigger lazy loaders. |
outputFilename |
String? |
auto | Override the generated filename. |
auth, cookies, headers |
HttpBasicAuth?, [BrowserCookie]?, [String: String]? |
none | HTTP Basic credentials, injected cookies (max 50), extra request headers (max 20, hop-by-hop rejected). |
saveTo |
String? |
none | Local path to write the PDF to. Parent directories are created. |
singlePage |
Bool? |
true |
true produces one continuous page. false paginates using pdfOptions.pageSize. |
pdfOptions |
PdfOptions? |
none | Page geometry. See PDF options. |
Pages behind a login take credentials, cookies, or headers:
_ = 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"
))
Do not combine auth with an Authorization entry in headers. The API rejects the conflict.
convertUrlToScreenshot#
Capture a PNG of any URL.
let shot = try await client.convertUrlToScreenshot(
"https://example.com",
options: UrlToScreenshotOptions(viewportWidth: 1440, saveTo: "shot.png")
)
UrlToScreenshotOptions accepts the same viewport, media, scroll, filename, and browser-access fields as UrlToPdfOptions, without singlePage and pdfOptions.
convertUrlToMarkdown#
Extract clean GitHub-Flavored Markdown from a URL. The converter strips navigation, footers, ads, and scripts, keeps the main article body, and prepends YAML frontmatter with the title, description, url, links, and images. Useful for RAG pipelines, importing third-party content into a CMS, or generating training data.
_ = try await client.convertUrlToMarkdown("https://example.com/article", options: UrlToMarkdownOptions(saveTo: "article.md"))
convertImage#
Convert between jpeg, png, svg, heic, and webp, or rasterize a PDF to JPEG.
let result = try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp"))
The input format comes from the filename extension (.jpg, .jpeg, .png, .svg, .heic, .webp, and .pdf for rasterization). outputFormat is required and accepts the aliases jpg, yml, htm, and md.
| Option | Type | Required | Description |
|---|---|---|---|
outputFormat |
String |
Yes | Target format, for example "webp". |
saveTo |
String? |
no | Local path to write the result to. |
outputFilename |
String? |
no | Override the generated filename. |
convertDocument#
Convert documents and structured-text formats. outputFormat defaults to "pdf".
// docx to pdf
_ = try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf"))
// json to yaml
_ = try await client.convertDocument(.path("data.json"), options: ConvertDocumentOptions(outputFormat: "yaml", saveTo: "data.yaml"))
// markdown to pdf with custom page setup
_ = try await client.convertDocument(.path("README.md"), options: ConvertDocumentOptions(
saveTo: "readme.pdf",
pdfOptions: PdfOptions(pageSize: "A4", margins: PdfMargins(top: 20, bottom: 20))
))
Supported inputs: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml. EPUB has no dedicated document pair, so send .epub files through convertToPdf or convertToMarkdown instead.
The SDK ships the gateway's full conversion table and validates every {input}-to-{output} pair locally, so an unsupported pair throws EnconvertError.invalidArgument with the list of valid outputs instead of paying for a round trip that is guaranteed to fail. There are 43 implemented pairs:
| Input | Outputs |
|---|---|
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 |
each of the other four (20 ordered pairs) |
pdf |
jpeg |
You can query that table yourself without making a request:
validOutputsFor("json") // ["csv", "toml", "xml", "yaml"]
IMPLEMENTED_CONVERSIONS.contains("heic-to-webp") // true
convertToMarkdown#
Convert an uploaded document of almost any format to clean Markdown, with the format auto-detected server-side. A good first stage for a RAG pipeline.
_ = try await client.convertToMarkdown(.path("handbook.docx"), options: ConvertToMarkdownOptions(saveTo: "handbook.md"))
PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, and legacy or ODF office files are accepted. Images are not. There are no PDF options on this endpoint.
convertToPdf#
Convert an uploaded file of almost any format to PDF: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, or an existing PDF passed through and normalized.
_ = try await client.convertToPdf(.path("slides.pptx"), options: ConvertToPdfOptions(saveTo: "slides.pdf"))
// PDF passthrough, converted to grayscale
_ = try await client.convertToPdf(
.path("scan.pdf"),
options: ConvertToPdfOptions(saveTo: "scan-gray.pdf", pdfOptions: PdfOptions(grayscale: true))
)
grayscale is honored here. convertToPdf forwards pdfOptions, but the anything-to-pdf endpoint reads grayscale and ignores the rest. Use convertDocument or convertUrlToPdf when you need page size, orientation, margins, scale, headers, or footers.
convertWebsiteToPdf and convertWebsiteToScreenshot#
Discover every page of a site, convert each one in the background, and collect a single ZIP. Both methods are async-only and require a private API key with crawl access.
let batch = try await client.convertWebsiteToPdf("https://example.com", options: WebsiteToPdfOptions(
crawlMode: .sitemap,
excludePatterns: ["/blog/tag/"]
))
print(batch.batchId, batch.urlCount, batch.discoveryMethod ?? "")
// Block until the batch settles and save the ZIP
let status = try await client.waitForBatch(batch.batchId, options: WaitForBatchOptions(saveTo: "site.zip"))
print("\(status.completed) of \(status.total) pages converted")
// Or poll it yourself
let snapshot = try await client.getBatchStatus(batch.batchId)
if snapshot.status != .processing {
print(snapshot.zipDownloadUrl ?? "")
}
| Option | Type | Default | Description |
|---|---|---|---|
crawlMode |
CrawlMode? |
.auto |
.auto, .sitemap (sitemap.xml only), or .full (sitemap plus BFS crawl). |
includePatterns, excludePatterns |
[String]? |
none | Allowlist then denylist for discovered URLs. Full crawl mode. |
notificationEmail |
String? |
project owner | Email notified when the batch finishes. |
callbackUrl |
String? |
none | Webhook POSTed when the batch finishes. |
singlePage, pdfOptions |
Bool?, PdfOptions? |
see above | PDF batches only. |
Both methods also take the viewport, media, scroll, and browser-access fields listed under convertUrlToPdf, applied to every page. waitForBatch polls every 5 seconds with a 30 minute deadline by default; override with WaitForBatchOptions(intervalMs:timeoutMs:saveTo:). Exceeding the deadline throws EnconvertError.api(statusCode: 504, ...). convertWebsiteToScreenshot behaves identically and produces a ZIP of PNGs.
Web intelligence (V2)#
Every V2 read carries a renderQuality score from 0.0 to 1.0, exposed as a Double? on PerceiveResult, PerceiveDirectResult, DistillItem, and WatcherSnapshot. A low score means the page did not render honestly: a bot challenge, a login wall, a cookie banner over an empty SPA shell, an HTTP error status. The content still comes back, flagged, next to a deductions dictionary naming each penalty that fired and a warnings array, so a bad read never quietly enters your agent's context. Gate on it before you trust anything:
if let quality = op.renderQuality, quality < 0.6 {
print("low quality read of \(op.url): \(op.deductions)")
}
Perceive#
Render one URL into the artifacts you ask for. Synchronous, with signed artifact URLs valid for 15 minutes. Every V2 method needs a private API key; public keys are rejected.
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)
// Re-sign the artifact URLs later
let again = try await client.v2.getPerceiveOperation(op.operationId)
| Option | Type | Default | Description |
|---|---|---|---|
outputs |
[PerceiveOutputName]? |
[.markdown, .structured] |
.markdown, .htmlCleaned, .htmlRaw, .screenshot, .screenshotFullPage, .pdf, .links, .images, .structured. |
extract |
[PerceiveExtractName]? |
none | .tables, .prices, .contacts, .metadata, .mainContent, .headings, .structuredData, .technologies, .all. |
schema |
JSONObject? |
none | JSON schema for structured extraction through the LLM tier. |
waitFor, waitTimeoutMs |
String?, Int? |
none, 30000 |
A CSS selector (optionally prefixed css:) or js:<expr> to await, and its budget in ms (0 to 60000). |
jsCode |
String? |
none | JavaScript executed after navigation, max 20000 characters. |
viewport |
PerceiveViewport? |
1920 by 1080 | width 320 to 3840, height 240 to 2160. |
headers, cookies, auth |
[String: String]?, [BrowserCookie]?, HttpBasicAuth? |
none | Request headers, injected cookies, HTTP Basic credentials. |
cacheMode |
PerceiveCacheMode? |
.enabled |
.enabled (1 hour cache), .bypass, .refresh. |
pdfOptions |
PdfOptions? |
none | Only meaningful when outputs includes .pdf. |
blockResources |
[PerceiveResourceType]? |
none | .image, .media, .font, .stylesheet, .script, .xhr, .fetch, .websocket, .manifest, .other. |
respectRobots, mobile |
Bool? |
server default | Honor robots.txt; emulate a mobile device. |
onlyMainContent |
Bool? |
true |
Strip nav, header, footer, and cookie banners from the markdown artifact and the main_content extract. Set false for the full page. |
directDownload |
Bool? |
false |
Stream raw bytes instead of a JSON envelope. Prefer perceiveDirect. |
proxyUrl, geolocation, and actionChain exist on PerceiveOptions and are serialized onto the wire, but the server currently answers 422 for all three. Leave them nil.
Streaming a single artifact straight to disk skips the JSON envelope and the signed-URL round trip. perceiveDirect checks locally that you asked for exactly one artifact-producing output, so a mistake costs nothing:
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"))
// Re-download a stored artifact later. Pass nil when the operation made only one.
let saved = try await client.v2.downloadPerceiveArtifact(direct.operationId, output: .pdf)
PerceiveDirectResult carries content, contentType, filename, operationId, objectKey, cacheHit, renderQuality, sourceStatusCode, contentHash, and warningsCount, all read from response headers. A 410 from downloadPerceiveArtifact means the artifact has aged out of its retention window.
Batches take up to 1000 URLs with one shared options block. Small batches finish inline; larger ones come back queued and you poll:
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 is .manifest (default) or .zip. directDownload is rejected with 422 on batches.
Discover#
Enumerate a site's URLs with no browser rendering. Fast, and it never runs a render.
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)
| Option | Type | Default | Description |
|---|---|---|---|
mode |
DiscoverMode? |
.hybrid |
.sitemap, .crawl, or .hybrid (sitemap plus HTTP crawl). |
maxUrls, maxDepth |
Int? |
100, 2 |
1 to 1000 URLs; crawl depth 1 to 5. |
includePatterns, excludePatterns |
[String]? |
none | Regex allowlist, then denylist applied after it. Max 50 patterns each. |
sameDomainOnly |
Bool? |
true |
Stay on the seed URL's domain. |
respectRobots |
Bool? |
server default | Honor robots.txt. |
DiscoverResult also reports pagesCrawled, robotsRespected, and warnings, and sources holds raw per-source counts taken before dedup.
Lookup#
Run a categorized web search, optionally rendering the top hits in the same call.
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)
}
| Option | Type | Default | Description |
|---|---|---|---|
category |
LookupCategory? |
.web |
.web, .news, .images, .scholar, .patents, .maps. |
country, locale |
String? |
none | Google gl country code ("us", "in") and hl interface language ("en"). |
timeFilter |
LookupTimeFilter? |
none | .hour, .day, .week, .month, .year. |
numResults, page |
Int? |
10, 1 |
1 to 100 results; page 1 to 10. |
location, autocorrect |
String?, Bool? |
none, true |
Free-text location such as "Austin, Texas"; let the provider correct the query. |
perceiveTop |
Int? |
0 |
Auto-render the top N result URLs, 0 to 10. Each runs a full browser render. |
LookupResult also exposes answerBox, knowledgeGraph, perceiveOperationIds, and credits.
Distill#
Pull structured data out of pages against a schema you define.
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 is a JSONObject, which is [String: JSONValue], so a flat {field: description} map or a full JSON-Schema object both work. The optional cssSchema runs first and answers whatever plain selectors can reach; only the fields it misses escalate to the LLM tier, and extractionTier reports which tiers actually answered (.css, .llm, .mixed, or .none). CssField.type is one of .text, .attribute, .html, .regex, .nested, .list, or .nestedList, nested up to 5 levels deep.
Swap urls for discoverFrom to discover then distill in one call. DistillDiscoverFrom takes url, mode (default .hybrid), and maxPages (1 to 50, default 10, capping both discovery and distillation):
_ = 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")]
))
Passing both urls and discoverFrom, or neither, throws EnconvertError.invalidArgument before any request is sent.
Ingest#
Turn a site, a URL list, or a stack of uploaded documents into chunked, RAG-ready JSONL. Always asynchronous.
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 ?? "")
}
| Option | Type | Default | Description |
|---|---|---|---|
mode |
IngestMode? |
.urls |
.urls, .sitemap, or .crawl. The fourth case, .files, is what ingestFiles reports back on its job; do not pass it here. |
url |
String? |
none | Seed URL. Required for .sitemap and .crawl, forbidden for .urls. |
urls |
[String]? |
none | Explicit URLs, max 1000. Required for .urls, forbidden otherwise. |
maxPages, maxDepth |
Int? |
50, 2 |
Discovery cap for .sitemap and .crawl, 1 to 1000; depth 1 to 5. |
sameDomainOnly |
Bool? |
true |
Stay on the seed URL's domain. |
includePatterns, excludePatterns |
[String]? |
none | Regex allowlist, then denylist. |
respectRobots |
Bool? |
server default | Honor robots.txt. |
waitFor, waitTimeoutMs |
String?, Int? |
30000 ms |
Selector or js: expression awaited per page, and its budget (0 to 60000). |
chunk |
IngestChunkOptions? |
none | maxWords 32 to 4000, default 512. sentenceOverlap 0 to 10, default 1. |
webhookUrl |
String? |
none | Completion webhook, HMAC-signed. |
The mode and URL rules above are enforced client-side: ingest throws EnconvertError.invalidArgument rather than making a doomed request if you pass urls with mode: .sitemap. Uploaded files run through the same pipeline and the same job lifecycle:
let files: [FileInput] = [.path("handbook.pdf"), .path("notes.docx")]
let fileJob = try await client.v2.ingestFiles(files, options: IngestFilesOptions(chunk: IngestChunkOptions(maxWords: 512)))
PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, and legacy or ODF office files are accepted, and at least one file is required. Job management and webhook plumbing:
let list = try await client.v2.listIngestJobs(V2ListOptions(limit: 20))
let canceled = try await client.v2.cancelIngestJob(job.jobId) // idempotent
let secret = try await client.v2.getWebhookSecret()
print(secret.signatureHeader, secret.signatureScheme, secret.replayToleranceSeconds)
_ = try await client.v2.rotateWebhookSecret() // old signatures stop verifying at once
let retry = try await client.v2.retryIngestWebhook(job.jobId)
print(retry.delivered, retry.attempts, retry.detail)
retryIngestWebhook answers 409 when the job is not completed and 400 when it has no webhook configured. V2ListOptions takes skip and limit (1 to 100, default 20).
Watch#
Re-render a page on a fixed cadence and get notified when it changes.
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)
}
| Option | Type | Default | Description |
|---|---|---|---|
frequencyMinutes |
Int? |
60 |
60 to 43200. The hourly floor is hard. |
diffMode |
WatchDiffMode? |
.auto |
.auto, .text, .structured, .tables, .metadata. |
trackFields |
JSONObject? |
none | Field or selector subset handed to the diff engine. |
webhookUrl |
String? |
none | Change-notification webhook, HMAC-signed. |
notifyEmail |
Bool? |
true |
Email the project owner on changes. |
// An empty string clears the webhook; nil leaves it alone.
_ = 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) // soft delete, idempotent
updateWatcher requires at least one field and throws EnconvertError.invalidArgument on an empty WatcherUpdate. WatchUpdateStatus accepts only .active or .paused; deleting goes through deleteWatcher, which returns the tombstoned watcher with status .deleted.
WatcherSnapshot.changes is an array of raw JSON objects lifted from the watched page. Escape the values before rendering them anywhere.
PDF options#
PdfOptions is shared by convertUrlToPdf, convertDocument, convertWebsiteToPdf, PerceiveOptions, and (for grayscale only) convertToPdf. Only the fields you set are sent.
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)
)
| Field | Type | Description |
|---|---|---|
pageSize |
String? |
"A4", "A3", "Letter", "Legal", and friends. |
pageWidth, pageHeight |
Double? |
Override pageSize when both are set together. |
orientation |
PdfOrientation? |
.portrait or .landscape. Defaults to portrait. |
margins |
PdfMargins? |
top, bottom, left, right, each a Double?. All four optional. |
scale |
Double? |
Render scale, for example 0.9 for 90%. |
grayscale |
Bool? |
Post-process the PDF to grayscale. |
header |
PdfHeaderFooter? |
content (max 2000 characters) and height. |
footer |
PdfHeaderFooter? |
Same shape as header. |
Error handling#
Swift gets one error type, EnconvertError, modeled as an enum rather than a class hierarchy. Match it with catch patterns:
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)") // renders as "[<status>] <message>"
}
| Case | Raised on | Status code |
|---|---|---|
.authentication(message:) |
Invalid, missing, or revoked key | 401, 403 (both report 401) |
.quota(message:) |
Any response the API answers with 402 |
402 |
.rateLimit(message:) |
Rate limit exceeded | 429 |
.api(statusCode:message:) |
Any other 4xx or 5xx | the actual code |
.invalidArgument(_:) |
Client-side validation, before any request | none |
EnconvertError conforms to CustomStringConvertible and LocalizedError, so String(describing:), localizedDescription, and string interpolation all render as "[<status>] <message>". Two convenience properties read the same values without pattern matching: error.statusCode (Int?, nil for .invalidArgument) and error.message (the text without the bracketed prefix). A well-formed 2xx response missing a field the SDK requires surfaces as .api(statusCode: 0, ...), which separates a malformed payload from a real HTTP failure.
Unsupported conversion pairs, a distill call with both urls and discoverFrom, a perceiveDirect call asking for two artifacts, and an empty WatcherUpdate all throw .invalidArgument before the network is touched. Response codes are catalogued in the error codes reference.
Timeout recovery#
Long URL renders and large document conversions can outlive a reverse proxy's 60 to 120 second ceiling even when the job finishes fine on the server. The SDK polls its way out of that, with no code from you:
- Before each single-file and single-URL conversion, the client generates a UUIDv4, strips the dashes, and sends it as
job_id. - If that request comes back with a status of 500 or higher, the client silently switches to
GET /v1/convert/status/{job_id}, polling every 3 seconds. A404there means "not recorded yet" and keeps the loop going. - On
successit returns the result. Onfailedit throws.api(statusCode: 500, message:)carrying the server's message. The polling deadline is 5 minutes, after which you get.api(statusCode: 504, message: "Conversion timed out").
ConversionResult.jobId is backfilled by the client even when the sync path succeeded and the response omitted it, so you can hand it to getJobStatus yourself:
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 and convertWebsiteToScreenshot have no per-job row to poll, so a 5xx there surfaces immediately instead of being retried. V2 methods do not use job fallback either.
Configuration#
let client = try Enconvert(
apiKey: ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? "",
baseURL: "https://api.enconvert.com",
timeout: 300
)
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
String |
required | Private API key. An empty string throws EnconvertError.invalidArgument. |
baseURL |
String |
https://api.enconvert.com |
Override for a self-hosted gateway. Trailing slashes are stripped. |
timeout |
TimeInterval |
300 |
Seconds. Sets both timeoutIntervalForRequest and timeoutIntervalForResource on the internal URLSession. |
The key travels as an X-API-Key header on every API call. Presigned downloads deliberately go out without it, since a signed storage URL authenticates itself and forwarding the key to a storage host would leak it. For per-call cancellation, wrap the call in a Task and cancel it: every method is a plain async throws function. Enconvert stores only let properties over one URLSession, so build a single client at startup and reuse it; client.v2 is a thin namespace over the same transport.
Result shape#
Single-file and single-URL conversions return a 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?
}
Presigned URLs are short-lived. Pass saveTo to have the SDK stream the bytes to disk for you, creating parent directories as needed, or fetch the URL yourself and store the file in your own bucket for long-term access.
V2 artifacts arrive as V2OutputArtifact values keyed by output name, each holding url (String?, pre-signed for 15 minutes and re-signed on every status GET), objectKey, sizeBytes, contentType, and expiresIn (seconds, 900 by default). PerceiveResult wraps them with the honesty metadata: renderQuality, statusCode, deductions, cacheHit, warnings, contentHash, urlFinal, structured, extractionTier, tokens, costCents, durationMs, and optionsEcho, which echoes back the options the server actually honored with secrets reduced to booleans. Caller-defined payloads (extraction schemas, distilled data, watcher trackFields, diff changes, lookup extra) round-trip through JSONValue, an enum with .null, .bool, .number, .string, .array, and .object cases, plus the JSONObject alias for [String: JSONValue]. Every result type is Codable, Equatable, and Sendable, so caching a parsed result to disk and reloading it later works out of the box.
Source and issues#
- Package:
Enconvert, via Swift Package Manager. Version exposed at runtime as the module-level constantVERSION - GitHub: conversionapi/swift-sdk
- License: MIT. Dependencies: none,
URLSessionand Foundation only
Related reading: all SDKs, V2 overview, perceive, discover, lookup, distill, ingest, watch, endpoints overview, parameters and options, and your dashboard for keys.
Frequently asked questions#
How do I convert files in Swift?#
Add https://github.com/conversionapi/swift-sdk.git to your Package.swift dependencies, build a client with try Enconvert(apiKey:), then call a typed method such as convertDocument, convertImage, or convertUrlToPdf. Pass saveTo in the options struct and the SDK streams the finished file straight to that path, creating parent directories as needed.
How do I convert a URL to PDF in Swift?#
Call try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf")). Set singlePage: false to paginate instead of producing one continuous page, and pass pdfOptions: for page size, orientation, margins, scale, grayscale, headers, and footers. Remember that Swift wants the labels in declaration order, so saveTo: comes before singlePage: and pdfOptions:.
How do I convert DOCX to PDF in Swift?#
try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf")). The output format defaults to "pdf", so outputFormat can be left out. The same method handles XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML, and TOML input.
How do I convert HEIC to WebP in Swift?#
try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp")). The input format is read from the filename extension, and all 20 ordered pairs among jpeg, png, svg, heic, and webp work the same way. Unsupported pairs throw EnconvertError.invalidArgument locally, before any request is sent.
Does the Swift SDK pull in any third-party dependencies?#
No. Package.swift declares an empty dependencies array. Everything runs on URLSession, JSONSerialization, and Foundation, with FoundationNetworking imported conditionally so the package builds on Linux as well as Apple platforms.
How do I scrape a web page into clean Markdown in Swift?#
Two options. client.convertUrlToMarkdown returns GitHub-Flavored Markdown with YAML frontmatter and is the simplest path. client.v2.perceive with outputs: [.markdown] gives you the same Markdown plus a renderQuality score, a deductions map, warnings, and the option to add screenshots, links, or structured extraction in the same render.
What does render quality mean and why should I check it?#
renderQuality is a Double? from 0.0 to 1.0 attached to every V2 read. It drops when the page did not render honestly: a bot challenge, a login wall, a cookie banner over an empty shell, or an HTTP error status. The content is still returned rather than swallowed, so check the score and the deductions dictionary naming each penalty before feeding the text to a model.
Can I use the Swift SDK inside an iOS or macOS app?#
Only behind your own backend. The package builds for iOS 15, tvOS 15, watchOS 8, and macOS 12 so you can share model code across targets, but it authenticates with a private API key and V2 endpoints reject public keys outright. Shipping that key in an app binary hands it to anyone who unzips the bundle. Call your own server from the app, and call EnConvert from the server.
What happens when a long conversion hits the proxy timeout?#
The SDK sends a client-generated job_id with each single-file and single-URL conversion. If the request returns 500 or higher, it polls GET /v1/convert/status/{job_id} every 3 seconds for up to 5 minutes, returning the result on success and throwing .api(statusCode: 500, ...) on failed. Exceeding the deadline yields .api(statusCode: 504, message: "Conversion timed out"). Website batch submissions deliberately skip this fallback.
How do I know which conversions are supported before I send a request?#
Call validOutputsFor("json") for the outputs a given input format supports, or check membership in IMPLEMENTED_CONVERSIONS, the set of all 43 implemented {input}-to-{output} endpoints. convertImage and convertDocument run the same check internally and throw EnconvertError.invalidArgument with the valid outputs listed for that input, before any request is sent.