Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

enconvert (Rust)

Honest eyes for your AI agent — the Rust SDK for Enconvert. Blocking client built on reqwest's blocking API — no async runtime required.

Read any web page or file into clean Markdown, JSON, or screenshots, and get a render_quality score (0.0–1.0) on every read — so a blocked, challenge, or empty-SPA page comes back flagged with a low score and warnings, never mistaken for real content. Perceive, discover, look up, distill, ingest, and watch the web; convert 43 file and document formats through the same key.

Wiring an agent (Claude, Cursor, Windsurf, n8n, …)? The MCP server is the native path — npx @enconvert/mcp setup. This SDK is the programmatic REST path for everything else.

Install

cargo add enconvert

or add it to Cargo.toml:

[dependencies]
enconvert = "0.0.1"

Quick Start

use enconvert::{Enconvert, PerceiveOptions, PerceiveOutputName};

let client = Enconvert::new("sk_...")?;

// Read a page the way your agent should — with a quality score attached.
let op = client.v2().perceive("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
    ..Default::default()
})?;
println!("{:?}", op.render_quality); // e.g. Some(0.93)

V2 — agent-ready data (client.v2())

The V2 namespace turns web pages into agent-ready data: render, search, extract, ingest, and monitor. All V2 endpoints require a private API key and are plan-gated — a disabled feature or exhausted monthly quota returns Error::Quota (HTTP 402).

Every render carries render_quality (0.0–1.0). A low score means the page didn't render cleanly (challenge page, cookie wall, empty shell); the content is still returned, flagged, so a bad read never quietly enters your agent's context.

Perceive — render a URL into artifacts

use enconvert::{PerceiveExtractName, PerceiveOptions, PerceiveOutputName};

let op = client.v2().perceive("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Screenshot, PerceiveOutputName::Structured]),
    extract: Some(vec![PerceiveExtractName::Tables, PerceiveExtractName::Metadata]),
    ..Default::default()
})?;
println!("{:?}", op.render_quality);   // honesty score, 0.0-1.0
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref())); // 15-min signed URL
println!("{:?}", op.structured);

// Re-sign artifact URLs later:
let again = client.v2().get_perceive_operation(&op.operation_id)?;

// Batch (<=1000 URLs; small batches run inline, larger return "queued" — poll):
use enconvert::{PerceiveBatchOptions, PerceiveBatchOutputMode};

let batch = client.v2().perceive_batch(
    vec!["https://a.com".to_string(), "https://b.com".to_string()],
    PerceiveBatchOptions {
        render: PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() },
        output_mode: Some(PerceiveBatchOutputMode::Zip),
    },
)?;
let done = client.v2().get_perceive_batch(&batch.job_id)?;

// Direct download — stream one artifact's raw bytes instead of the JSON envelope
// (exactly one artifact-producing output; metadata arrives via headers):
let direct = client.v2().perceive_direct("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Pdf]),
    ..Default::default()
})?;
std::fs::write(direct.filename.as_deref().unwrap_or("page.pdf"), &direct.content)?;

// Re-download a stored artifact later (`None` when the operation has only one):
let saved = client.v2().download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;

Discover — enumerate a site's URLs (no rendering)

use enconvert::{DiscoverMode, DiscoverOptions};

let found = client.v2().discover("https://example.com", DiscoverOptions {
    mode: Some(DiscoverMode::Hybrid), // Sitemap | Crawl | Hybrid
    max_urls: Some(200),
    exclude_patterns: Some(vec!["/tag/".to_string()]),
    ..Default::default()
})?;
println!("{} {:?}", found.total, found.urls);

Lookup — web search with optional auto-perceive

use enconvert::{LookupCategory, LookupOptions};

let search = client.v2().lookup("best static site generators", LookupOptions {
    category: Some(LookupCategory::Web), // Web | News | Images | Scholar | Patents | Maps
    num_results: Some(10),
    perceive_top: Some(3), // auto-render top 3 results (uses perceive quota)
    ..Default::default()
})?;
for hit in &search.results {
    println!("{:?} {:?} {:?}", hit.title, hit.url, hit.perceive.as_ref().map(|p| p.render_quality));
}

Distill — schema-driven structured extraction

use enconvert::{CssField, CssFieldType, CssSchema, DistillOptions};
use serde_json::json;

// A small helper since CssField has no Default (field_type is required,
// so it deliberately has no silent default):
fn text_field(name: &str, selector: &str) -> CssField {
    CssField {
        name: name.to_string(),
        field_type: CssFieldType::Text,
        selector: Some(selector.to_string()),
        attribute: None,
        pattern: None,
        default: None,
        transform: None,
        fields: None,
    }
}

let extraction = client.v2().distill(DistillOptions {
    urls: Some(vec!["https://example.com/pricing".to_string()]),
    schema: json!({ "plans": "list of plan names with monthly prices" }).as_object().unwrap().clone(),
    css_schema: Some(CssSchema { // optional free CSS pass before the LLM tier
        base_selector: ".plan-card".to_string(),
        fields: vec![text_field("name", "h3"), text_field("price", ".price")],
        name: None,
        target_field: None,
    }),
    ..Default::default()
})?;
println!("{:?} {:?}", extraction.results[0].data, extraction.results[0].extraction_tier);

// Or discover-then-distill:
use enconvert::DistillDiscoverFrom;

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom::new("https://example.com")),
    schema: json!({ "title": "page title", "summary": "one-line summary" }).as_object().unwrap().clone(),
    ..Default::default()
})?;

Ingest — site or files to RAG-ready JSONL (always async)

Turn a whole site — or a set of uploaded documents — into chunked, RAG-ready JSONL through one pipeline.

use enconvert::{IngestChunkOptions, IngestFilesOptions, IngestMode, IngestOptions};

// From a site:
let job = client.v2().ingest(IngestOptions {
    mode: Some(IngestMode::Sitemap),
    url: Some("https://docs.example.com".to_string()),
    max_pages: Some(100),
    chunk: Some(IngestChunkOptions { max_words: Some(512), sentence_overlap: Some(1) }),
    webhook_url: Some("https://my.app/hooks/enconvert".to_string()),
    ..Default::default()
})?;

// Or from uploaded files (PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD, legacy/ODF office):
let file_job = client.v2().ingest_files(
    vec!["handbook.pdf".into(), "notes.docx".into()],
    IngestFilesOptions {
        chunk: Some(IngestChunkOptions { max_words: Some(512), sentence_overlap: Some(1) }),
        ..Default::default()
    },
)?;

let status = client.v2().get_ingest_job(&job.job_id)?; // poll
if status.status == enconvert::IngestStatus::Completed {
    println!("{:?}", status.output_url); // JSONL
}

client.v2().list_ingest_jobs(Default::default())?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotent

// Webhook signing (HMAC):
let secret = client.v2().get_webhook_secret()?;
println!("{} {}", secret.secret, secret.signature_header);
client.v2().rotate_webhook_secret()?;       // invalidates old secret
client.v2().retry_ingest_webhook(&job.job_id)?; // re-deliver

Watch — recurring change monitoring

use enconvert::{WatchCreateOptions, WatchDiffMode, WatchUpdateStatus, WatcherUpdate};

let watcher = client.v2().create_watcher("https://example.com/pricing", WatchCreateOptions {
    frequency_minutes: Some(60), // hourly floor
    diff_mode: Some(WatchDiffMode::Auto), // Auto | Text | Structured | Tables | Metadata
    webhook_url: Some("https://my.app/hooks/changes".to_string()),
    notify_email: Some(true),
    ..Default::default()
})?;

client.v2().list_watchers(Default::default())?;
client.v2().get_watcher(&watcher.watcher_id)?;
client.v2().get_watcher_snapshots(&watcher.watcher_id, enconvert::SnapshotListOptions { limit: Some(10) })?;
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate { status: Some(WatchUpdateStatus::Paused), ..Default::default() })?;
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate { webhook_url: Some(String::new()), ..Default::default() })?; // clears webhook
client.v2().delete_watcher(&watcher.watcher_id)?; // soft-delete, idempotent

V2 error handling

use enconvert::IngestOptions;

match client.v2().ingest(IngestOptions {
    urls: Some(vec!["https://example.com".to_string()]),
    ..Default::default()
}) {
    Err(e) if e.is_quota() => eprintln!("Upgrade plan or wait for quota reset"),
    Err(e) => return Err(e.into()),
    Ok(_) => {}
}

File conversion

The same key also converts 43 file and document formats. Two "anything → X" endpoints auto-detect the input; the format-specific methods below give you a validated, typed path.

Anything to Markdown / PDF

use enconvert::{ConvertToMarkdownOptions, ConvertToPdfOptions, PdfOptions};

// Any document → clean Markdown (a RAG-ingestion building block):
client.convert_to_markdown("report.docx", ConvertToMarkdownOptions {
    save_to: Some("report.md".into()),
    ..Default::default()
})?;
// PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT/MD, and legacy/ODF office. (Images not supported.)

// Almost anything → PDF:
client.convert_to_pdf("slides.pptx", ConvertToPdfOptions {
    save_to: Some("slides.pdf".into()),
    ..Default::default()
})?;
// office/ODF/Pages/Numbers/RTF/CSV, HTML, Markdown, text, images, SVG, EPUB, or a PDF passthrough.
// Only pdf_options.grayscale is honored on this endpoint:
client.convert_to_pdf("scan.pdf", ConvertToPdfOptions {
    pdf_options: Some(PdfOptions { grayscale: Some(true), ..Default::default() }),
    save_to: Some("gray.pdf".into()),
    ..Default::default()
})?;

Image Conversion

use enconvert::ConvertImageOptions;

let result = client.convert_image(
    "photo.heic",
    ConvertImageOptions {
        output_format: "webp".to_string(),
        save_to: Some("photo.webp".into()),
        ..Default::default()
    },
)?;

Any pair among jpeg, png, svg, heic, webp — plus PDF rasterization (pdfjpeg). Unsupported pairs error before any request is made:

use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};

valid_outputs_for("json"); // ["csv", "toml", "xml", "yaml"]
valid_outputs_for("pdf");  // ["jpeg"]

Document Conversion

use enconvert::ConvertDocumentOptions;

client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

client.convert_document("data.json", ConvertDocumentOptions {
    output_format: Some("yaml".to_string()),
    save_to: Some("data.yaml".into()),
    ..Default::default()
})?;

client.convert_document("notes.md", ConvertDocumentOptions {
    output_format: Some("html".to_string()),
    save_to: Some("notes.html".into()),
    ..Default::default()
})?;

Supported inputs: doc/docx, xls/xlsx, ppt/pptx, odt, ods, odp, ots, pages, numbers, html, markdown, csv, json, xml, yaml, toml. (EPUB → use convert_to_pdf / convert_to_markdown.)

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

URL to PDF / Screenshot / Markdown

use enconvert::{UrlToMarkdownOptions, UrlToPdfOptions, UrlToScreenshotOptions};

client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    save_to: Some("page.pdf".into()),
    ..Default::default()
})?;
client.convert_url_to_screenshot("https://example.com", UrlToScreenshotOptions {
    save_to: Some("shot.png".into()),
    ..Default::default()
})?;
client.convert_url_to_markdown("https://example.com/article", UrlToMarkdownOptions {
    save_to: Some("article.md".into()),
    ..Default::default()
})?;

Website to PDF / Screenshot (whole-site batch)

Discover every page of a website (sitemap, or full crawl on higher plans), convert each in the background, and receive a single ZIP. Requires a private API key with crawl access.

use enconvert::{CrawlMode, WaitForBatchOptions, WebsiteConversionOptions, WebsiteToPdfOptions};

let batch = client.convert_website_to_pdf(
    "https://example.com",
    WebsiteToPdfOptions {
        website: WebsiteConversionOptions {
            crawl_mode: Some(CrawlMode::Sitemap), // Auto (default) | Sitemap | Full
            exclude_patterns: Some(vec!["/blog/tag/".to_string()]), // full crawl mode only
            ..Default::default()
        },
        ..Default::default()
    },
)?;
println!("{} {} {:?}", batch.batch_id, batch.url_count, batch.discovery_method);

// Block until done and save the ZIP:
let status = client.wait_for_batch(
    &batch.batch_id,
    WaitForBatchOptions {
        save_to: Some("site.zip".into()),
        ..Default::default()
    },
)?;
println!("{} of {} pages converted", status.completed, status.total);

convert_website_to_screenshot works the same way and produces a ZIP of PNGs.

PDF Options & Authenticated Pages

use enconvert::{PdfHeaderFooter, PdfMargins, PdfOptions, PdfOrientation, UrlToPdfOptions};

let result = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    pdf_options: Some(PdfOptions {
        page_size: Some("A4".to_string()), // or custom dimensions via page_width + page_height
        orientation: Some(PdfOrientation::Landscape),
        margins: Some(PdfMargins { top: Some(10.0), bottom: Some(10.0), left: Some(15.0), right: Some(15.0) }),
        header: Some(PdfHeaderFooter { content: Some("Quarterly Report".to_string()), height: Some(15.0) }),
        footer: Some(PdfHeaderFooter { content: Some("Confidential".to_string()), height: Some(12.0) }),
        ..Default::default()
    }),
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

All URL and website conversions also accept HTTP Basic Auth, cookies, and custom headers for pages behind a login (plan-gated):

use enconvert::{BrowserCookie, HttpBasicAuth, UrlRenderOptions, UrlToPdfOptions};
use std::collections::HashMap;

client.convert_url_to_pdf("https://internal.example.com/report", UrlToPdfOptions {
    render: UrlRenderOptions {
        auth: Some(HttpBasicAuth { username: "user".to_string(), password: "pass".to_string() }),
        // or cookies / headers:
        cookies: Some(vec![BrowserCookie {
            name: "session".to_string(),
            value: "abc123".to_string(),
            domain: Some("internal.example.com".to_string()),
            ..Default::default()
        }]),
        headers: Some(HashMap::from([("X-Tenant".to_string(), "acme".to_string())])),
        ..Default::default()
    },
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

Do not combine auth with an Authorization header — the API rejects the conflict.

Job Status (async polling)

let status = client.get_job_status("job_abc123")?;
if status.status == enconvert::JobStatusValue::Success {
    println!("{:?}", status.presigned_url);
}

Error Handling

use enconvert::Error;

match client.convert_url_to_pdf("https://example.com", Default::default()) {
    Ok(result) => println!("{}", result.presigned_url),
    Err(e) if e.is_authentication() => eprintln!("Invalid API key"),
    Err(e) if e.is_rate_limit() => eprintln!("Too many requests — slow down"),
    Err(Error::Api { status, message }) => eprintln!("API error [{status}]: {message}"),
    Err(e) => return Err(e.into()),
}

Configuration

use std::time::Duration;
use enconvert::Enconvert;

let client = Enconvert::with_options(
    "sk_...",
    None,                                  // base_url override, defaults to https://api.enconvert.com
    Some(Duration::from_millis(300_000)),  // request timeout, default
)?;

Get an API Key

Sign up at enconvert.com. Free tier: 100 ops/month, no credit card.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages