C# and .NET File Conversion SDK#
Enconvert is the official C# and .NET client for the EnConvert API, published on NuGet and targeting .NET 8 or newer. It converts files from C#: URL to PDF, DOCX to PDF, HEIC to WebP, JSON to YAML, any document to Markdown, and whole websites into a single ZIP. The same client carries a V2 namespace for web scraping and web intelligence, so one key covers perceive, discover, lookup, distill, ingest, and watch. Every call is async and cancellable, every response is a typed record, and the package pulls in no third-party dependencies.
Enconvert · Source: conversionapi/csharp-sdk · Runtime: .NET 8+ · Dependencies: none beyond the BCL
Install#
dotnet add package Enconvert
The assembly targets net8.0 with nullable reference types enabled and is built on System.Net.Http and System.Text.Json only.
Quick start#
using Enconvert;
using var client = new EnconvertClient(Environment.GetEnvironmentVariable("ENCONVERT_API_KEY")!);
// Convert a live page to PDF and stream it to disk.
var pdf = await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions { SaveTo = "page.pdf" });
Console.WriteLine(pdf.PresignedUrl);
// Read a page the way your agent should, with a quality score attached.
var op = await client.V2.PerceiveAsync("https://example.com", new PerceiveOptions { Outputs = new[] { "markdown", "structured" } });
Console.WriteLine($"{op.Outputs["markdown"].Url} scored {op.RenderQuality}");
EnconvertClient implements IDisposable, so declare it with using or register it as a singleton. It authenticates with a private API key, which means it belongs on the server: never ship the key inside a desktop, mobile, or Blazor WebAssembly build. Key setup is covered in Authentication.
What the client exposes#
| Member | Endpoint | Returns |
|---|---|---|
ConvertUrlToPdfAsync(url, opts?) |
POST /v1/convert/url-to-pdf |
ConversionResult |
ConvertUrlToScreenshotAsync(url, opts?) |
POST /v1/convert/url-to-screenshot |
ConversionResult |
ConvertUrlToMarkdownAsync(url, opts?) |
POST /v1/convert/url-to-markdown |
ConversionResult |
ConvertImageAsync(file, opts) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
ConvertDocumentAsync(file, opts?) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
ConvertToMarkdownAsync(file, opts?) |
POST /v1/convert/anything-to-markdown |
ConversionResult |
ConvertToPdfAsync(file, opts?) |
POST /v1/convert/anything-to-pdf |
ConversionResult |
ConvertWebsiteToPdfAsync(url, opts?) |
POST /v1/convert/website-to-pdf |
BatchSubmission |
ConvertWebsiteToScreenshotAsync(url, opts?) |
POST /v1/convert/website-to-screenshot |
BatchSubmission |
GetJobStatusAsync(jobId) |
GET /v1/convert/status/{jobId} |
JobStatus |
GetBatchStatusAsync(batchId) |
GET /v1/convert/batch/{batchId} |
BatchStatus |
WaitForBatchAsync(batchId, opts?) |
GET /v1/convert/batch/{batchId} (polled) |
BatchStatus |
V2 |
the /v2/* surface |
EnconvertV2 |
client.V2 groups 23 methods across six capabilities:
| Capability | Methods | Reference |
|---|---|---|
| Perceive | PerceiveAsync, GetPerceiveOperationAsync, PerceiveBatchAsync, GetPerceiveBatchAsync, PerceiveDirectAsync, DownloadPerceiveArtifactAsync |
/docs/v2-perceive |
| Discover | DiscoverAsync |
/docs/v2-discover |
| Lookup | LookupAsync |
/docs/v2-lookup |
| Distill | DistillAsync |
/docs/v2-distill |
| Ingest | IngestAsync, IngestFilesAsync, ListIngestJobsAsync, GetIngestJobAsync, CancelIngestJobAsync, RetryIngestWebhookAsync, GetWebhookSecretAsync, RotateWebhookSecretAsync |
/docs/v2-ingest |
| Watch | CreateWatcherAsync, ListWatchersAsync, GetWatcherAsync, GetWatcherSnapshotsAsync, UpdateWatcherAsync, DeleteWatcherAsync |
/docs/v2-watch |
Every method takes an optional trailing CancellationToken. Options are records with init properties, so build them with an object initializer and reuse them with with. Other language clients are in the SDK index; the raw REST surface is in Endpoints overview.
File conversion#
ConvertUrlToPdfAsync#
Render any reachable URL to PDF.
var result = await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions
{
SinglePage = false,
ViewportWidth = 1440,
PdfOptions = new PdfOptions { PageSize = "A4", Orientation = "landscape" },
SaveTo = "report.pdf",
});
Console.WriteLine($"{result.Filename} ({result.FileSize} bytes)");
| Option | Type | Default | Description |
|---|---|---|---|
SaveTo |
string? |
-- | Local path to stream the PDF to. Missing parent directories are created. |
SinglePage |
bool? |
true |
true produces one continuous page. false paginates using PdfOptions.PageSize. |
PdfOptions |
PdfOptions? |
-- | Page size, orientation, margins, scale, grayscale, header, footer. See PDF options. |
ViewportWidth, ViewportHeight |
int? |
1920, 1080 |
Browser viewport in pixels. |
LoadMedia, EnableScroll |
bool? |
true |
Wait for images and video before capture; scroll top to bottom so lazy loaders fire. |
OutputFilename |
string? |
auto | Override the generated filename. |
Auth |
HttpBasicAuth? |
-- | HTTP Basic credentials for a page behind a login. |
Cookies, Headers |
IReadOnlyList<BrowserCookie>?, IReadOnlyDictionary<string, string>? |
-- | Up to 50 cookies injected before rendering, and up to 20 extra request headers. Hop-by-hop headers are rejected. |
Auth with your own Authorization header. The API rejects that conflict rather than guessing which credential wins. Pick one.
ConvertUrlToScreenshotAsync#
Capture a PNG of any URL.
await client.ConvertUrlToScreenshotAsync("https://example.com", new UrlToScreenshotOptions
{
ViewportWidth = 1440,
SaveTo = "screenshot.png",
});
UrlToScreenshotOptions accepts the same viewport, media, scroll, filename, and access options as ConvertUrlToPdfAsync, minus SinglePage and PdfOptions.
ConvertUrlToMarkdownAsync#
Extract clean GitHub-Flavored Markdown from a URL. Navigation, footers, ads, and scripts are stripped, the main article body is kept, and YAML frontmatter (title, description, url, links, images) is prepended.
await client.ConvertUrlToMarkdownAsync("https://example.com/article", new UrlToMarkdownOptions { SaveTo = "article.md" });
UrlToMarkdownOptions shares the same render and access options. For agent-facing reads that also need a quality score and structured extraction, use Perceive instead.
ConvertImageAsync#
Convert between jpeg, png, svg, heic, and webp in any direction, and rasterize a PDF to JPEG.
// From a path.
await client.ConvertImageAsync("photo.heic", new ConvertImageOptions { OutputFormat = "webp", SaveTo = "photo.webp" });
// From bytes, with an explicit filename so the input format can be detected.
var bytes = await File.ReadAllBytesAsync("scan.pdf");
await client.ConvertImageAsync(new FileInput(bytes, "scan.pdf"), new ConvertImageOptions
{
OutputFormat = "jpeg",
SaveTo = "scan.jpeg",
});
Three overloads accept a path string, a raw byte[], or a FileInput record. The input format comes from the filename extension, so prefer FileInput when you hold bytes: the bare byte[] overload sends upload.bin, which only works on the auto-detecting endpoints.
| Option | Type | Required | Description |
|---|---|---|---|
OutputFormat |
string |
Yes | Target format. jpg, yml, htm, and md are normalized to their canonical names. |
SaveTo |
string? |
-- | Local path to stream the result to. |
OutputFilename |
string? |
-- | Override the generated filename. |
Unsupported pairs throw ArgumentException before any HTTP request is made, so a typo costs nothing.
ConvertDocumentAsync#
Convert documents and data formats. OutputFormat defaults to "pdf".
// docx to pdf
await client.ConvertDocumentAsync("report.docx", new ConvertDocumentOptions { SaveTo = "report.pdf" });
// json to yaml
await client.ConvertDocumentAsync("data.json", new ConvertDocumentOptions { OutputFormat = "yaml", SaveTo = "data.yaml" });
// markdown to pdf with page geometry
await client.ConvertDocumentAsync("README.md", new ConvertDocumentOptions
{
PdfOptions = new PdfOptions { PageSize = "A4", Margins = new PdfMargins { Top = 20, Bottom = 20 } },
SaveTo = "readme.pdf",
});
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 through ConvertToPdfAsync or ConvertToMarkdownAsync.
| Option | Type | Default | Description |
|---|---|---|---|
OutputFormat |
string? |
"pdf" |
Target format. |
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. |
Like ConvertImageAsync, this method has path, byte[], and FileInput overloads.
ConvertToMarkdownAsync#
Send almost any document to the auto-detecting Markdown endpoint: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats. Images are not supported here.
await client.ConvertToMarkdownAsync("handbook.docx", new ConvertToMarkdownOptions { SaveTo = "handbook.md" });
The format is detected server side, so there is no client-side extension check and no PDF options on this endpoint. ConvertToMarkdownOptions carries SaveTo and OutputFilename only. The output is a single heading-aware .md file, which makes it a solid first stage in a RAG pipeline: a semantic chunker can split on the document's own heading hierarchy instead of arbitrary character counts.
ConvertToPdfAsync#
The other auto-detecting endpoint: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, or an existing PDF passed through for normalization.
// Slides to PDF.
await client.ConvertToPdfAsync("slides.pptx", new ConvertToPdfOptions { SaveTo = "slides.pdf" });
// PDF passthrough, converted to grayscale.
await client.ConvertToPdfAsync("scan.pdf", new ConvertToPdfOptions
{
PdfOptions = new PdfOptions { Grayscale = true },
SaveTo = "gray.pdf",
});
Grayscale is honored here. The anything-to-pdf endpoint ignores the rest of PdfOptions. When you need page size, orientation, margins, or a header and footer, route HTML and Markdown through ConvertDocumentAsync, or render the page with ConvertUrlToPdfAsync.
ConvertToPdfOptions carries SaveTo, OutputFilename, and PdfOptions, and has the same three input overloads as the methods above.
Whole-site batches#
ConvertWebsiteToPdfAsync and ConvertWebsiteToScreenshotAsync discover every page of a site, convert each one in the background, and bundle the results into a single ZIP. Both are async only and require a private API key.
var batch = await client.ConvertWebsiteToPdfAsync("https://example.com", new WebsiteToPdfOptions
{
CrawlMode = "sitemap",
ExcludePatterns = new[] { "/blog/tag/" },
NotificationEmail = "[email protected]",
});
Console.WriteLine($"{batch.BatchId}: {batch.UrlCount} pages via {batch.DiscoveryMethod}");
// Block until the ZIP is ready, then save it.
var status = await client.WaitForBatchAsync(batch.BatchId, new WaitForBatchOptions { SaveTo = "site.zip" });
Console.WriteLine($"{status.Completed} of {status.Total} converted, {status.Failed} failed");
To poll on your own schedule instead, call GetBatchStatusAsync(batchId) and read ZipDownloadUrl once Status leaves "processing".
| Option | Type | Default | Description |
|---|---|---|---|
CrawlMode |
string? |
"auto" |
"auto", "sitemap" (sitemap.xml only), or "full" (sitemap plus a breadth-first crawl). |
IncludePatterns, ExcludePatterns |
IReadOnlyList<string>? |
-- | Only crawl, or skip, URLs matching these patterns. Full crawl mode. |
NotificationEmail, CallbackUrl |
string? |
project owner, -- | Address notified when the batch finishes, and a webhook POSTed on completion. |
SinglePage, PdfOptions |
bool?, PdfOptions? |
-- | PDF batches only. Viewport, media, scroll, and access options are shared with the single-URL methods. |
WaitForBatchOptions takes IntervalMs (default 5_000), TimeoutMs (default 1_800_000, so 30 minutes), and SaveTo. Blowing the deadline throws ApiException with status 504.
GetJobStatusAsync#
Poll a single conversion job by id.
var status = await client.GetJobStatusAsync("job_abc123");
if (status.Status == "success") Console.WriteLine(status.PresignedUrl);
else if (status.Status == "failed") Console.Error.WriteLine(status.Error);
Supported conversion pairs#
The SDK mirrors the gateway's converter map and implements 43 typed {input}-to-{output} 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 other, all 20 pairs |
pdf |
jpeg |
The static Formats class exposes the same table, so you can validate before building a UI or a job queue:
Formats.ValidOutputsFor("json"); // ["csv", "toml", "xml", "yaml"]
Formats.ValidOutputsFor("pdf"); // ["jpeg"]
Formats.ImplementedConversions; // the full set of 43 endpoint names
Formats.NormalizeOutputFormat(".JPG"); // "jpeg"
Anything outside this table goes through ConvertToPdfAsync or ConvertToMarkdownAsync, which detect the format server side. The full parameter reference lives in Parameters and options.
Web intelligence (V2)#
Every V2 read carries RenderQuality, a score from 0.0 to 1.0 that says how honestly the page rendered. A bot challenge, cookie or login wall, HTTP error, or empty single-page-app shell comes back with a low score, a populated Deductions map naming which checks fired, and a Warnings list, while the content is still returned. That is the point: a bad read is flagged rather than quietly entering your agent's context. The same score appears on perceive results, distill items, auto-perceived lookup hits, and watcher snapshots. Concepts are covered in the V2 overview. V2 endpoints require a private API key; public keys are rejected.
Perceive#
Render one URL into agent-ready artifacts.
var op = await client.V2.PerceiveAsync("https://example.com", new PerceiveOptions
{
Outputs = new[] { "markdown", "screenshot", "structured" },
Extract = new[] { "tables", "metadata" },
OnlyMainContent = true,
});
Console.WriteLine(op.RenderQuality); // 0.0 to 1.0
Console.WriteLine(op.StatusCode); // upstream HTTP status
Console.WriteLine(op.Outputs["markdown"].Url); // signed for 15 minutes
Console.WriteLine(op.Structured); // JsonObject, shape is yours
foreach (var (check, penalty) in op.Deductions) Console.WriteLine($"deduction {check}: {penalty}");
| Option | Type | Default | Description |
|---|---|---|---|
Outputs |
IReadOnlyList<string>? |
["markdown", "structured"] |
Any of markdown, html_cleaned, html_raw, screenshot, screenshot_full_page, pdf, links, images, structured. |
Extract |
IReadOnlyList<string>? |
-- | Heuristic targets: tables, prices, contacts, metadata, main_content, headings, structured_data, technologies, all. |
Schema |
JsonObject? |
-- | JSON schema driving LLM-tier structured extraction. |
WaitFor, WaitTimeoutMs |
string?, int? |
--, 30000 |
A CSS selector (optionally css:...) or js:<expr> to await before capture, bounded by 0 to 60000 ms. |
JsCode |
string? |
-- | JavaScript run after navigation, max 20000 characters. |
Viewport |
PerceiveViewport? |
1920 x 1080 | Width 320 to 3840, Height 240 to 2160. |
Headers, Cookies, Auth |
see above | -- | Extra headers, injected cookies, HTTP Basic credentials. |
CacheMode |
string? |
"enabled" |
"enabled" reuses a 1 hour cache, "bypass" skips it, "refresh" re-renders. |
PdfOptions |
PdfOptions? |
-- | Only meaningful when Outputs contains pdf. |
BlockResources |
IReadOnlyList<string>? |
-- | Resource types to skip: image, media, font, stylesheet, script. |
RespectRobots, Mobile |
bool? |
-- | 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. |
DirectDownload |
bool? |
-- | Stream artifact bytes instead of the JSON envelope. Prefer PerceiveDirectAsync. |
ProxyUrl, Geolocation, and ActionChain exist on PerceiveOptions but are not yet available server side and currently come back as 422.
Re-sign artifact URLs later, batch up to 1000 URLs, or stream bytes without the signed-URL round trip:
// Artifact URLs are re-signed on every fetch of the operation.
var again = await client.V2.GetPerceiveOperationAsync(op.OperationId);
// Batches: small ones run inline, larger ones return "queued" so you poll.
var batch = await client.V2.PerceiveBatchAsync(
new[] { "https://a.example.com", "https://b.example.com" },
new PerceiveBatchOptions { Outputs = new[] { "markdown" }, OutputMode = "zip" });
var done = await client.V2.GetPerceiveBatchAsync(batch.JobId);
Console.WriteLine($"{done.Completed}/{done.Total} done, {done.Failed} failed, zip {done.Zip?.Url}");
// Direct download: exactly one artifact-producing output required.
var direct = await client.V2.PerceiveDirectAsync("https://example.com", new PerceiveOptions { Outputs = new[] { "pdf" } });
await File.WriteAllBytesAsync(direct.Filename ?? "page.pdf", direct.Content);
// Re-download a stored artifact from an earlier operation.
var artifact = await client.V2.DownloadPerceiveArtifactAsync(op.OperationId, "markdown");
PerceiveDirectAsync throws ArgumentException locally unless exactly one of markdown, html_cleaned, html_raw, screenshot, screenshot_full_page, pdf, links, or images is requested, since structured is inline JSON rather than a stored file. Both direct methods return a PerceiveDirectResult carrying Content, ContentType, Filename, OperationId, ObjectKey, CacheHit, RenderQuality, SourceStatusCode, ContentHash, and WarningsCount. DownloadPerceiveArtifactAsync takes the output name only when the operation produced more than one artifact, and returns 410 once the artifact passes its retention window.
Discover#
Enumerate a site's URLs with no browser rendering, which makes it far cheaper than a crawl.
var found = await client.V2.DiscoverAsync("https://example.com", new DiscoverOptions
{
Mode = "hybrid",
MaxUrls = 200,
ExcludePatterns = new[] { "/tag/" },
});
Console.WriteLine($"{found.Total} urls, truncated: {found.Truncated}");
foreach (var url in found.Urls) Console.WriteLine(url);
| Option | Type | Default | Description |
|---|---|---|---|
Mode |
string? |
"hybrid" |
"sitemap", "crawl", or "hybrid" (sitemap plus HTTP crawl). |
MaxUrls |
int? |
100 |
1 to 1000. |
MaxDepth |
int? |
2 |
Crawl depth, 1 to 5. |
IncludePatterns, ExcludePatterns |
IReadOnlyList<string>? |
-- | Regex allowlist and denylist, max 50 entries each. The denylist is applied second. |
SameDomainOnly, RespectRobots |
bool? |
true, -- |
Stay on the seed domain; honor robots.txt during discovery. |
DiscoverResult reports Total, Urls, PagesCrawled, Truncated, RobotsRespected, a per-source count map in Sources, and Warnings.
Lookup#
Run a categorized web search, and optionally render the top results in the same call.
var search = await client.V2.LookupAsync("best static site generators", new LookupOptions
{
Category = "web",
NumResults = 10,
TimeFilter = "month",
PerceiveTop = 3,
});
foreach (var hit in search.Results)
{
Console.WriteLine($"{hit.Position}. {hit.Title} {hit.Url}");
if (hit.Perceive is { } page)
Console.WriteLine($" quality {page.RenderQuality}, markdown {page.Outputs["markdown"].Url}");
}
| Option | Type | Default | Description |
|---|---|---|---|
Category |
string? |
"web" |
"web", "news", "images", "scholar", "patents", "maps". |
Country, Locale |
string? |
-- | Google gl country code ("us", "in") and hl interface language ("en"). |
TimeFilter |
string? |
-- | "hour", "day", "week", "month", "year". |
NumResults, Page |
int? |
10, 1 |
1 to 100 results; page 1 to 10. |
Location, Autocorrect |
string?, bool? |
--, true |
Free-text location such as "Austin, Texas"; let the provider fix obvious typos. |
PerceiveTop |
int? |
0 |
0 to 10. Auto-perceive the top N result URLs with a full browser render. |
LookupResult also carries AnswerBox, KnowledgeGraph, PerceiveOperationIds, and Warnings.
Distill#
Schema-driven structured extraction. A free CSS pass runs first when you supply one, and only the fields it misses escalate to the LLM tier.
using System.Text.Json.Nodes;
var extraction = await client.V2.DistillAsync(new DistillOptions
{
Urls = new[] { "https://example.com/pricing" },
Schema = new JsonObject { ["plans"] = "list of plan names with monthly prices" },
CssSchema = new CssSchema
{
BaseSelector = ".plan-card",
Fields = new[]
{
new CssField { Name = "name", Type = "text", Selector = "h3" },
new CssField { Name = "price", Type = "text", Selector = ".price" },
},
},
});
var first = extraction.Results[0];
Console.WriteLine($"{first.Data} via {first.ExtractionTier}");
Console.WriteLine($"css fields {first.FieldsFromCss}, llm fields {first.FieldsFromLlm}");
// Or discover a site first, then distill every page it finds.
await client.V2.DistillAsync(new DistillOptions
{
DiscoverFrom = new DistillDiscoverFrom { Url = "https://example.com", Mode = "sitemap", MaxPages = 10 },
Schema = new JsonObject { ["title"] = "page title" },
});
| Option | Type | Default | Description |
|---|---|---|---|
Schema |
JsonObject |
required | A JSON-Schema object or a flat {field: description} map. The response Data matches this shape. |
Urls |
IReadOnlyList<string>? |
-- | Explicit URLs, max 50. Exactly one of Urls or DiscoverFrom. |
DiscoverFrom |
DistillDiscoverFrom? |
-- | Url, Mode ("sitemap", "crawl", "hybrid"), and MaxPages 1 to 50, default 10. |
CssSchema |
CssSchema? |
-- | BaseSelector plus Fields, with optional Name and TargetField. |
WaitFor, WaitTimeoutMs |
string?, int? |
--, 30000 |
Wait condition before extraction. |
Headers, Cookies, RespectRobots |
-- | -- | Same shapes as the render options above. |
CssField.Type is one of text, attribute, html, regex, nested, list, or nested_list, nesting up to five levels deep. Passing both Urls and DiscoverFrom, or neither, throws ArgumentException before any request goes out.
Ingest#
Turn a whole site, or a stack of uploaded documents, into chunked JSONL that a vector store can read. Ingest is always asynchronous.
// From a site.
var job = await client.V2.IngestAsync(new IngestOptions
{
Mode = "sitemap",
Url = "https://docs.example.com",
MaxPages = 100,
Chunk = new IngestChunkOptions { MaxWords = 512 },
WebhookUrl = "https://my.app/hooks/enconvert",
});
// From uploaded files: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, legacy and ODF office.
var fileJob = await client.V2.IngestFilesAsync(
new[] { new FileInput(await File.ReadAllBytesAsync("handbook.pdf"), "handbook.pdf") },
new IngestFilesOptions { Chunk = new IngestChunkOptions { MaxWords = 512 } });
// Poll until the JSONL is ready.
var status = await client.V2.GetIngestJobAsync(job.JobId);
Console.WriteLine($"{status.Status}: {status.PagesProcessed}/{status.PagesDiscovered} pages, {status.TotalChunks} chunks");
if (status.Status == "completed") Console.WriteLine(status.OutputUrl);
var recent = await client.V2.ListIngestJobsAsync(new V2ListOptions { Limit = 20 });
await client.V2.CancelIngestJobAsync(job.JobId); // idempotent
| Option | Type | Default | Description |
|---|---|---|---|
Mode |
string? |
"urls" |
"urls", "sitemap", or "crawl". |
Url |
string? |
-- | Seed URL. Required for sitemap and crawl, rejected for urls. |
Urls |
IReadOnlyList<string>? |
-- | Explicit URLs, max 1000. Required for urls, rejected otherwise. |
MaxPages, MaxDepth, SameDomainOnly |
int?, int?, bool? |
50, 2, true |
Discovery cap 1 to 1000 for sitemap and crawl, crawl depth 1 to 5, stay on the seed domain. |
IncludePatterns, ExcludePatterns |
IReadOnlyList<string>? |
-- | Regex allowlist and denylist. |
RespectRobots, WaitFor, WaitTimeoutMs |
-- | -- | Per-page render controls. |
Chunk |
IngestChunkOptions? |
-- | MaxWords 32 to 4000, default 512. SentenceOverlap 0 to 10, default 1. |
WebhookUrl |
string? |
-- | Completion webhook, HMAC signed. |
Mode rules are enforced client side, so a urls job that also sets Url throws ArgumentException immediately. A job moves through queued, discovering, processing, and then completed, failed, or canceled. Webhook delivery is verifiable end to end:
var secret = await client.V2.GetWebhookSecretAsync();
Console.WriteLine($"{secret.SignatureHeader} using {secret.SignatureScheme}, replay tolerance {secret.ReplayToleranceSeconds}s");
await client.V2.RotateWebhookSecretAsync(); // old signatures stop verifying
var retry = await client.V2.RetryIngestWebhookAsync(job.JobId);
Console.WriteLine($"delivered: {retry.Delivered} after {retry.Attempts} attempts");
Watch#
Re-render a page on a schedule and get notified when it changes.
var watcher = await client.V2.CreateWatcherAsync("https://example.com/pricing", new WatchCreateOptions
{
FrequencyMinutes = 60,
DiffMode = "auto",
WebhookUrl = "https://my.app/hooks/changes",
});
var page = await client.V2.ListWatchersAsync(new V2ListOptions { Limit = 20 });
var one = await client.V2.GetWatcherAsync(watcher.WatcherId);
var history = await client.V2.GetWatcherSnapshotsAsync(watcher.WatcherId, new SnapshotListOptions { Limit = 10 });
foreach (var snap in history.Snapshots)
Console.WriteLine($"{snap.CheckedAt} changed: {snap.HasChanges} similarity: {snap.Similarity}");
await client.V2.UpdateWatcherAsync(watcher.WatcherId, new WatcherUpdate { Status = "paused" });
await client.V2.UpdateWatcherAsync(watcher.WatcherId, new WatcherUpdate { WebhookUrl = "" }); // clears it
await client.V2.DeleteWatcherAsync(watcher.WatcherId); // soft delete, idempotent
| Option | Type | Default | Description |
|---|---|---|---|
FrequencyMinutes |
int? |
60 |
60 to 43200. The hourly floor is hard. |
DiffMode |
string? |
"auto" |
"auto", "text", "structured", "tables", "metadata". |
TrackFields |
JsonObject? |
-- | Field or selector subset for the diff engine. |
WebhookUrl |
string? |
-- | Change webhook, HMAC signed. Set to "" in an update to clear it. |
NotifyEmail |
bool? |
true |
Email the project owner on changes. |
WatcherUpdate also accepts Status ("active" or "paused") and throws ArgumentException if you pass an update with no fields set.
WatcherSnapshot.Changes is raw JSON lifted from the watched page. Escape it before rendering it in a dashboard or an email.
PDF options#
PdfOptions is shared by ConvertUrlToPdfAsync, ConvertDocumentAsync, ConvertToPdfAsync (grayscale only), and PerceiveOptions when pdf is among the outputs.
await client.ConvertUrlToPdfAsync("https://internal.example.com/report", new UrlToPdfOptions
{
PdfOptions = new PdfOptions
{
PageSize = "A4",
Orientation = "landscape",
Margins = new PdfMargins { Top = 10, Bottom = 10, Left = 15, Right = 15 },
Scale = 0.9,
Header = new PdfHeaderFooter { Content = "Quarterly Report", Height = 15 },
Footer = new PdfHeaderFooter { Content = "Confidential", Height = 12 },
},
Auth = new HttpBasicAuth { Username = "user", Password = "pass" },
SaveTo = "report.pdf",
});
| Field | Type | Description |
|---|---|---|
PageSize |
string? |
"A4", "A3", "Letter", "Legal", and friends. |
PageWidth, PageHeight |
double? |
Custom dimensions. Set together, they override PageSize. |
Orientation |
string? |
"portrait" or "landscape". |
Margins |
PdfMargins? |
Top, Bottom, Left, Right, all optional. |
Scale |
double? |
Render scale, for example 0.9 for 90 percent. |
Grayscale |
bool? |
Post-process the PDF to grayscale. |
Header, Footer |
PdfHeaderFooter? |
Content up to 2000 characters, plus Height. |
Error handling#
Every failure surfaces as a typed exception, so catch blocks read from most specific to least.
using Enconvert;
try
{
await client.V2.PerceiveAsync("https://example.com");
}
catch (AuthenticationException) { Console.Error.WriteLine("Invalid or missing API key."); }
catch (QuotaException) { Console.Error.WriteLine("Request rejected with 402."); }
catch (RateLimitException) { Console.Error.WriteLine("Too many requests, back off and retry."); }
catch (ApiException e) { Console.Error.WriteLine($"API error [{e.StatusCode}]: {e.Message}"); }
| Class | Raised on | Status code |
|---|---|---|
AuthenticationException |
Invalid, missing, or revoked key | 401, 403 (reported as 401) |
QuotaException |
Raised on HTTP 402 | 402 |
RateLimitException |
Rate limit exceeded | 429 |
ApiException |
Any other 4xx or 5xx | the actual code |
EnconvertException |
Base class for all of the above | -- |
The hierarchy runs EnconvertException to ApiException (which carries StatusCode) to the three specific classes, so a single catch (EnconvertException) catches everything the SDK raises. Messages are lifted from the response body's detail or error field when present. Validation the SDK performs locally, such as an unsupported conversion pair or a malformed distill request, throws ArgumentException instead and never reaches the network. Response codes are catalogued in Error codes.
Timeout recovery#
Long URL renders and large document conversions can outlive a 60 to 120 second reverse-proxy timeout even when the conversion itself succeeds. The client absorbs that for you:
- Before each single-file or single-URL conversion, the SDK generates a job id and sends it with the request.
- If that request comes back 5xx, the SDK switches to polling
GET /v1/convert/status/{jobId}every 3 seconds instead of failing. - A job recorded as
successreturns the normalConversionResult. A job recorded asfailedthrowsApiExceptionwith status500and the server's error message. - The polling deadline is 5 minutes, after which the SDK throws
ApiException(504, "Conversion timed out").
This covers ConvertUrlToPdfAsync, ConvertUrlToScreenshotAsync, ConvertUrlToMarkdownAsync, and all four file-upload methods. Website batch submissions deliberately opt out, because a failed submission has no job row to poll and must surface immediately. When a response omits job_id, the SDK backfills the id it generated, so ConversionResult.JobId is always usable with GetJobStatusAsync.
Configuration#
using var client = new EnconvertClient(
apiKey: Environment.GetEnvironmentVariable("ENCONVERT_API_KEY")!,
baseUrl: null, // defaults to https://api.enconvert.com
timeoutMs: 300_000);
| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
required | Private API key. An empty value throws ArgumentException. |
baseUrl |
string? |
https://api.enconvert.com |
Override the API base URL. Trailing slashes are stripped. |
timeoutMs |
int |
300_000 |
Request timeout in milliseconds, 5 minutes by default. |
The key travels in an X-API-Key header on every request. Artifact downloads go straight to signed storage URLs on a second HttpClient that sends no key and applies no timeout, so a large ZIP can stream for as long as it needs. Dispose the client once at the end of your process, or register it as a singleton rather than constructing one per request.
Result shape#
Every conversion method returns a ConversionResult:
public sealed record ConversionResult
{
public required string PresignedUrl { get; init; } // signed download URL
public required string ObjectKey { get; init; } // storage object key
public required string Filename { get; init; } // server-side filename
public int? FileSize { get; init; } // bytes
public double? ConversionTimeSeconds { get; init; }
public string? JobId { get; init; } // usable with GetJobStatusAsync
}
Async work returns its own records: JobStatus (Status, PresignedUrl, ObjectKey, Error), BatchSubmission (BatchId, Status, UrlCount, TotalDiscovered, DiscoveryMethod, OutputFormat), and BatchStatus (aggregate counts, OutputMode, ZipDownloadUrl, and per-URL Items).
V2 artifacts arrive as V2OutputArtifact values keyed by output name, each with Url (signed for 15 minutes), ObjectKey, SizeBytes, ContentType, and ExpiresIn (900 seconds by default). Signed URLs expire, so for permanent access download the bytes (pass SaveTo, use PerceiveDirectAsync, or fetch the URL yourself) and store them in your own bucket. Calling GetPerceiveOperationAsync again re-signs the artifact URLs of an operation still inside its retention window.
Source and issues#
- NuGet:
Enconvert - GitHub: conversionapi/csharp-sdk
- License: MIT
- Other languages: SDK index · API keys: dashboard · pricing
Frequently asked questions#
How do I convert files in C# with a NuGet package?#
Run dotnet add package Enconvert, create a client with your key (new EnconvertClient(apiKey)), and call an async method such as ConvertUrlToPdfAsync, ConvertImageAsync, or ConvertDocumentAsync. Pass SaveTo on the options record to stream the output straight to a local path, or read result.PresignedUrl to download it yourself.
How do I convert a URL to PDF in .NET?#
Call await client.ConvertUrlToPdfAsync("https://example.com", new UrlToPdfOptions { SaveTo = "page.pdf" }). Set SinglePage = false to paginate, and pass a PdfOptions record for page size, orientation, margins, scale, grayscale, header, and footer. The viewport defaults to 1920 x 1080.
How do I convert DOCX to PDF in C#?#
Call ConvertDocumentAsync("report.docx", new ConvertDocumentOptions { SaveTo = "report.pdf" }). PDF is the default output format, so OutputFormat is optional here. The same method handles XLSX, PPTX, ODF, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML, and TOML input.
How do I scrape a web page with the C# SDK?#
Use the V2 namespace: await client.V2.PerceiveAsync(url, new PerceiveOptions { Outputs = new[] { "markdown", "structured" } }). You get Markdown, cleaned or raw HTML, screenshots, PDF, links, images, and structured extraction, each as a signed artifact, plus a RenderQuality score for the read. To enumerate a site's URLs first without rendering anything, call DiscoverAsync.
What does render quality mean and why is it on every read?#
RenderQuality is a 0.0 to 1.0 honesty score attached to every V2 read. A low score means the page did not render cleanly: a bot challenge, a cookie or login wall, an HTTP error, or an empty single-page-app shell. The content still comes back, along with a Deductions map naming which checks fired and a Warnings list, so your agent can reject a bad read instead of treating it as fact.
Does the SDK handle conversions that outlive the proxy timeout?#
Yes. It sends a generated job id with each single-file or single-URL conversion, and if the request returns 5xx it polls GET /v1/convert/status/{jobId} every 3 seconds for up to 5 minutes. Success returns the normal result, a recorded failure throws ApiException, and hitting the deadline throws ApiException(504, "Conversion timed out"). Website batch submissions are excluded on purpose.
Can I use this SDK from Blazor WebAssembly or a mobile app?#
No. The client authenticates with a private API key that must never ship in code a user can read. Run it from ASP.NET Core, a worker service, an Azure Function, or any other server-side .NET 8 host, and have your front end call your own endpoint instead.
Which image conversions are supported?#
Every pair among jpeg, png, svg, heic, and webp, which is 20 combinations, plus pdf to jpeg rasterization. The input format comes from the filename extension, and unsupported pairs throw ArgumentException before any network call. Check the table yourself with Formats.ValidOutputsFor("heic").
How do I turn a documentation site into RAG-ready chunks?#
Call client.V2.IngestAsync(new IngestOptions { Mode = "sitemap", Url = "https://docs.example.com", MaxPages = 100 }), then poll GetIngestJobAsync until Status is "completed" and read OutputUrl for the JSONL. For local documents, IngestFilesAsync runs uploads through the same chunker. Tune Chunk.MaxWords and Chunk.SentenceOverlap to match your embedding model.
How long are the download URLs valid?#
V2 artifact URLs are signed for 15 minutes (ExpiresIn is 900 seconds) and are re-signed every time you fetch the operation with GetPerceiveOperationAsync. Conversion results return a presigned URL too. Either way, if you need the file to outlive the signature, download it and store it in your own bucket.