---
seo_title: Go File Conversion SDK: Zero Dependency API Client | EnConvert
meta_desc: Official EnConvert Go SDK for Go 1.21+. Standard library only, no third-party dependencies, with typed file conversion plus the V2 web intelligence namespace.
keywords: go file conversion sdk, convert files go, url to pdf go, go web scraping api, docx to pdf go, enconvert go sdk, golang html to pdf library, heic to webp golang, go markdown extraction api, golang website screenshot api, go rag ingestion pipeline, structured data extraction golang
---

# Go File Conversion SDK

`github.com/conversionapi/go-sdk` is the official EnConvert client for Go 1.21 and newer. It imports as package `enconvert` and carries zero third-party dependencies: everything runs on `net/http`, `encoding/json`, and `mime/multipart`. Twelve methods on the client cover file conversion, URL rendering, and whole-site batches (DOCX to PDF, HEIC to WebP, URL to PDF, URL to Markdown), and the `client.V2` namespace adds twenty-three web intelligence methods for perceiving, discovering, looking up, distilling, ingesting, and watching pages. Every call takes a `context.Context` first, so cancellation and deadlines stay in your hands.

<div class="alert alert-info">
<strong>Module:</strong> <code>github.com/conversionapi/go-sdk</code> · <strong>Package:</strong> <code>enconvert</code> · <strong>Source:</strong> <a href="https://github.com/conversionapi/go-sdk">conversionapi/go-sdk</a> · <strong>Go:</strong> 1.21+ · <strong>Dependencies:</strong> none
</div>

---

## Install

```bash
go get github.com/conversionapi/go-sdk
```

The module path ends in `go-sdk` but the package is named `enconvert`, so import it under an explicit name: `import enconvert "github.com/conversionapi/go-sdk"`.

---

## Quick start

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    enconvert "github.com/conversionapi/go-sdk"
)

func main() {
    client, err := enconvert.New(os.Getenv("ENCONVERT_API_KEY"))
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()

    result, err := client.ConvertURLToPDF(ctx, "https://example.com", enconvert.URLToPDFOptions{SaveTo: "page.pdf"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result.Filename, result.PresignedURL)
}
```

`New` returns an error only when the API key is empty; see [Configuration](#configuration) for the functional options. Reading a page the way an agent should, with a quality score attached, is one call on the V2 namespace:

```go
op, err := client.V2.Perceive(ctx, "https://example.com", enconvert.PerceiveOptions{
    Outputs: []enconvert.PerceiveOutputName{enconvert.PerceiveOutputMarkdown},
})
fmt.Println(op.Outputs["markdown"].URL, *op.RenderQuality) // e.g. 0.93
```

Every snippet below assumes a `client` and a `ctx` built exactly like this.

---

## What the client exposes

Twelve methods hang off `*enconvert.Client` and map 1:1 to REST endpoints. Options are always passed as a struct value, never a pointer, so the zero value (`enconvert.URLToPDFOptions{}`) means "all defaults", and optional numbers and booleans are pointer fields throughout: use the `enconvert.Int`, `enconvert.Bool`, `enconvert.Float64`, and `enconvert.String` helpers instead of a throwaway local variable.

| Method | Endpoint | Returns |
|--------|----------|---------|
| `ConvertURLToPDF(ctx, url, opts)` | `POST /v1/convert/url-to-pdf` | `ConversionResult` |
| `ConvertURLToScreenshot(ctx, url, opts)` | `POST /v1/convert/url-to-screenshot` | `ConversionResult` |
| `ConvertURLToMarkdown(ctx, url, opts)` | `POST /v1/convert/url-to-markdown` | `ConversionResult` |
| `ConvertImage(ctx, file, opts)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `ConvertDocument(ctx, file, opts)` | `POST /v1/convert/{from}-to-{to}` | `ConversionResult` |
| `ConvertToMarkdown(ctx, file, opts)` | `POST /v1/convert/anything-to-markdown` | `ConversionResult` |
| `ConvertToPDF(ctx, file, opts)` | `POST /v1/convert/anything-to-pdf` | `ConversionResult` |
| `ConvertWebsiteToPDF(ctx, url, opts)` | `POST /v1/convert/website-to-pdf` | `BatchSubmission` |
| `ConvertWebsiteToScreenshot(ctx, url, opts)` | `POST /v1/convert/website-to-screenshot` | `BatchSubmission` |
| `GetJobStatus(ctx, jobID)` | `GET /v1/convert/status/{jobID}` | `JobStatus` |
| `GetBatchStatus(ctx, batchID)` | `GET /v1/convert/batch/{batchID}` | `BatchStatus` |
| `WaitForBatch(ctx, batchID, opts)` | `GET /v1/convert/batch/{batchID}` (polled) | `BatchStatus` |

`client.V2` holds 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` |

---

## File conversion

Uploads accept any `FileSource`: `enconvert.FilePath("report.docx")` for a path on disk (the basename decides the input format and MIME type), `enconvert.FileBytes(buf)` for raw bytes with no name (uploaded as `upload.bin`, `application/octet-stream`), or `enconvert.FileInput{Data: buf, Filename: "report.docx"}` for raw bytes with an explicit filename and optional `ContentType`.

### ConvertURLToPDF

Render any reachable URL to PDF.

```go
result, err := client.ConvertURLToPDF(ctx, "https://example.com", enconvert.URLToPDFOptions{
    SinglePage:       enconvert.Bool(false),
    PDFOptions:       &enconvert.PDFOptions{PageSize: "A4", Orientation: "landscape"},
    URLRenderOptions: enconvert.URLRenderOptions{ViewportWidth: enconvert.Int(1440)},
    SaveTo:           "report.pdf",
})
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `SaveTo` | `string` | -- | Local path to stream the PDF to. Parent directories are created for you. |
| `SinglePage` | `*bool` | `true` | `true` renders one continuous page. `false` paginates using `PDFOptions.PageSize`. |
| `PDFOptions` | `*PDFOptions` | -- | Page size, orientation, margins, scale, grayscale, header, footer. See [PDF options](#pdf-options). |
| `ViewportWidth`, `ViewportHeight` | `*int` | `1920`, `1080` | Browser viewport size in pixels. |
| `LoadMedia` | `*bool` | `true` | Wait for images and video before capture. |
| `EnableScroll` | `*bool` | `true` | Scroll top to bottom to trigger lazy loaders. |
| `OutputFilename` | `string` | auto | Override the generated filename. |
| `Auth` | `*HTTPBasicAuth` | -- | HTTP Basic credentials for pages behind a login. |
| `Cookies`, `Headers` | `[]BrowserCookie`, `map[string]string` | -- | Cookies injected before rendering (max 50) and extra request headers (max 20, hop-by-hop rejected). |

Everything from `ViewportWidth` down lives on the embedded `URLRenderOptions` struct, shared by every URL-based method.

<div class="alert alert-warning">
<strong>Do not combine <code>Auth</code> with an <code>Authorization</code> header.</strong> The API rejects the conflict rather than guessing which one you meant.
</div>

### ConvertURLToScreenshot and ConvertURLToMarkdown

Capture a PNG of any URL, or extract clean GitHub-Flavored Markdown with YAML frontmatter (title, description, url, links, images). The Markdown converter strips navigation, footers, ads, and scripts, and keeps the main article body.

```go
shot, err := client.ConvertURLToScreenshot(ctx, "https://example.com", enconvert.URLToScreenshotOptions{
    URLRenderOptions: enconvert.URLRenderOptions{ViewportWidth: enconvert.Int(1440)},
    SaveTo:           "shot.png",
})

article, err := client.ConvertURLToMarkdown(ctx, "https://example.com/article",
    enconvert.URLToMarkdownOptions{SaveTo: "article.md"})
```

Both take the same `URLRenderOptions` as `ConvertURLToPDF`, plus `SaveTo`. Neither accepts `SinglePage` or `PDFOptions`.

### ConvertImage

Convert between `jpeg`, `png`, `svg`, `heic`, and `webp` in any direction, or rasterize a PDF to JPEG.

```go
// From a path
result, err := client.ConvertImage(ctx, enconvert.FilePath("photo.heic"),
    enconvert.ConvertImageOptions{OutputFormat: "webp", SaveTo: "photo.webp"})

// From bytes already in memory
buf, _ := os.ReadFile("photo.heic")
result, err = client.ConvertImage(ctx, enconvert.FileInput{Data: buf, Filename: "photo.heic"},
    enconvert.ConvertImageOptions{OutputFormat: "webp", SaveTo: "photo.webp"})
```

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `OutputFormat` | `string` | Yes | `jpeg`, `png`, `svg`, `heic`, or `webp`. `jpg` is accepted as an alias for `jpeg`. |
| `SaveTo` | `string` | -- | Local path to stream the result to. |
| `OutputFilename` | `string` | -- | Override the generated filename. |

The input format comes from the filename extension (`.jpg`, `.jpeg`, `.png`, `.svg`, `.heic`, `.webp`, `.pdf`). Unsupported pairs fail locally, before any network call, with an error listing what is available:

```go
enconvert.ValidOutputsFor("pdf")  // []string{"jpeg"}
enconvert.ValidOutputsFor("json") // []string{"csv", "toml", "xml", "yaml"}
```

### ConvertDocument

Convert documents and data formats. `OutputFormat` defaults to `pdf` when left empty.

```go
// docx to pdf
_, err := client.ConvertDocument(ctx, enconvert.FilePath("report.docx"),
    enconvert.ConvertDocumentOptions{SaveTo: "report.pdf"})

// json to yaml
_, err = client.ConvertDocument(ctx, enconvert.FilePath("data.json"),
    enconvert.ConvertDocumentOptions{OutputFormat: "yaml", SaveTo: "data.yaml"})
```

**Supported inputs:** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `OutputFormat` | `string` | `"pdf"` | Target format. `yml`, `htm`, `md`, and `jpg` are normalized to their canonical names. |
| `SaveTo` | `string` | -- | Local path to stream the result to. |
| `OutputFilename` | `string` | -- | Override the generated filename. |
| `PDFOptions` | `*PDFOptions` | -- | Page setup. Only meaningful when the output is PDF. |

The 43 implemented pairs, exposed as the `enconvert.ImplementedConversions` map, are: `json` to `csv`, `toml`, `xml`, `yaml`; `xml` to `csv`, `json`; `yaml` to `json`; `csv` to `json`, `xml`; `toml` to `json`; `markdown` to `html`, `pdf`; `html` to `pdf`; `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, and `numbers` to `pdf`; all 20 ordered pairs among `jpeg`, `png`, `svg`, `heic`, `webp`; and `pdf` to `jpeg`.

EPUB has no dedicated document pair. Send `.epub` files through `ConvertToPDF` or `ConvertToMarkdown` instead.

### ConvertToMarkdown

Auto-detect the input server-side and return clean Markdown. This is the RAG-ingestion building block: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats.

```go
_, err := client.ConvertToMarkdown(ctx, enconvert.FilePath("handbook.pdf"),
    enconvert.ConvertToMarkdownOptions{SaveTo: "handbook.md"})
```

`ConvertToMarkdownOptions` has two fields, `SaveTo` and `OutputFilename`. Images are not supported by this endpoint, and there are no PDF options on it. The output keeps the document's heading hierarchy, so a semantic chunker can split on headings rather than arbitrary character counts.

### ConvertToPDF

Auto-detect the input server-side and return a PDF: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, or an existing PDF passed through for normalization.

```go
_, err := client.ConvertToPDF(ctx, enconvert.FilePath("slides.pptx"),
    enconvert.ConvertToPDFOptions{SaveTo: "slides.pdf"})

// PDF passthrough, converted to grayscale
_, err = client.ConvertToPDF(ctx, enconvert.FilePath("scan.pdf"), enconvert.ConvertToPDFOptions{
    PDFOptions: &enconvert.PDFOptions{Grayscale: enconvert.Bool(true)},
    SaveTo:     "scan-gray.pdf",
})
```

<div class="alert alert-warning">
<strong>Only <code>PDFOptions.Grayscale</code> is honored here.</strong> Every other geometry field is ignored on <code>anything-to-pdf</code>. Use <code>ConvertDocument</code> or <code>ConvertURLToPDF</code> when you need page size, orientation, margins, scale, headers, or footers.
</div>

`ConvertToPDFOptions` has three fields: `SaveTo`, `OutputFilename`, and `PDFOptions`.

### ConvertWebsiteToPDF and ConvertWebsiteToScreenshot

Discover every page of a site, convert each in the background, and collect a single ZIP. Both are async only: they return a `BatchSubmission`, and you poll with `GetBatchStatus` or block with `WaitForBatch`. A private API key with crawl access is required.

```go
batch, err := client.ConvertWebsiteToPDF(ctx, "https://example.com", enconvert.WebsiteToPDFOptions{
    WebsiteConversionOptions: enconvert.WebsiteConversionOptions{CrawlMode: enconvert.CrawlModeSitemap},
})

status, err := client.WaitForBatch(ctx, batch.BatchID, enconvert.WaitForBatchOptions{SaveTo: "site.zip"})
fmt.Println(status.Completed, "of", status.Total, "pages converted")
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `CrawlMode` | `CrawlMode` | `auto` | `CrawlModeAuto`, `CrawlModeSitemap` (sitemap.xml only), or `CrawlModeFull` (sitemap plus BFS crawl). |
| `IncludePatterns` | `[]string` | -- | Only crawl URLs matching these patterns, full crawl mode. |
| `ExcludePatterns` | `[]string` | -- | Skip URLs matching these patterns, full crawl mode. |
| `NotificationEmail` | `string` | project owner | Address notified when the batch finishes. |
| `CallbackURL` | `string` | -- | Webhook POSTed when the batch finishes. |
| `SinglePage` | `*bool` | server default | PDF only. Continuous page versus paginated. |
| `PDFOptions` | `*PDFOptions` | -- | PDF only. Applied to every page. |

Everything on `URLRenderOptions` is accepted here too, and is sent per page only when you set it. `WaitForBatchOptions` takes `Interval` (default 5 seconds), `Timeout` (default 30 minutes), and `SaveTo`. Blowing the timeout returns an `*APIError` with status `504`; a finished batch with no ZIP returns one with status `500`. `ConvertWebsiteToScreenshot` is identical minus `SinglePage` and `PDFOptions`, 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 `*float64` on `PerceiveResult`, `DistillItem`, `WatcherSnapshot`, and the direct-download result. A low score means the page did not render cleanly: a challenge page, a cookie wall, an empty SPA shell, an HTTP error. The content still comes back, flagged, alongside a `Deductions` map naming each penalty that fired and a `Warnings` slice, so a bad read never quietly enters your agent's context. All V2 methods require a private API key; public keys are rejected. Gate on the score before you trust anything:

```go
if op.RenderQuality != nil && *op.RenderQuality < 0.6 {
    log.Printf("low quality read of %s: %v", op.URL, op.Deductions)
}
```

### Perceive

Render one URL into the artifacts you ask for. Synchronous, with artifact URLs signed for 15 minutes.

```go
op, err := client.V2.Perceive(ctx, "https://example.com", enconvert.PerceiveOptions{
    Outputs:  []enconvert.PerceiveOutputName{enconvert.PerceiveOutputMarkdown, enconvert.PerceiveOutputStructured},
    Extract:  []enconvert.PerceiveExtractName{enconvert.PerceiveExtractTables, enconvert.PerceiveExtractMetadata},
    Viewport: &enconvert.PerceiveViewport{Width: enconvert.Int(1440)},
})
fmt.Println(op.OperationID, op.Outputs["markdown"].URL, op.Structured, op.ExtractionTier)

// Re-sign the artifact URLs later
again, err := client.V2.GetPerceiveOperation(ctx, op.OperationID)
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Outputs` | `[]PerceiveOutputName` | `markdown`, `structured` | `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. |
| `Extract` | `[]PerceiveExtractName` | -- | `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `Schema` | `map[string]any` | -- | JSON schema for structured extraction through the LLM tier. |
| `WaitFor`, `WaitTimeoutMs` | `string`, `*int` | `30000` ms | A CSS selector, optionally prefixed `css:`, or `js:<expr>`, and its budget (0 to 60000). |
| `JSCode` | `string` | -- | JavaScript executed after navigation, max 20000 characters. |
| `Viewport` | `*PerceiveViewport` | 1920 x 1080 | `Width` 320 to 3840, `Height` 240 to 2160. |
| `Headers`, `Cookies`, `Auth` | `map[string]string`, `[]BrowserCookie`, `*HTTPBasicAuth` | -- | Request headers, injected cookies, HTTP Basic credentials. |
| `CacheMode` | `PerceiveCacheMode` | `enabled` | `enabled` (1 hour cache), `bypass`, `refresh`. |
| `PDFOptions` | `*PDFOptions` | -- | Only meaningful when `Outputs` includes `pdf`. |
| `BlockResources` | `[]PerceiveResourceType` | -- | `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`. |

<div class="alert alert-warning">
<strong>Three options are declared but not live yet.</strong> <code>ProxyURL</code>, <code>Geolocation</code>, and <code>ActionChain</code> are accepted by the Go struct and serialized, but the server currently answers <code>422</code> for all three. Leave them unset.
</div>

Streaming a single artifact straight to disk skips the JSON envelope. `PerceiveDirect` validates locally that you asked for exactly one artifact-producing output:

```go
direct, err := client.V2.PerceiveDirect(ctx, "https://example.com", enconvert.PerceiveOptions{
    Outputs: []enconvert.PerceiveOutputName{enconvert.PerceiveOutputPDF},
})
os.WriteFile(direct.Filename, direct.Content, 0o644)

// Re-download a stored artifact later. Pass "" when the operation made only one.
saved, err := client.V2.DownloadPerceiveArtifact(ctx, direct.OperationID, enconvert.PerceiveOutputPDF)
```

`PerceiveDirectResult` carries `Content`, `ContentType`, `Filename`, `OperationID`, `ObjectKey`, `CacheHit`, `RenderQuality`, `SourceStatusCode`, `ContentHash`, and `WarningsCount`, all read from response headers. A `410` `*APIError` from `DownloadPerceiveArtifact` means the artifact 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`, so poll `GetPerceiveBatch` until `Status` is `PerceiveBatchStatusCompleted` and read `Zip` or `Items`:

```go
batch, err := client.V2.PerceiveBatch(ctx, []string{"https://a.example", "https://b.example"},
    enconvert.PerceiveBatchOptions{OutputMode: enconvert.PerceiveBatchOutputZip})
done, err := client.V2.GetPerceiveBatch(ctx, batch.JobID)
```

`OutputMode` is `PerceiveBatchOutputManifest` (default) or `PerceiveBatchOutputZip`. `DirectDownload` is rejected with `422` on batches.

### Discover

Enumerate a site's URLs from its sitemap, an HTTP crawl, or both. No browser is started, so it is fast and cheap.

```go
found, err := client.V2.Discover(ctx, "https://example.com", enconvert.DiscoverOptions{
    Mode: enconvert.DiscoverModeHybrid, MaxURLs: enconvert.Int(200), ExcludePatterns: []string{"/tag/"},
})
fmt.Println(found.Total, found.Truncated, found.Sources)
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Mode` | `DiscoverMode` | `hybrid` | `DiscoverModeSitemap`, `DiscoverModeCrawl`, or `DiscoverModeHybrid` (sitemap plus HTTP crawl). |
| `MaxURLs`, `MaxDepth` | `*int` | `100`, `2` | 1 to 1000 URLs, crawl depth 1 to 5. |
| `IncludePatterns`, `ExcludePatterns` | `[]string` | -- | Regex allowlist then denylist, `re.search` semantics, max 50 each. |
| `SameDomainOnly` | `*bool` | `true` | Stay on the seed URL's domain. |
| `RespectRobots` | `*bool` | server default | Honor `robots.txt`. |

`DiscoverResult` gives you `URL`, `Mode`, `Total`, `URLs`, `PagesCrawled`, `Truncated`, `RobotsRespected`, `Sources` (raw counts per source before dedup), and `Warnings`.

### Lookup

Categorized web search, optionally rendering the top results in the same round trip.

```go
search, err := client.V2.Lookup(ctx, "best static site generators", enconvert.LookupOptions{
    Category: enconvert.LookupCategoryWeb, NumResults: enconvert.Int(10), PerceiveTop: enconvert.Int(3),
})
for _, hit := range search.Results {
    fmt.Println(hit.Position, hit.Title, hit.URL)
    if hit.Perceive != nil {
        fmt.Println(hit.Perceive.Outputs["markdown"].URL)
    }
}
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Category` | `LookupCategory` | `web` | `web`, `news`, `images`, `scholar`, `patents`, `maps`. |
| `Country`, `Locale` | `string` | -- | Google `gl` country code (`us`, `in`) and `hl` interface language (`en`). |
| `TimeFilter` | `LookupTimeFilter` | -- | `hour`, `day`, `week`, `month`, `year`. |
| `NumResults`, `Page` | `*int` | `10`, `1` | 1 to 100 results, page 1 to 10. |
| `Location` | `string` | -- | Free-text location, for example `Austin, Texas`. |
| `Autocorrect` | `*bool` | `true` | Let the provider fix typos in the query. |
| `PerceiveTop` | `*int` | `0` | 0 to 10. Runs a full browser render of the top N result URLs and attaches each `PerceiveResult` inline. |

`LookupResult` also carries `AnswerBox`, `KnowledgeGraph`, `PerceiveOperationIDs`, and `Warnings`.

### Distill

Schema-driven structured extraction. Provide exactly one of `URLs` (max 50) or `DiscoverFrom`; `Schema` is always required. Both rules are checked locally before any request goes out.

```go
extraction, err := client.V2.Distill(ctx, enconvert.DistillOptions{
    URLs:   []string{"https://example.com/pricing"},
    Schema: map[string]any{"plans": "list of plan names with monthly prices"},
    CSSSchema: &enconvert.CSSSchema{
        BaseSelector: ".plan-card",
        Fields: []enconvert.CSSField{
            {Name: "name", Type: enconvert.CSSFieldText, Selector: "h3"},
            {Name: "price", Type: enconvert.CSSFieldText, Selector: ".price"},
        },
    },
})
first := extraction.Results[0]
fmt.Println(first.Data, first.ExtractionTier, first.FieldsFromCSS, first.FieldsFromLLM)
```

The optional `CSSSchema` runs first and answers whatever it can with plain selectors; 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 `nested_list`, 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):

```go
_, err = client.V2.Distill(ctx, enconvert.DistillOptions{
    DiscoverFrom: &enconvert.DistillDiscoverFrom{URL: "https://example.com", MaxPages: enconvert.Int(10)},
    Schema:       map[string]any{"title": "page title", "summary": "one-line summary"},
})
```

### Ingest

Turn a site, a URL list, or a stack of uploaded documents into chunked, RAG-ready JSONL. Always asynchronous.

```go
job, err := client.V2.Ingest(ctx, enconvert.IngestOptions{
    Mode:       enconvert.IngestModeSitemap,
    URL:        "https://docs.example.com",
    MaxPages:   enconvert.Int(100),
    Chunk:      &enconvert.IngestChunkOptions{MaxWords: enconvert.Int(512), SentenceOverlap: enconvert.Int(1)},
    WebhookURL: "https://my.app/hooks/enconvert",
})

status, err := client.V2.GetIngestJob(ctx, job.JobID)
if status.Status == enconvert.IngestStatusCompleted {
    fmt.Println(status.TotalChunks, status.OutputURL)
}
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `Mode` | `IngestMode` | `urls` | `IngestModeURLs`, `IngestModeSitemap`, `IngestModeCrawl`, `IngestModeFiles`. |
| `URL` | `string` | -- | Seed URL. Required for `sitemap` and `crawl`, forbidden for `urls`. |
| `URLs` | `[]string` | -- | Explicit URLs, max 1000. Required for `urls`, forbidden otherwise. |
| `MaxPages`, `MaxDepth` | `*int` | `50`, `2` | Discovery cap 1 to 1000 for `sitemap` and `crawl`, depth 1 to 5. |
| `SameDomainOnly` | `*bool` | `true` | Stay on the seed URL's domain. |
| `IncludePatterns`, `ExcludePatterns` | `[]string` | -- | 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` | -- | `MaxWords` 32 to 4000, default 512. `SentenceOverlap` 0 to 10, default 1. |
| `WebhookURL` | `string` | -- | Completion webhook, HMAC-signed. |

The mode and URL rules above are enforced client-side: `Ingest` returns a plain Go error, not an API round trip, if you send `URLs` with `Mode: IngestModeSitemap`.

Uploaded files run through the same pipeline and the same job lifecycle:

```go
fileJob, err := client.V2.IngestFiles(ctx,
    []enconvert.FileSource{enconvert.FilePath("handbook.pdf"), enconvert.FilePath("notes.docx")},
    enconvert.IngestFilesOptions{Chunk: &enconvert.IngestChunkOptions{MaxWords: enconvert.Int(512)}})
```

PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, and legacy or ODF office files are accepted; at least one file is required. Job management and webhook plumbing:

```go
list, err := client.V2.ListIngestJobs(ctx, enconvert.V2ListOptions{Limit: enconvert.Int(20)})
canceled, err := client.V2.CancelIngestJob(ctx, job.JobID) // idempotent
secret, err := client.V2.GetWebhookSecret(ctx)             // Secret, SignatureHeader, SignatureScheme, ...
rotated, err := client.V2.RotateWebhookSecret(ctx)         // old signatures stop verifying at once
retry, err := client.V2.RetryIngestWebhook(ctx, job.JobID)
```

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

```go
watcher, err := client.V2.CreateWatcher(ctx, "https://example.com/pricing", enconvert.WatchCreateOptions{
    FrequencyMinutes: enconvert.Int(60),
    DiffMode:         enconvert.WatchDiffAuto,
    WebhookURL:       "https://my.app/hooks/changes",
})

history, err := client.V2.GetWatcherSnapshots(ctx, watcher.WatcherID, enconvert.SnapshotListOptions{Limit: enconvert.Int(10)})
for _, snap := range history.Snapshots {
    fmt.Println(snap.CheckedAt, snap.HasChanges, snap.ChangeCount, snap.Changes)
}
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `FrequencyMinutes` | `*int` | `60` | 60 to 43200. The hourly floor is hard. |
| `DiffMode` | `WatchDiffMode` | `auto` | `WatchDiffAuto`, `WatchDiffText`, `WatchDiffStructured`, `WatchDiffTables`, `WatchDiffMetadata`. |
| `TrackFields` | `map[string]any` | -- | Field or selector subset handed to the diff engine. |
| `WebhookURL`, `NotifyEmail` | `string`, `*bool` | --, `true` | HMAC-signed change webhook, and whether to email the project owner. |

```go
// A pointer to the empty string clears the webhook; nil leaves it alone.
_, err = client.V2.UpdateWatcher(ctx, watcher.WatcherID, enconvert.WatcherUpdate{
    Status: enconvert.WatcherStatusPaused, WebhookURL: enconvert.String(""),
})
_, err = client.V2.DeleteWatcher(ctx, watcher.WatcherID) // soft delete, idempotent
```

`UpdateWatcher` requires at least one field and returns a plain Go error if you hand it an empty struct. `Status` accepts only `WatcherStatusActive` or `WatcherStatusPaused`; deleting goes through `DeleteWatcher`, which returns the tombstoned watcher with status `deleted`. `ListWatchers` and `GetWatcher` round out the group.

<div class="alert alert-warning">
<strong>Snapshot diffs contain untrusted page content.</strong> <code>WatcherSnapshot.Changes</code> is a slice of raw maps lifted from the watched page. Escape the values before rendering them anywhere.
</div>

---

## PDF options

`PDFOptions` is shared by `ConvertURLToPDF`, `ConvertDocument`, `ConvertWebsiteToPDF`, `PerceiveOptions`, and (for `Grayscale` only) `ConvertToPDF`. Only the fields you set are sent.

```go
opts := &enconvert.PDFOptions{
    PageSize:    "A4",
    Orientation: "landscape",
    Margins:     &enconvert.PDFMargins{Top: enconvert.Float64(10), Bottom: enconvert.Float64(10)},
    Scale:       enconvert.Float64(0.9),
    Footer:      &enconvert.PDFHeaderFooter{Content: "Confidential", Height: enconvert.Float64(20)},
}
```

| Field | Type | Description |
|-------|------|-------------|
| `PageSize` | `string` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"`, and friends. |
| `PageWidth`, `PageHeight` | `*float64` | Set both together to override `PageSize`. |
| `Orientation` | `string` | `"portrait"` or `"landscape"`. Defaults to portrait. |
| `Margins` | `*PDFMargins` | `Top`, `Bottom`, `Left`, `Right`, each a `*float64` in millimetres. All four optional. |
| `Scale` | `*float64` | Render scale, for example `0.9` for 90%. |
| `Grayscale` | `*bool` | Post-process the PDF to grayscale. |
| `Header`, `Footer` | `*PDFHeaderFooter` | Each has `Content` (max 2000 characters) and `Height`. |

---

## Error handling

Go has no exception classes, so every API failure is one concrete type, `*enconvert.APIError`, plus three predicates. `Error()` renders as `"[<status>] <message>"`.

```go
result, err := client.ConvertURLToPDF(ctx, "https://example.com", enconvert.URLToPDFOptions{})
switch {
case err == nil:
    fmt.Println(result.PresignedURL)
case enconvert.IsAuthenticationError(err):
    log.Println("invalid or missing API key")
case enconvert.IsRateLimitError(err):
    log.Println("too many requests, back off and retry")
case enconvert.IsQuotaError(err):
    log.Println("request rejected with 402")
default:
    var apiErr *enconvert.APIError
    if errors.As(err, &apiErr) {
        log.Printf("api error [%d]: %s", apiErr.StatusCode, apiErr.Message)
    } else {
        log.Println(err) // network failure, context cancellation, local validation
    }
}
```

| Check | Raised on | Status code |
|-------|-----------|-------------|
| `IsAuthenticationError(err)` | Invalid, missing, or revoked key | `401`, `403` (both recorded as `401`) |
| `IsQuotaError(err)` | Any response the API answers with `402` | `402` |
| `IsRateLimitError(err)` | Rate limit exceeded | `429` |
| `errors.As(err, &apiErr)` | Any other 4xx or 5xx | the actual code |

Errors that never reach the network, such as an unsupported conversion pair, a `Distill` call with both `URLs` and `DiscoverFrom`, or a `PerceiveDirect` call asking for two artifacts, come back as plain `error` values from `errors.New` or `fmt.Errorf`, not `*APIError`. Response codes are catalogued in the [error codes reference](/docs/error-codes).

---

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

1. Before each single-file and single-URL conversion, the client generates a UUIDv4 and sends it as `job_id`.
2. If that request comes back `>= 500`, the client silently switches to `GET /v1/convert/status/{job_id}`, polling every 3 seconds.
3. On `success` it returns the result. On `failed` it returns `*APIError` with status `500` and the server's message.
4. The polling deadline is 5 minutes, after which you get an `*APIError` with status `504` and the message `Conversion timed out`.

`ConversionResult.JobID` is always populated, even when the sync path succeeded and the response omitted it, so you can hand it to `GetJobStatus` yourself:

```go
status, err := client.GetJobStatus(ctx, result.JobID)
switch status.Status {
case enconvert.JobStatusSuccess:
    fmt.Println(status.PresignedURL)
case enconvert.JobStatusFailed:
    log.Println(status.Error)
}
```

<div class="alert alert-info">
<strong>Website batches opt out on purpose.</strong> <code>ConvertWebsiteToPDF</code> and <code>ConvertWebsiteToScreenshot</code> 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. Polling runs inside the <code>context.Context</code> you pass, so cancelling the context aborts the wait at once.
</div>

---

## Configuration

```go
client, err := enconvert.New(os.Getenv("ENCONVERT_API_KEY"),
    enconvert.WithTimeout(300*time.Second),
    enconvert.WithBaseURL("https://api.enconvert.com"),
)
```

| Constructor input | Type | Default | Description |
|-------------------|------|---------|-------------|
| `apiKey` (first argument) | `string` | required | Private API key. `New` returns an error when it is empty. |
| `WithTimeout(d)` | `time.Duration` | `300 * time.Second` | Sets `Timeout` on the internal `*http.Client`, covering downloads too. |
| `WithBaseURL(u)` | `string` | `https://api.enconvert.com` | Override for a self-hosted gateway. Trailing slashes are stripped. |

Per-call deadlines layer on top of the client timeout through the context: wrap it with `context.WithTimeout(context.Background(), 30*time.Second)` and pass that as the first argument.

<div class="alert alert-warning">
<strong>Never hardcode the API key.</strong> Read it from an environment variable or your secret manager, and keep it on the server. Anyone holding your private key can run conversions against your project.
</div>

The client is safe to share across goroutines: it holds an `*http.Client` and no per-request mutable state. Build one at startup and reuse it. See [authentication](/docs/authentication) for key types and rotation.

---

## Result shape

Single-file and single-URL conversions return a `ConversionResult`:

```go
type ConversionResult struct {
    PresignedURL          string   // signed download URL for the output
    ObjectKey             string   // storage object key
    Filename              string   // server-side filename
    FileSize              *int64   // bytes, nil when the API omits it
    ConversionTimeSeconds *float64 // nil when the API omits it
    JobID                 string   // always set by the client
}
```

Presigned URLs are short-lived. Pass `SaveTo` to have the SDK stream the bytes to disk for you, or fetch the URL yourself and store the file in your own bucket if you need long-term access. The download deliberately does not carry your API key, since a presigned URL is self-authenticating and forwarding the key to a storage host would leak it.

V2 artifacts arrive as `V2OutputArtifact` values keyed by output name, each carrying `URL` (pre-signed for 15 minutes and re-signed on every status GET), `ObjectKey`, `SizeBytes`, `ContentType`, and `ExpiresIn` (900 seconds 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.

---

## Source and issues

- **Module and source:** [github.com/conversionapi/go-sdk](https://github.com/conversionapi/go-sdk)
- **Version:** exposed at runtime as the `enconvert.Version` constant
- **License:** MIT, no third-party dependencies

Related reading: [all SDKs](/docs/sdks), [V2 overview](/docs/v2-overview), [perceive](/docs/v2-perceive), [discover](/docs/v2-discover), [lookup](/docs/v2-lookup), [distill](/docs/v2-distill), [ingest](/docs/v2-ingest), [watch](/docs/v2-watch), [endpoints overview](/docs/endpoints-overview), [parameters and options](/docs/parameters-options), and your [dashboard](/dashboard) for keys.

---

## Frequently asked questions

### How do I convert files in Go?

Run `go get github.com/conversionapi/go-sdk`, build a client with `enconvert.New(os.Getenv("ENCONVERT_API_KEY"))`, 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 Go?

Call `client.ConvertURLToPDF(ctx, url, enconvert.URLToPDFOptions{SaveTo: "page.pdf"})`. Set `SinglePage: enconvert.Bool(false)` to paginate instead of producing one continuous page, and pass `PDFOptions` for page size, orientation, margins, scale, grayscale, headers, and footers.

### How do I convert DOCX to PDF in Go?

`client.ConvertDocument(ctx, enconvert.FilePath("report.docx"), enconvert.ConvertDocumentOptions{SaveTo: "report.pdf"})`. The output format defaults to `pdf`, so you can leave `OutputFormat` empty. 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 Go?

`client.ConvertImage(ctx, enconvert.FilePath("photo.heic"), enconvert.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 fail locally before any request is sent.

### Does the Go SDK pull in any third-party dependencies?

No. `go.mod` declares the module and a Go 1.21 floor and nothing else. The client is built on `net/http`, `encoding/json`, `mime/multipart`, and `crypto/rand` from the standard library, so it adds no transitive supply chain to your build.

### How do I scrape a web page into clean Markdown in Go?

Two options. `client.ConvertURLToMarkdown` returns GitHub-Flavored Markdown with YAML frontmatter and is the simplest path. `client.V2.Perceive` with `Outputs: []enconvert.PerceiveOutputName{enconvert.PerceiveOutputMarkdown}` gives you the same Markdown plus a `RenderQuality` score, `Deductions`, `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 `*float64` 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` map naming each penalty) before feeding the text to a model.

### How do I set a per-request timeout or cancel a conversion in Go?

Every method takes a `context.Context` first. Wrap it with `context.WithTimeout` or `context.WithCancel` for per-call control; `WithTimeout` on the constructor sets the floor-level HTTP client timeout for all calls, including the download of a `SaveTo` file.

### 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`, it polls `GET /v1/convert/status/{job_id}` every 3 seconds for up to 5 minutes, returning the result on `success` and an `*APIError` on `failed`. Exceeding the deadline yields an `*APIError` with status `504`. Website batch submissions deliberately skip this fallback.

### Can I ship the Go SDK inside a desktop or mobile client?

No. It authenticates with a private API key, and V2 endpoints reject public keys outright. Keep the client on a server you control and let your app talk to that. See [authentication](/docs/authentication) for the key model.

### Is the Go client safe to use from multiple goroutines?

Yes. `*enconvert.Client` wraps a single `*http.Client` and keeps no mutable per-request state, so build one at startup and share it everywhere.
