---
seo_title: SDK Rust de Conversión de Archivos: Cliente crates.io | EnConvert
meta_desc: SDK oficial de EnConvert para Rust. Cliente bloqueante sobre reqwest, sin runtime asíncrono, con conversión de archivos y el espacio de nombres V2 completo.
keywords: sdk de conversión de archivos para rust, convertir archivos en rust, url a pdf en rust, api de web scraping con rust, docx a pdf en rust, enconvert rust sdk, crate de html a pdf en rust, heic a webp en rust, api de capturas de pantalla de webs en rust, cliente api bloqueante con reqwest, extraer markdown de una url en rust
---

# SDK de Conversión de Archivos para Rust

`enconvert` es el crate oficial de Rust para la API de EnConvert. Es un cliente bloqueante construido sobre la API bloqueante de `reqwest`, así que no hay runtime asíncrono, no hay `tokio` en tu árbol de dependencias y no hay ningún `.await` en tu código. Doce métodos de `Enconvert` cubren la conversión de archivos: URL a PDF, capturas de pantalla, extracción de Markdown, pares de formatos de imagen y de documento, cualquier cosa a PDF y lotes de sitios completos. Un segundo espacio de nombres, `client.v2()`, añade veintitrés métodos de inteligencia web: perceive, discover, lookup, distill, ingest y watch. Cada struct de opciones deriva `Default`, y cada fallo es un único enum de error.

<div class="alert alert-info">
<strong>crates.io:</strong> <code>enconvert</code> (0.1.0) &middot; <strong>Fuente:</strong> <a href="https://github.com/conversionapi/rust-sdk">conversionapi/rust-sdk</a> &middot; <strong>Rust:</strong> edición 2021 &middot; <strong>Licencia:</strong> MIT
</div>

---

## Instalación

```bash
cargo add enconvert
```

O añádelo a mano. `distill` y la extracción por `schema` de perceive reciben un `serde_json::Map<String, Value>`, así que añade también `serde_json` si piensas usarlos:

```toml
[dependencies]
enconvert = "0.1"
serde_json = "1"
```

El crate arrastra `reqwest` (blocking, json, multipart), `serde`, `serde_json`, `thiserror` y `uuid`. No hay features opcionales que activar.

---

## Inicio rápido

```rust
use enconvert::{Enconvert, PerceiveOptions, PerceiveOutputName, UrlToPdfOptions};

fn main() -> Result<(), enconvert::Error> {
    let key = std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set");
    let client = Enconvert::new(key)?;

    // Convierte una URL a PDF y escribe el resultado directamente en disco.
    let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
        save_to: Some("page.pdf".into()),
        ..Default::default()
    })?;
    println!("{}", pdf.presigned_url);

    // Lee la misma página como debería hacerlo tu agente, con una puntuación de calidad adjunta.
    let op = client.v2().perceive("https://example.com", PerceiveOptions {
        outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
        ..Default::default()
    })?;
    println!("{:?}", op.render_quality); // p. ej. Some(0.93)
    Ok(())
}
```

Cada método bloquea el hilo que llama, lo que hace fácil meter el SDK en una CLI, en un build script, en un hilo trabajador o en un handler web síncrono. Todos los tipos públicos se reexportan en la raíz del crate, así que los fragmentos siguientes no necesitan más que `use enconvert::{...}` y el `client` de arriba.

<div class="alert alert-warning">
<strong>No llames al SDK desde un hilo de un runtime asíncrono.</strong> El cliente bloqueante de <code>reqwest</code> no se puede accionar desde un hilo que ya pertenece a un reactor de Tokio (o similar). Desde código asíncrono, envuelve cada llamada en <code>tokio::task::spawn_blocking</code>.
</div>

---

## Qué expone el cliente

| Superficie | Cómo se accede | Qué cubre |
|------------|----------------|-----------|
| Conversión de archivos | `client.<method>` | 12 métodos: URL a PDF, captura de pantalla y Markdown; pares de imagen y de documento; cualquier cosa a Markdown y cualquier cosa a PDF; lotes de sitios completos; estado de job y de lote |
| Inteligencia web | `client.v2().<method>` | 23 métodos repartidos entre perceive, discover, lookup, distill, ingest y watch |
| Tablas de formatos | `valid_outputs_for`, `IMPLEMENTED_CONVERSIONS` | Los 43 endpoints `{input}-to-{output}` implementados, comprobados en el cliente antes de enviar una solicitud |
| Errores | `enconvert::Error` | Un enum, nueve variantes, con `is_authentication`, `is_quota`, `is_rate_limit`, `is_server_error` y `status_code` |

Nada es un builder y nada es `async`. La forma idiomática de llamada es un literal de struct con `..Default::default()`.

---

## Conversión de archivos

### `convert_url_to_pdf`

Renderiza cualquier URL pública a PDF.

```rust
use enconvert::{PdfOptions, PdfOrientation, UrlToPdfOptions};

let result = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    single_page: Some(false),
    pdf_options: Some(PdfOptions {
        page_size: Some("A4".to_string()),
        orientation: Some(PdfOrientation::Landscape),
        ..Default::default()
    }),
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;
println!("{} ({:?} bytes)", result.filename, result.file_size);
```

| Campo | Tipo | Por defecto | Descripción |
|-------|------|-------------|-------------|
| `save_to` | `Option<PathBuf>` | - | Ruta local donde escribir el PDF. Los directorios padre se crean automáticamente. |
| `single_page` | `Option<bool>` | `true` | `true` produce una única página continua. `false` pagina usando `pdf_options.page_size`. |
| `pdf_options` | `Option<PdfOptions>` | - | Geometría de página, escala, escala de grises, encabezado y pie. Consulta [Opciones de PDF](#opciones-de-pdf). |
| `render.viewport_width` / `render.viewport_height` | `Option<u32>` | `1920` / `1080` | Viewport del navegador en píxeles. |
| `render.load_media` / `render.enable_scroll` | `Option<bool>` | `true` | Espera a imágenes y vídeo, y desplaza de arriba abajo para disparar los cargadores diferidos. |
| `render.output_filename` | `Option<String>` | automático | Sobrescribe el nombre de archivo generado. |
| `render.auth` / `render.cookies` / `render.headers` | ver tipos | - | Autenticación HTTP Basic, hasta 50 cookies inyectadas, hasta 20 encabezados de solicitud adicionales. |

El bloque `render` es `UrlRenderOptions`, compartido por todas las conversiones de URL y de sitio web. No combines `auth` con un encabezado `Authorization`: la API rechaza el conflicto.

### `convert_url_to_screenshot`

Captura un PNG de cualquier URL. `UrlToScreenshotOptions` lleva el mismo bloque `render` más `save_to`, y nada más.

```rust
use enconvert::{UrlRenderOptions, UrlToScreenshotOptions};

client.convert_url_to_screenshot("https://example.com", UrlToScreenshotOptions {
    render: UrlRenderOptions { viewport_width: Some(1440), ..Default::default() },
    save_to: Some("shot.png".into()),
})?;
```

### `convert_url_to_markdown`

Extrae Markdown limpio con sabor GitHub a partir de una URL. Se eliminan la navegación, los pies de página, los anuncios y los scripts, se conserva el cuerpo principal del artículo y se antepone frontmatter YAML (title, description, url, links, images).

```rust
use enconvert::UrlToMarkdownOptions;

client.convert_url_to_markdown("https://example.com/article", UrlToMarkdownOptions {
    save_to: Some("article.md".into()),
    ..Default::default()
})?;
```

Útil para pipelines de RAG, importaciones a un CMS y recolección de datos de entrenamiento. Para una lectura puntuada, con artefactos y extracción estructurada en una sola llamada, usa [perceive](#perceive) en su lugar.

### `convert_image`

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp` (los 20 pares ordenados), o rasteriza un PDF a JPEG.

```rust
use enconvert::{ConvertImageOptions, NamedFile};

// Desde una ruta: el formato de entrada procede de la extensión.
client.convert_image("photo.heic", ConvertImageOptions {
    output_format: "webp".to_string(),
    save_to: Some("photo.webp".into()),
    ..Default::default()
})?;

// Desde bytes: proporciona un nombre de archivo para que la extensión se pueda seguir leyendo.
let data = std::fs::read("photo.heic")?;
client.convert_image(
    NamedFile { data, filename: "photo.heic".to_string(), content_type: None },
    ConvertImageOptions { output_format: "png".to_string(), ..Default::default() },
)?;
```

`output_format` es el único campo obligatorio; `save_to` y `output_filename` son opcionales. El primer argumento es cualquier cosa que se convierta en `FileInput`: una ruta `&str` o `String`, un `&Path` o `PathBuf`, un `Vec<u8>` o `&[u8]` de bytes crudos, o un `NamedFile` cuando tienes los bytes y quieres declarar tú mismo el nombre de archivo y el tipo de contenido.

### `convert_document`

Convierte documentos y formatos de datos. `output_format` es `"pdf"` por defecto, y `yml`, `htm`, `md` y `jpg` se normalizan a sus nombres canónicos. `save_to`, `output_filename` y `pdf_options` son opcionales.

```rust
use enconvert::{ConvertDocumentOptions, PdfOptions};

// docx a pdf (el formato de salida por defecto)
client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

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

// markdown a pdf con configuración de página personalizada
client.convert_document("README.md", ConvertDocumentOptions {
    pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
    ..Default::default()
})?;
```

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

| Formato de entrada | Salidas válidas |
|--------------------|-----------------|
| `json` | `csv`, `toml`, `xml`, `yaml` |
| `xml` | `csv`, `json` |
| `csv` | `json`, `xml` |
| `yaml`, `toml` | `json` |
| `markdown` | `html`, `pdf` |
| `html` | `pdf` |
| `doc`, `excel`, `ppt`, `odt`, `ods`, `odp`, `ots`, `pages`, `numbers` | `pdf` |
| `jpeg`, `png`, `svg`, `heic`, `webp` | entre sí, los 20 pares |
| `pdf` | `jpeg` |

EPUB no tiene un par de documento dedicado. Envía los archivos `.epub` a través de `convert_to_pdf` o `convert_to_markdown`.

Tanto `convert_image` como `convert_document` resuelven el formato de entrada a partir de la extensión del archivo y comprueban el par contra la tabla del lado del cliente, así que un par no implementado devuelve `Error::UnsupportedConversion` con las salidas válidas enumeradas, antes de realizar ninguna solicitud HTTP. Consulta esa misma tabla directamente:

```rust
use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};

println!("{:?}", valid_outputs_for("json"));   // ["csv", "toml", "xml", "yaml"]
println!("{}", IMPLEMENTED_CONVERSIONS.len()); // 43
```

### `convert_to_markdown`

Sube un documento de casi cualquier tipo y recibe Markdown limpio: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, y archivos de ofimática antiguos o en ODF. El formato se detecta en el servidor, así que no se ejecuta ninguna comprobación de extensión en el cliente. Las imágenes no están admitidas.

```rust
use enconvert::ConvertToMarkdownOptions;

client.convert_to_markdown("handbook.pdf", ConvertToMarkdownOptions {
    save_to: Some("handbook.md".into()),
    ..Default::default()
})?;
```

La salida es un único archivo `.md` consciente de los encabezados, lo que lo convierte en una buena pieza de partida para el troceado de RAG: un chunker semántico puede dividir por la propia jerarquía de encabezados del documento en lugar de por recuentos arbitrarios de caracteres. Aquí no hay opciones de PDF, solo `save_to` y `output_filename`.

### `convert_to_pdf`

Sube casi cualquier cosa y recibe un PDF: ofimática, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB o un PDF existente que pasa de largo.

```rust
use enconvert::{ConvertToPdfOptions, PdfOptions};

client.convert_to_pdf("slides.pptx", ConvertToPdfOptions {
    save_to: Some("slides.pdf".into()),
    ..Default::default()
})?;

// PDF de entrada, PDF en escala de grises de salida.
client.convert_to_pdf("scan.pdf", ConvertToPdfOptions {
    pdf_options: Some(PdfOptions { grayscale: Some(true), ..Default::default() }),
    save_to: Some("scan-gray.pdf".into()),
    ..Default::default()
})?;
```

<div class="alert alert-warning">
<strong>Aquí solo se respeta <code>pdf_options.grayscale</code>.</strong> El endpoint de cualquier cosa a PDF detecta automáticamente la entrada y aplica su propia geometría. Para controlar el tamaño de página, la orientación, los márgenes, la escala, los encabezados y los pies, usa <code>convert_url_to_pdf</code> o <code>convert_document</code> con una entrada HTML o Markdown.
</div>

### `convert_website_to_pdf` y `convert_website_to_screenshot`

Descubre todas las páginas de un sitio web, convierte cada una en segundo plano y recoge un único ZIP. Ambos son solo asíncronos: devuelven un `BatchSubmission`, nunca un archivo terminado.

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

let batch = client.convert_website_to_pdf("https://example.com", WebsiteToPdfOptions {
    website: WebsiteConversionOptions {
        crawl_mode: Some(CrawlMode::Sitemap),
        exclude_patterns: Some(vec!["/blog/tag/".to_string()]),
        notification_email: Some("ops@example.com".to_string()),
        ..Default::default()
    },
    ..Default::default()
})?;

// Bloquea hasta que el lote se resuelva y luego guarda el 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);
```

| Campo | Tipo | Por defecto | Descripción |
|-------|------|-------------|-------------|
| `website.crawl_mode` | `Option<CrawlMode>` | `Auto` | `Auto`, `Sitemap` (solo sitemap.xml) o `Full` (sitemap más un rastreo en anchura). |
| `website.include_patterns` / `website.exclude_patterns` | `Option<Vec<String>>` | - | Rastrea solo, o salta, las URL que coincidan con estos patrones. Solo en modo de rastreo completo. |
| `website.notification_email` / `website.callback_url` | `Option<String>` | propietario del proyecto / - | Dirección a la que se envía el correo, y webhook al que se hace POST, cuando el lote termina. |
| `website.render` | `UrlRenderOptions` | - | Los mismos campos de viewport, medios, desplazamiento, autenticación, cookies y encabezados que en las conversiones de una sola URL. |
| `single_page` / `pdf_options` | ver arriba | - | Solo en lotes de PDF. |

`convert_website_to_screenshot` recibe `WebsiteToScreenshotOptions`, un alias de tipo de `WebsiteConversionOptions`, y produce un ZIP de PNG. `wait_for_batch` sondea `get_batch_status` cada `interval_ms` (5000 por defecto) hasta que el lote deja el estado `Processing`, y después descarga opcionalmente el ZIP. Si primero se agota `timeout_ms` (1.800.000 por defecto, es decir, 30 minutos), devuelve `Error::Api { status: 504, .. }`.

### `get_job_status` y `get_batch_status`

```rust
use enconvert::JobStatusValue;

let status = client.get_job_status("job_abc123")?;
match status.status {
    JobStatusValue::Success => println!("{:?}", status.presigned_url),
    JobStatusValue::Failed => eprintln!("{:?}", status.error),
    other => println!("still running: {other:?}"),
}

let batch = client.get_batch_status("batch_abc123")?; // recuentos agregados y elementos por URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
```

<div class="alert alert-info">
<strong>Rara vez necesitas <code>get_job_status</code> directamente.</strong> El SDK lo sondea por ti cuando una conversión síncrona devuelve 5xx. Consulta <a href="#timeout-recovery">Recuperación de timeouts</a>.
</div>

---

## Inteligencia web (V2)

Todo lo que hay bajo `client.v2()` convierte páginas web en datos listos para agentes. Lo único que toda lectura tiene en común es `render_quality`, una puntuación de 0.0 a 1.0 adjunta a cada página renderizada. Una puntuación baja significa que la página no se renderizó con limpieza: una página de desafío, un muro de cookies o de inicio de sesión, el armazón vacío de una SPA o un error HTTP. El contenido se sigue devolviendo, pero vuelve marcado, con un mapa `deductions` con nombres y una lista `warnings`, de modo que una mala lectura nunca entra en silencio en el contexto de tu agente. Los resultados de perceive, distill, lookup y watch lo llevan todos. Empieza por la [visión general de V2](/es/docs/v2-overview) para conocer los conceptos detrás de las seis capacidades.

### Perceive

Renderiza una URL en los artefactos que pidas. Es síncrono: la llamada devuelve una operación completada con URL de artefacto firmadas durante 15 minutos. Consulta [perceive](/es/docs/v2-perceive).

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

let op = client.v2().perceive("https://example.com", PerceiveOptions {
    outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Screenshot]),
    extract: Some(vec![PerceiveExtractName::Tables, PerceiveExtractName::Metadata]),
    ..Default::default()
})?;

println!("{:?}", op.render_quality);
println!("{:?}", op.deductions); // p. ej. {"http_error": 0.7} cuando algo va mal
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);

// Las URL de artefacto se vuelven a firmar en cada lectura de estado.
client.v2().get_perceive_operation(&op.operation_id)?;
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `outputs` | `Option<Vec<PerceiveOutputName>>` | `markdown`, `structured` | `Markdown`, `HtmlCleaned`, `HtmlRaw`, `Screenshot`, `ScreenshotFullPage`, `Pdf`, `Links`, `Images`, `Structured`. |
| `extract` | `Option<Vec<PerceiveExtractName>>` | - | `Tables`, `Prices`, `Contacts`, `Metadata`, `MainContent`, `Headings`, `StructuredData`, `Technologies`, `All`. |
| `schema` | `Option<Map<String, Value>>` | - | Schema JSON para la extracción estructurada. |
| `only_main_content` | `Option<bool>` | `true` | Elimina la navegación, el encabezado, el pie y los banners de cookies del artefacto Markdown y del extract `main_content`. |
| `wait_for` / `wait_timeout_ms` / `js_code` | ver tipos | - / `30000` / - | Un selector CSS (opcionalmente `css:...`) o `js:<expr>` que esperar, cuánto esperar (de 0 a 60000) y JavaScript que ejecutar tras la navegación (máximo 20000 caracteres). |
| `viewport` / `mobile` | `Option<PerceiveViewport>` / `Option<bool>` | 1920x1080 | Ancho de 320 a 3840, alto de 240 a 2160, o un perfil de dispositivo móvil. |
| `cache_mode` / `block_resources` | ver tipos | `Enabled` / - | `Enabled` cachea durante 1 hora, `Bypass` se salta la caché, `Refresh` vuelve a renderizar; más los tipos de recurso que el navegador no debería cargar. |
| `headers`, `cookies`, `auth`, `respect_robots`, `pdf_options` | ver `UrlRenderOptions` y `PdfOptions` | - | Las mismas formas que las opciones de renderizado de V1. `pdf_options` solo importa cuando `outputs` incluye `Pdf`. |

`perceive_direct` devuelve en streaming un único artefacto como bytes crudos en lugar de un sobre JSON. Hay que pedir exactamente una salida que produzca artefacto, y el SDK lo comprueba localmente con `Error::InvalidInput` antes de enviar nada.

```rust
use enconvert::{PerceiveOptions, PerceiveOutputName};

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)?;

// Vuelve a descargar más tarde un artefacto almacenado. Pasa None cuando la operación produjo solo uno.
let saved = client
    .v2()
    .download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());
```

`PerceiveDirectResult` lleva `content`, `content_type`, `filename`, `operation_id`, `object_key`, `cache_hit`, `render_quality`, `source_status_code`, `content_hash` y `warnings_count`, todos leídos de los encabezados de la respuesta. Un `Error::Api { status: 410, .. }` desde `download_perceive_artifact` significa que el artefacto ha superado su ventana de retención.

`perceive_batch` acepta hasta 1000 URL y un único bloque de opciones compartido. Los lotes pequeños se resuelven en línea; los más grandes vuelven con estado `Queued`, así que sondea `get_perceive_batch` con el `job_id`.

```rust
use enconvert::{PerceiveBatchOptions, PerceiveBatchOutputMode, PerceiveOptions, PerceiveOutputName};

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

let done = client.v2().get_perceive_batch(&batch.job_id)?;
for item in &done.items {
    println!("{} {:?}", item.url, item.render_quality);
}
```

### Discover

Enumera las URL de un sitio sin renderizado de navegador alguno. Consulta [discover](/es/docs/v2-discover).

```rust
use enconvert::{DiscoverMode, DiscoverOptions};

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

`mode` es `Hybrid` por defecto (`Sitemap` y `Crawl` son las alternativas), `max_urls` es 100 (de 1 a 1000), `max_depth` es 2 (de 1 a 5) y `same_domain_only` es true. `include_patterns` y después `exclude_patterns` aplican filtrado por regex, con un máximo de 50 cada uno, y `respect_robots` respeta robots.txt. El resultado lleva `urls`, `total`, `pages_crawled`, `truncated`, `robots_respected` y un mapa `sources` con los recuentos crudos por fuente.

### Lookup

Ejecuta una búsqueda web categorizada y, opcionalmente, renderiza los primeros resultados en la misma llamada. Consulta [lookup](/es/docs/v2-lookup).

```rust
use enconvert::{LookupCategory, LookupOptions};

let search = client.v2().lookup("best static site generators", LookupOptions {
    category: Some(LookupCategory::Web),
    num_results: Some(10),
    country: Some("us".to_string()),
    perceive_top: Some(3),
    ..Default::default()
})?;

for hit in &search.results {
    let quality = hit.perceive.as_ref().and_then(|p| p.render_quality);
    println!("{:?} {:?} {quality:?}", hit.title, hit.url);
}
```

`category` es `Web` por defecto (`News`, `Images`, `Scholar`, `Patents` y `Maps` son las otras), `num_results` es 10 (de 1 a 100), `page` es 1 (de 1 a 10) y `autocorrect` es true. `country` y `locale` reciben códigos como `"us"` y `"en"`, `location` recibe texto libre como `"Austin, Texas"`, y `time_filter` acepta `Hour`, `Day`, `Week`, `Month` o `Year`. `perceive_top` (de 0 a 10, 0 por defecto) renderiza automáticamente las N primeras URL de resultado y adjunta un `PerceiveResult` completo a cada acierto. El resultado también lleva `answer_box`, `knowledge_graph` y `perceive_operation_ids`.

### Distill

Extracción estructurada guiada por schema: le das una forma y recibes esa forma para cada URL. Primero se ejecuta una pasada CSS opcional, y todo lo que se le escape escala al nivel LLM. Consulta [distill](/es/docs/v2-distill).

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

// CssField no tiene Default porque `field_type` es obligatorio.
fn text_field(name: &str, selector: &str) -> CssField {
    CssField {
        name: name.into(), field_type: CssFieldType::Text, selector: Some(selector.into()),
        attribute: None, pattern: None, default: None, transform: None, fields: None,
    }
}

let distilled = 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 {
        base_selector: ".plan-card".to_string(),
        fields: vec![text_field("name", "h3"), text_field("price", ".price")],
        name: None,
        target_field: None,
    }),
    ..Default::default()
})?;

for item in &distilled.results {
    println!("{:?} tier {:?} css {} llm {}",
        item.data, item.extraction_tier, item.fields_from_css, item.fields_from_llm);
}
```

O descubre primero un sitio y destila cada página que encuentre:

```rust
use enconvert::{DistillDiscoverFrom, DistillOptions};
use serde_json::json;

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom {
        max_pages: Some(20), // de 1 a 50, limita tanto el descubrimiento como la destilación
        ..DistillDiscoverFrom::new("https://example.com")
    }),
    schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
    ..Default::default()
})?;
```

Proporciona exactamente uno de `urls` (máximo 50) o `discover_from`. Ambos, o ninguno, devuelve `Error::InvalidInput` antes de enviar ninguna solicitud. `schema` es obligatorio y es o bien un objeto JSON-Schema o bien un mapa plano `{field: description}`. `CssFieldType` cubre `Text`, `Attribute`, `Html`, `Regex`, `Nested`, `List` y `NestedList`; `transform` acepta `Lowercase`, `Uppercase` o `Strip`; el anidamiento está limitado a una profundidad de 5.

### Ingest

Convierte un sitio entero, o un montón de documentos subidos, en JSONL troceado y listo para RAG a través de un único pipeline. Ingest es siempre asíncrono. Consulta [ingest](/es/docs/v2-ingest).

```rust
use enconvert::{
    IngestChunkOptions, IngestFilesOptions, IngestMode, IngestOptions, IngestStatus, V2ListOptions,
};

// Desde un sitio.
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()
})?;

// O desde archivos subidos: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD,
// y documentos de ofimática antiguos o en ODF.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;

// Sondea, lista, cancela.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
    println!("{:?}", status.output_url); // URL firmada del JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotente
```

`mode` es `Urls` por defecto, lo que exige una lista `urls` no vacía (máximo 1000) y rechaza `url`; `Sitemap` y `Crawl` son lo contrario, y exigen la `url` semilla. Ese emparejamiento se valida en el cliente, así que una discrepancia devuelve `Error::InvalidInput` de inmediato. `max_pages` es 50 por defecto (de 1 a 1000), `max_depth` es 2 (de 1 a 5), `same_domain_only` es true, `chunk.max_words` es 512 (de 32 a 4000) y `chunk.sentence_overlap` es 1 (de 0 a 10). `include_patterns`, `exclude_patterns`, `respect_robots`, `wait_for` y `wait_timeout_ms` se comportan igual que en discover y perceive. Los webhooks de finalización van firmados con HMAC:

```rust
let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?;            // las firmas antiguas dejan de verificarse al instante
client.v2().retry_ingest_webhook("ing_abc123")?; // reenvía el webhook de un job completado
```

### Watch

Vuelve a renderizar una URL con una cadencia fija y descubre qué ha cambiado. Consulta [watch](/es/docs/v2-watch).

```rust
use enconvert::{
    SnapshotListOptions, V2ListOptions, WatchCreateOptions, WatchDiffMode, WatchUpdateStatus,
    WatcherUpdate,
};

let watcher = client.v2().create_watcher("https://example.com/pricing", WatchCreateOptions {
    frequency_minutes: Some(60),
    diff_mode: Some(WatchDiffMode::Auto),
    webhook_url: Some("https://my.app/hooks/changes".to_string()),
    notify_email: Some(true),
    ..Default::default()
})?;
println!("{} next check {:?}", watcher.watcher_id, watcher.next_check_at);

// Lee el historial de comprobaciones, de la más reciente a la más antigua.
let history = client
    .v2()
    .get_watcher_snapshots(&watcher.watcher_id, SnapshotListOptions { limit: Some(10) })?;
for snap in &history.snapshots {
    println!("{} changed: {} similarity: {:?}", snap.checked_at, snap.has_changes, snap.similarity);
}
client.v2().list_watchers(V2ListOptions::default())?;
client.v2().get_watcher(&watcher.watcher_id)?;

// Ponlo en pausa. Pasar Some(String::new()) en webhook_url borraría el webhook.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
    status: Some(WatchUpdateStatus::Paused),
    ..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // borrado lógico, idempotente
```

`frequency_minutes` es 60 por defecto y acepta de 60 a 43200, así que el suelo horario es rígido. `diff_mode` es `Auto` por defecto y también acepta `Text`, `Structured`, `Tables` y `Metadata`, con `track_fields` para limitar el motor de diferencias a un subconjunto de campos o selectores. `notify_email` es true por defecto y avisa por correo al propietario del proyecto cuando hay cambios; `webhook_url` añade un webhook de cambios firmado con HMAC.

`update_watcher` exige al menos un campo y devuelve `Error::InvalidInput` ante un `WatcherUpdate` con todo en `None`. Las entradas `changes` de un snapshot contienen contenido de página no confiable, así que escápalas antes de renderizarlas en cualquier sitio.

---

## Opciones de PDF

`PdfOptions` es compartido por `convert_url_to_pdf`, `convert_document`, `convert_to_pdf` y la salida `Pdf` de perceive. Todos los campos son opcionales y solo se envían cuando se establecen.

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

fn block(text: &str, height: f64) -> PdfHeaderFooter {
    PdfHeaderFooter { content: Some(text.into()), height: Some(height) }
}

client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
    pdf_options: Some(PdfOptions {
        page_size: Some("A4".to_string()),
        orientation: Some(PdfOrientation::Landscape),
        margins: Some(PdfMargins {
            top: Some(10.0), bottom: Some(10.0), left: Some(15.0), right: Some(15.0),
        }),
        scale: Some(0.9),
        header: Some(block("Quarterly Report", 15.0)),
        footer: Some(block("Confidential", 12.0)),
        ..Default::default()
    }),
    ..Default::default()
})?;
```

| Campo | Tipo | Descripción |
|-------|------|-------------|
| `page_size` | `Option<String>` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` y similares. |
| `page_width` / `page_height` | `Option<f64>` | Dimensiones personalizadas. Establecidas juntas, prevalecen sobre `page_size`. |
| `orientation` | `Option<PdfOrientation>` | `Portrait` o `Landscape`. |
| `margins` | `Option<PdfMargins>` | `top`, `bottom`, `left`, `right`, cada uno opcional. |
| `scale` | `Option<f64>` | Escala de renderizado, por ejemplo `0.9` para el 90 por ciento. |
| `grayscale` | `Option<bool>` | Posprocesa el PDF a escala de grises. |
| `header` / `footer` | `Option<PdfHeaderFooter>` | `content` (máximo 2000 caracteres) y `height`. |

La semántica completa de los parámetros está en [parámetros y opciones](/es/docs/parameters-options).

---

## Manejo de errores

Hay un único tipo de error, `enconvert::Error`. Implementa `std::error::Error` mediante `thiserror`, así que `?` se propaga a cualquier `Box<dyn Error>` o `anyhow::Result`.

```rust
use enconvert::{Error, UrlToPdfOptions};

match client.convert_url_to_pdf("https://example.com", UrlToPdfOptions::default()) {
    Ok(result) => println!("{}", result.presigned_url),
    Err(e) if e.is_authentication() => eprintln!("check ENCONVERT_API_KEY"),
    Err(e) if e.is_rate_limit() => eprintln!("too many requests, back off"),
    Err(e) if e.is_server_error() => eprintln!("gateway problem, retry later"),
    Err(Error::UnsupportedConversion(msg)) => eprintln!("{msg}"),
    Err(Error::Api { status, message }) => eprintln!("API error [{status}]: {message}"),
    Err(e) => return Err(e),
}
```

| Variante | Se devuelve en | Código de estado |
|----------|----------------|------------------|
| `Error::Authentication(String)` | Clave de API inválida, ausente o revocada | `401`, `403` |
| `Error::Quota(String)` | Se lanza ante un HTTP 402 | `402` |
| `Error::RateLimit(String)` | Límite de tasa superado | `429` |
| `Error::Api { status, message }` | Cualquier otra respuesta 4xx o 5xx | el código real |
| `Error::UnsupportedConversion(String)` | Un par `{input}-to-{output}` que la API no implementa, detectado en el cliente | - |
| `Error::InvalidInput(String)` | Argumentos incorrectos detectados antes de la solicitud, como una clave de API vacía | - |
| `Error::Http(reqwest::Error)` | Fallo de conexión, TLS, timeout o decodificación de la respuesta | el del transporte, cuando existe |
| `Error::Io(std::io::Error)` | Leer un archivo para subirlo, o escribir una descarga | - |
| `Error::Json(serde_json::Error)` | Serializar un cuerpo de solicitud | - |

Cuatro predicados mantienen cortos los brazos del `match`: `is_authentication()`, `is_quota()`, `is_rate_limit()` e `is_server_error()`. `status_code()` devuelve `Option<u16>` para cada variante que lleve uno. El mapa completo de mensajes está en la [referencia de códigos de error](/es/docs/error-codes).

---

## Recuperación de timeouts

Los renderizados largos de URL a PDF y las conversiones de documentos grandes pueden sobrevivir al timeout de un proxy inverso incluso cuando la conversión en sí tiene éxito en el servidor. El SDK se recupera por su cuenta:

1. Antes de cada solicitud genera un UUID y lo envía como `job_id` en el cuerpo o en el formulario multipart.
2. Si esa solicitud vuelve como un `Error::Api` 5xx, el SDK pasa en silencio a sondear `GET /v1/convert/status/{job_id}` cada 3 segundos.
3. En cuanto el job marca `success`, devuelve el resultado. En cuanto marca `failed`, devuelve `Error::Api` con el mensaje del servidor.
4. El plazo de sondeo es de 5 minutos. Pasado ese punto obtienes `Error::Api { status: 504, message: "Conversion timed out" }`.

La recuperación cubre `convert_url_to_pdf`, `convert_url_to_screenshot`, `convert_url_to_markdown`, `convert_image`, `convert_document`, `convert_to_markdown` y `convert_to_pdf`. Deliberadamente no cubre los envíos de lotes de sitios web: esos no tienen una fila por job, así que un 5xx ahí significa que falló el propio envío y se expone directamente. Los endpoints V2 responden directamente y tampoco tienen respaldo por job. A las respuestas que omiten `job_id` se les rellena el generado por el cliente, así que `result.job_id` siempre es algo que puedes pasarle después a `get_job_status`.

---

## Configuración

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

let client = Enconvert::with_options(
    std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set"),
    Some("https://api.enconvert.com"), // sobrescribe la URL base
    Some(Duration::from_secs(300)),    // timeout de la solicitud
)?;
println!("enconvert {}", enconvert::VERSION);
```

| Argumento | Tipo | Por defecto | Descripción |
|-----------|------|-------------|-------------|
| `api_key` | `impl Into<String>` | obligatorio | Clave de API privada. Una cadena vacía devuelve `Error::InvalidInput`, y también una clave que contenga caracteres inválidos para un encabezado. |
| `base_url` | `Option<&str>` | `https://api.enconvert.com` | Las barras finales se eliminan. |
| `timeout` | `Option<Duration>` | 300 segundos | Se aplica a la solicitud completa. |

`Enconvert::new(api_key)` es la forma abreviada de `with_options(api_key, None, None)`.

<div class="alert alert-warning">
<strong>Nunca escribas la clave de API directamente en el código.</strong> Léela desde una variable de entorno o un gestor de secretos. El SDK marca el valor del encabezado <code>X-API-Key</code> como sensible para que no aparezca en la salida de depuración, y descarga las URL firmadas mediante un segundo cliente HTTP sin autenticar, de modo que tu clave nunca se envía al almacenamiento de objetos. Genera y rota claves en el <a href="/es/dashboard">panel de control</a>, y consulta <a href="/es/docs/authentication">autenticación</a> para conocer los tipos de clave.
</div>

---

## Forma del resultado

Todos los métodos de conversión de un solo archivo devuelven un `ConversionResult`:

```rust
pub struct ConversionResult {
    pub presigned_url: String,
    pub object_key: String,
    pub filename: String,
    pub file_size: Option<u64>,
    pub conversion_time_seconds: Option<f64>,
    pub job_id: Option<String>,
}
```

Descarga el archivo tú mismo desde `presigned_url`, o pasa `save_to` y deja que el SDK lo escriba por ti. Las URL firmadas caducan, así que guarda en tu propio bucket todo lo que necesites conservar.

Las rutas asíncronas devuelven sus propias formas. `JobStatus` lleva `status` (`Processing`, `Success`, `Failed` o `Unknown(String)`), `presigned_url`, `object_key` y `error`. `BatchSubmission` lleva `batch_id`, `status`, `url_count`, `total_discovered`, `discovery_method` y `output_format`. `BatchStatus` añade los contadores `total`, `completed`, `failed` e `in_progress`, más `output_mode`, `zip_download_url` y un `Vec<BatchItem>` con las filas por URL.

Las lecturas V2 devuelven `PerceiveResult`, cuyo mapa `outputs` está indexado por nombre de salida (`"markdown"`, `"screenshot_full_page"`, y así) con valores `V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in }`. Esas URL de artefacto están firmadas durante 15 minutos y se vuelven a firmar en cada llamada a `get_perceive_operation`. Junto a ellas están `render_quality`, `status_code`, `deductions`, `cache_hit`, `structured`, `extraction_tier`, `tokens`, `cost_cents`, `duration_ms`, `warnings` y `options_echo`. Cada enum de respuesta lleva una variante `Unknown(String)`, así que un valor de estado añadido en el servidor después de compilar tu binario se parsea sin problemas en lugar de fallar.

---

## Código fuente e incidencias

- **crates.io:** [enconvert](https://crates.io/crates/enconvert)
- **GitHub:** [conversionapi/rust-sdk](https://github.com/conversionapi/rust-sdk)
- **Licencia:** MIT
- **Otros clientes:** [todos los SDK](/es/docs/sdks) y la [referencia de endpoints REST](/es/docs/endpoints-overview)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en Rust con un paquete de crates.io?

Ejecuta `cargo add enconvert`, construye un cliente con `Enconvert::new(api_key)?` y llama a un método como `convert_url_to_pdf`, `convert_image` o `convert_document`. Cada struct de opciones deriva `Default`, así que escribes un literal de struct con `..Default::default()` y estableces solo los campos que te importan. Pasa `save_to` para que el SDK escriba la salida directamente en disco.

### ¿El SDK de Rust necesita tokio o un runtime asíncrono?

No. Está construido sobre la API bloqueante de `reqwest`, así que cada método bloquea el hilo que llama y no hay `.await`, ni executor, ni `tokio` en tu árbol de dependencias. Si tu aplicación ya es asíncrona, llama al SDK desde `tokio::task::spawn_blocking` en lugar de hacerlo directamente en un hilo del reactor, porque el cliente bloqueante de `reqwest` no puede ejecutarse dentro del contexto de un runtime asíncrono.

### ¿Cómo convierto una URL a PDF en Rust?

Llama a `convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() })`. Establece `single_page: Some(false)` para paginar en lugar de producir una única página continua, y pasa `pdf_options` para el tamaño de página, la orientación, los márgenes, la escala, la escala de grises, los encabezados y los pies.

### ¿Cómo convierto DOCX a PDF en Rust?

Llama a `convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() })`. PDF es la salida por defecto, así que `output_format` se puede dejar sin establecer. El mismo método se encarga de XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML y Markdown a PDF, además de pares de formatos de datos como JSON a YAML y CSV a XML.

### ¿Cómo convierto HEIC a WebP en Rust?

Llama a `convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() })`. El formato de entrada procede de la extensión del archivo, y están implementados los 20 pares ordenados entre `jpeg`, `png`, `svg`, `heic` y `webp`, más `pdf` a `jpeg` para rasterizar. Un par no admitido devuelve `Error::UnsupportedConversion` antes de cualquier llamada de red.

### ¿Cómo extraigo una página web a Markdown limpio desde Rust?

De dos maneras. `convert_url_to_markdown` devuelve un único archivo Markdown con frontmatter YAML. `client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() })` devuelve el mismo contenido con una puntuación `render_quality`, deducciones con nombre, avisos y, opcionalmente, capturas de pantalla, enlaces o extracción estructurada del mismo renderizado. Usa perceive cuando un agente vaya a leer el resultado.

### ¿Cómo sé si una página se renderizó de verdad?

Lee `render_quality`, una puntuación de 0.0 a 1.0 presente en cada lectura V2. Una puntuación baja significa que el renderizado no fue limpio: una página de desafío, un muro de cookies o de inicio de sesión, el armazón vacío de una SPA o un error HTTP. El mapa `deductions` nombra cada penalización que se activó, `status_code` da el estado HTTP de origen y `warnings` enumera qué salió mal. El contenido se sigue devolviendo, solo que marcado.

### ¿Qué pasa cuando una conversión tarda más que el timeout del proxy?

El SDK se encarga. Cada solicitud lleva un `job_id` generado por el cliente; si la solicitud devuelve 5xx, el SDK sondea `GET /v1/convert/status/{job_id}` cada 3 segundos durante hasta 5 minutos y devuelve el resultado terminado como si nada hubiera pasado. Pasado el plazo obtienes `Error::Api { status: 504, message: "Conversion timed out" }`. Los envíos de lotes de sitios completos son la excepción, y `wait_for_batch` es la herramienta para esos casos.

### ¿Puedo usar el SDK de Rust desde un navegador o un target WASM?

No. Se autentica con una clave de API privada que nunca debe llegar a un cliente, y depende del cliente bloqueante de `reqwest` con TLS nativo e hilos, y ninguna de esas dos cosas existe en `wasm32-unknown-unknown`. Ejecútalo en un servidor, en una CLI o en un worker, y haz que tu frontend hable con tu propio backend.
