---
seo_title: SDK Ruby de Conversión de Archivos: Gem para RubyGems | EnConvert
meta_desc: SDK oficial de EnConvert para Ruby 3.0+. Instala la gem enconvert para convertir archivos y para perceive, discover, distill, ingest y watch de páginas web.
keywords: sdk de conversión de archivos para ruby, convertir archivos en ruby, url a pdf en ruby, api de web scraping con ruby, docx a pdf en ruby, enconvert ruby sdk, gem de conversión de archivos rubygems, heic a webp en ruby, html a pdf gem de ruby, página web a markdown en ruby, extracción de datos estructurados en ruby, gem para rastrear sitios web
---

# SDK de Conversión de Archivos para Ruby

`enconvert` es la gem oficial de Ruby para la API de EnConvert. Convierte URL, imágenes y documentos a través de 43 endpoints de conversión implementados, y expone un segundo espacio de nombres, `client.v2`, que lee páginas web en vivo y las transforma en Markdown, JSON, capturas de pantalla y PDF listos para agentes. Cada lectura V2 lleva una puntuación `render_quality`, así que una página bloqueada o el armazón vacío de una SPA nunca pasa por contenido real. La gem está dirigida a Ruby 3.0+ y se distribuye sin dependencias en tiempo de ejecución, construida sobre `net/http`, `json` y `securerandom` de la librería estándar. Las respuestas vuelven como Structs con lectores en snake_case.

<div class="alert alert-info">
<strong>RubyGems:</strong> <code>enconvert</code> · <strong>Fuente:</strong> <a href="https://github.com/conversionapi/ruby-sdk">conversionapi/ruby-sdk</a> · <strong>Ruby:</strong> 3.0.0 o posterior · <strong>Dependencias en tiempo de ejecución:</strong> ninguna
</div>

---

## Instalación

```bash
gem install enconvert
```

O añádela a tu `Gemfile`:

```ruby
gem "enconvert"
```

```bash
bundle install
```

---

## Inicio rápido

```ruby
require "enconvert"

client = Enconvert::Client.new(api_key: ENV.fetch("ENCONVERT_API_KEY"))

# Convierte una página en vivo a PDF y transmítela a disco.
result = client.convert_url_to_pdf("https://example.com", save_to: "page.pdf")
puts result.presigned_url

# Lee la misma página como debería hacerlo un agente, con una puntuación de calidad adjunta.
op = client.v2.perceive("https://example.com", outputs: %w[markdown structured])
puts op.outputs["markdown"].url, op.render_quality  # p. ej. 0.93
```

La gem es solo del lado del servidor. Tu clave de API privada debe quedarse en el servidor, así que léela desde una variable de entorno o un gestor de secretos y nunca la envíes a un navegador ni a un binario móvil.

---

## Qué expone el cliente

`Enconvert::Client` lleva la superficie de conversión de archivos directamente, y toda la superficie de inteligencia web cuelga de `client.v2`.

| Grupo | Métodos | Devuelve |
|-------|---------|----------|
| URL individual | `convert_url_to_pdf`, `convert_url_to_screenshot`, `convert_url_to_markdown` | `ConversionResult` |
| Archivo subido | `convert_image`, `convert_document`, `convert_to_markdown`, `convert_to_pdf` | `ConversionResult` |
| Lote de sitio completo | `convert_website_to_pdf`, `convert_website_to_screenshot` | `BatchSubmission` |
| Estado y sondeo | `get_job_status`, `get_batch_status`, `wait_for_batch` | `JobStatus`, `BatchStatus` |
| Inteligencia web | `client.v2.*`, 23 métodos repartidos en seis capacidades | Structs de V2 |
| Ayudas de formatos | `Enconvert.valid_outputs_for`, `Enconvert::IMPLEMENTED_CONVERSIONS` | `Array`, `Set` |

Cada tipo de respuesta es un `Struct` creado con `keyword_init: true`, así que lees los campos como métodos corrientes: `result.presigned_url`, `op.render_quality`, `job.total_chunks`.

---

## Conversión de archivos

### convert_url_to_pdf

Renderiza cualquier URL pública a PDF.

```ruby
result = client.convert_url_to_pdf(
  "https://example.com",
  single_page: false,
  pdf_options: { page_size: "A4", orientation: "landscape" },
  viewport_width: 1440,
  save_to: "report.pdf"
)

puts result.filename, result.file_size
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `save_to` | `String` | `nil` | Ruta local donde descargar el PDF. Los directorios padre se crean automáticamente. |
| `single_page` | `Boolean` | `true` | `true` genera una única página continua. `false` pagina usando `pdf_options[:page_size]`. |
| `pdf_options` | `Hash` | `nil` | Geometría de página. Ver [Opciones de PDF](#opciones-de-pdf). |
| `viewport_width` | `Integer` | `1920` | Ancho del viewport del navegador en píxeles. |
| `viewport_height` | `Integer` | `1080` | Alto del viewport del navegador en píxeles. |
| `load_media` | `Boolean` | `true` | Espera a las imágenes y el vídeo antes de capturar. |
| `enable_scroll` | `Boolean` | `true` | Desplaza de arriba abajo para que se disparen los cargadores diferidos. |
| `output_filename` | `String` | `nil` | Sustituye el nombre de archivo generado. |
| `auth` | `Hash` | `nil` | Credenciales HTTP basic, por ejemplo `{ username: "user", password: "pass" }`. |
| `cookies`, `headers` | `Array`, `Hash` | `nil` | Cookies que establecer y encabezados de solicitud extra. Los tres campos de acceso del navegador se pasan sin cambios. |

<div class="alert alert-warning">
<strong>No combines <code>auth</code> con un encabezado <code>Authorization</code>.</strong> La API rechaza el conflicto, así que elige uno u otro.
</div>

### convert_url_to_screenshot

Captura un PNG de página completa de cualquier URL.

```ruby
client.convert_url_to_screenshot("https://example.com", viewport_width: 1440, save_to: "shot.png")
```

Acepta las mismas opciones de viewport, medios, desplazamiento, nombre de archivo y acceso del navegador que `convert_url_to_pdf`. No admite `single_page` ni `pdf_options`.

### convert_url_to_markdown

Extrae Markdown limpio con sabor GitHub desde cualquier URL. El conversor elimina la navegación, los pies de página, los anuncios y los scripts, conserva el cuerpo principal del artículo y antepone un frontmatter YAML con el título, la descripción, la url, los enlaces y las imágenes.

```ruby
client.convert_url_to_markdown("https://example.com/article", save_to: "article.md")
```

Útil para alimentar un pipeline de RAG, importar contenido de terceros a un CMS o construir un corpus de entrenamiento.

### convert_image

Convierte entre `jpeg`, `png`, `svg`, `heic` y `webp` en cualquier dirección, o rasteriza un PDF a JPEG.

```ruby
# Desde una ruta en disco.
client.convert_image("photo.heic", output_format: "webp", save_to: "photo.webp")

# Desde bytes que ya tienes.
bytes = File.binread("photo.heic")
client.convert_image({ data: bytes, filename: "photo.heic" }, output_format: "webp", save_to: "photo.webp")

# Rasteriza la primera página de un PDF.
client.convert_image("invoice.pdf", output_format: "jpeg", save_to: "invoice.jpg")
```

El formato de entrada se resuelve a partir de la extensión del nombre de archivo, así que aquí no se puede usar un `IO` en crudo sin nombre de archivo. Pasa una ruta o un Hash `{ data:, filename: }` en su lugar.

| Opción | Tipo | Obligatorio | Descripción |
|--------|------|-------------|-------------|
| `output_format` | `String` | Sí | Formato de destino. Los alias `jpg`, `yml`, `htm` y `md` se normalizan por ti. |
| `save_to` | `String` | No | Ruta local donde descargar el resultado. |
| `output_filename` | `String` | No | Sustituye el nombre de archivo generado. |

### convert_document

Convierte documentos y formatos de datos. `output_format` es `"pdf"` por defecto.

```ruby
# docx a pdf
client.convert_document("report.docx", save_to: "report.pdf")

# json a yaml
client.convert_document("data.json", output_format: "yaml", save_to: "data.yaml")

# markdown a pdf con configuración de página personalizada
client.convert_document(
  "README.md",
  output_format: "pdf",
  pdf_options: { page_size: "A4", margins: { top: 20, bottom: 20, left: 25, right: 25 } },
  save_to: "readme.pdf"
)
```

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

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

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `output_format` | `String` | `"pdf"` | Formato de destino. |
| `save_to` | `String` | `nil` | Ruta local donde descargar el resultado. |
| `output_filename` | `String` | `nil` | Sustituye el nombre de archivo generado. |
| `pdf_options` | `Hash` | `nil` | Configuración de página, respetada cuando la salida es PDF. |

### convert_to_markdown

Convierte un archivo subido de casi cualquier formato de documento a Markdown limpio. El formato se detecta en el servidor.

```ruby
client.convert_to_markdown("handbook.docx", save_to: "handbook.md")
```

**Entradas aceptadas:** PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, además de formatos ofimáticos heredados y ODF. Las imágenes no están admitidas en este endpoint.

La salida es un único archivo `.md` consciente de los encabezados, lo que lo convierte en un buen bloque de construcción para la ingesta de RAG: un troceador semántico puede dividir por la propia jerarquía de encabezados del documento en lugar de por recuentos arbitrarios de caracteres. Las únicas opciones son `save_to:` y `output_filename:`.

### convert_to_pdf

Convierte un archivo subido de casi cualquier formato a PDF.

```ruby
# pptx a pdf
client.convert_to_pdf("slides.pptx", save_to: "slides.pdf")

# pdf de paso directo, normalizado a escala de grises
client.convert_to_pdf("scan.pdf", pdf_options: { grayscale: true }, save_to: "gray.pdf")
```

**Entradas aceptadas:** formatos ofimáticos, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB o un PDF existente que se pasa directamente y se normaliza.

<div class="alert alert-warning">
<strong>Aquí solo se respeta <code>pdf_options[:grayscale]</code>.</strong> Los demás campos de geometría de página se ignoran en este endpoint. Cuando necesites control total sobre tamaño de página, orientación y márgenes, usa <code>convert_document</code> o <code>convert_url_to_pdf</code>.
</div>

Las opciones son `save_to:`, `output_filename:` y `pdf_options:` (solo escala de grises).

### convert_website_to_pdf y convert_website_to_screenshot

Descubren todas las páginas de un sitio web, convierten cada una en segundo plano y reúnen los resultados en un único ZIP. Ambos métodos son asíncronos y devuelven un `BatchSubmission` de inmediato.

```ruby
batch = client.convert_website_to_pdf(
  "https://example.com",
  crawl_mode: "sitemap",
  exclude_patterns: ["/tag/"],
  notification_email: "me@example.com"
)

status = client.wait_for_batch(batch.batch_id, save_to: "site.zip")
puts "#{status.completed} of #{status.total} pages converted"
status.items.each { |item| puts "#{item.status}\t#{item.source_url}" }
```

`convert_website_to_screenshot` funciona igual y produce un ZIP de PNG.

| Opción | Tipo | Descripción |
|--------|------|-------------|
| `crawl_mode` | `String` | Cómo se descubren las páginas, por ejemplo `"sitemap"`. |
| `include_patterns`, `exclude_patterns` | `Array` | Rastrea solo, u omite, las URL que coincidan con estos patrones. |
| `notification_email` | `String` | Dirección de correo a la que avisar cuando termine el lote. |
| `callback_url` | `String` | Webhook al que se llama al finalizar. |
| `output_filename` | `String` | Sustituye el nombre de archivo generado del ZIP. |
| `viewport_width`, `viewport_height`, `load_media`, `enable_scroll` | `Integer`, `Boolean` | Comportamiento de renderizado por página. Se envían solo cuando los estableces; de lo contrario el gateway aplica sus propios valores por defecto. |
| `auth`, `cookies`, `headers` | `Hash`, `Array`, `Hash` | Acceso del navegador para páginas protegidas. |
| `single_page`, `pdf_options` | `Boolean`, `Hash` | Solo para lotes de PDF. |

`wait_for_batch(batch_id, interval: 5, timeout: 1800, save_to: nil)` sondea hasta que el lote sale de `"processing"` y luego devuelve el `BatchStatus` final. Con `save_to` también descarga el ZIP. Lanza `Enconvert::APIError` con estado `504` cuando se alcanza el timeout, y con estado `500` cuando un lote termina sin ZIP que guardar.

### get_job_status

Sondea un único job de conversión por id.

```ruby
status = client.get_job_status("job_abc123")
case status.status
when "success" then puts status.presigned_url
when "failed"  then warn status.error
else                puts "still processing"
end
```

Devuelve un `JobStatus` con `status`, `presigned_url`, `object_key` y `error`. Rara vez necesitas llamarlo tú mismo, porque el SDK ya lo sondea en tu nombre. Ver [Recuperación de timeouts](#recuperacion-de-timeouts).

### Conversiones admitidas

La gem lleva la tabla completa de endpoints `{input}-to-{output}` implementados y valida cada par localmente, así que un par no admitido lanza `Enconvert::Error` antes de que ninguna solicitud salga de tu proceso.

```ruby
Enconvert.valid_outputs_for("json")      # => ["csv", "toml", "xml", "yaml"]
Enconvert.valid_outputs_for("pdf")       # => ["jpeg"]
Enconvert::IMPLEMENTED_CONVERSIONS.size  # => 43
```

| Entrada | Salidas |
|---------|---------|
| `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` | entre sí, los 20 pares |
| `pdf` | `jpeg` |

Eso son 43 endpoints: 13 de texto estructurado, 9 de documentos y 21 de imágenes.

---

## Inteligencia web (V2)

`client.v2` convierte páginas web en vivo en datos listos para agentes: renderizar, enumerar, buscar, extraer, ingerir y monitorizar. Cada renderizado lleva `render_quality`, una puntuación de 0.0 a 1.0 adjunta a cada lectura. Una puntuación baja significa que la página no se renderizó limpiamente, normalmente por una página de desafío, un muro de cookies, una barrera de inicio de sesión o un armazón vacío de JavaScript. El contenido se sigue devolviendo, pero marcado, junto con un array `warnings` y un Hash `deductions` que indica qué comprobaciones saltaron. Lee la puntuación antes de poner el contenido en ningún sitio, y una mala lectura nunca entrará en silencio en el contexto de tu agente.

Todos los endpoints V2 requieren una clave de API privada. Consulta la [visión general de V2](/es/docs/v2-overview) para la referencia a nivel de endpoint.

### Perceive

Renderiza una URL en los artefactos que pidas. `perceive` es síncrono y devuelve la operación completada con URL de artefacto recién firmadas.

```ruby
op = client.v2.perceive(
  "https://example.com",
  outputs: %w[markdown screenshot structured],
  extract: %w[tables metadata],
  only_main_content: true
)

puts op.operation_id, op.render_quality          # "per_...", luego de 0.0 a 1.0
puts op.outputs["markdown"].url                  # URL de artefacto firmada
puts op.outputs["markdown"].expires_in           # segundos hasta que deje de funcionar
puts op.structured, op.warnings.inspect          # Hash de Ruby corriente, luego Array de String
```

Vuelve a firmar las URL de artefacto de una operación anterior en cualquier momento:

```ruby
again = client.v2.get_perceive_operation(op.operation_id)
```

Percibe hasta 1000 URL con un único bloque de opciones compartido. Los lotes pequeños se ejecutan en línea y vuelven completados. Los más grandes vuelven con estado `"queued"`, así que sondéalos por `job_id`:

```ruby
batch = client.v2.perceive_batch(
  ["https://a.example.com", "https://b.example.com"],
  outputs: %w[markdown],
  output_mode: "zip"
)

done = client.v2.get_perceive_batch(batch.job_id)
puts "#{done.completed}/#{done.total} complete, #{done.failed} failed"
puts done.zip&.url
```

Sáltate por completo la ida y vuelta de la URL firmada con `perceive_direct`, donde el cuerpo de la respuesta HTTP es el propio artefacto:

```ruby
direct = client.v2.perceive_direct("https://example.com", outputs: %w[markdown])
puts direct.filename, direct.content_type, direct.content.bytesize
File.binwrite("page.md", direct.content)

# Vuelve a descargar un artefacto almacenado de una operación anterior.
raw = client.v2.download_perceive_artifact(op.operation_id, output: "markdown")
```

`perceive_direct` necesita exactamente una salida que produzca artefacto y, en caso contrario, lanza `Enconvert::Error` localmente. `structured` puede acompañar, porque se queda en línea en el servidor y nunca se transmite. `download_perceive_artifact` toma un `output:` opcional, que puedes omitir cuando la operación produjo exactamente un artefacto y debes nombrar cuando produjo más de uno, y devuelve `410` una vez que un artefacto supera su ventana de retención.

| Opción | Tipo | Descripción |
|--------|------|-------------|
| `outputs` | `Array` | Cualquiera de `markdown`, `html_cleaned`, `html_raw`, `screenshot`, `screenshot_full_page`, `pdf`, `links`, `images`, `structured`. En el servidor su valor por defecto es `["markdown", "structured"]`. |
| `extract` | `Array` | Cualquiera de `tables`, `prices`, `contacts`, `metadata`, `main_content`, `headings`, `structured_data`, `technologies`, `all`. |
| `schema` | `Hash` | Descripciones de campos para la extracción estructurada. |
| `wait_for`, `wait_timeout_ms` | `String`, `Integer` | Selector CSS que esperar antes de capturar, y el límite de esa espera. |
| `js_code` | `String` | JavaScript que ejecutar en la página antes de capturar. |
| `viewport`, `mobile` | `Hash`, `Boolean` | Dimensiones del viewport y si renderizar con un perfil móvil. |
| `headers`, `cookies`, `auth` | `Hash`, `Array`, `Hash` | Acceso del navegador para páginas protegidas. |
| `proxy_url` | `String` | Enruta el renderizado a través de tu propio proxy. |
| `geolocation` | `Hash` | Geolocalización simulada para el contexto del navegador. |
| `action_chain` | `Array` | Clics, desplazamientos y entradas guionizados antes de capturar. |
| `cache_mode` | `String` | `enabled`, `bypass` o `refresh`. |
| `pdf_options` | `Hash` | Geometría de página cuando `pdf` está entre las salidas. |
| `block_resources` | `Array` | Tipos de recurso que bloquear, por ejemplo `image`, `font`, `script`. |
| `respect_robots` | `Boolean` | Respeta `robots.txt`. |
| `only_main_content` | `Boolean` | Elimina la navegación y el relleno de la salida Markdown. |
| `direct_download` | `Boolean` | Solo lo acepta `perceive`. `perceive_batch` lo rechaza. |

La semántica completa de los parámetros vive en [Perceive](/es/docs/v2-perceive) y [Parámetros y opciones](/es/docs/parameters-options).

### Discover

Enumera las URL de un sitio sin renderizar nada. No interviene ningún navegador, así que es rápido y barato.

```ruby
found = client.v2.discover(
  "https://example.com",
  mode: "hybrid",            # "sitemap", "crawl" o "hybrid"
  max_urls: 200,
  max_depth: 3,
  exclude_patterns: ["/tag/"],
  same_domain_only: true,
  respect_robots: true
)

puts found.total, found.urls.first(10)
puts found.truncated        # true cuando max_urls limitó el resultado
puts found.sources.inspect  # de dónde vino cada URL
```

Consulta [Discover](/es/docs/v2-discover) para el comportamiento modo a modo.

### Lookup

Ejecuta una búsqueda web por categorías y, opcionalmente, percibe los primeros resultados en la misma llamada.

```ruby
search = client.v2.lookup(
  "best static site generators",
  category: "web",           # web, news, images, scholar, patents, maps
  num_results: 10,
  time_filter: "month",      # hour, day, week, month, year
  perceive_top: 3
)

search.results.each do |hit|
  puts hit.position, hit.title, hit.url
  puts hit.perceive&.render_quality  # presente en los resultados percibidos automáticamente
end

puts search.answer_box.inspect, search.knowledge_graph.inspect
```

Con `perceive_top: 3`, las URL de los tres primeros resultados se renderizan y llevan un `PerceiveResult` completo en línea en `hit.perceive`. Sus ids de operación también se recogen en `search.perceive_operation_ids`. Más en [Lookup](/es/docs/v2-lookup).

### Distill

Extracción estructurada guiada por schema. Proporciona exactamente uno de `urls:` o `discover_from:`, y `schema:` siempre es obligatorio. El SDK lanza `Enconvert::Error` localmente si te equivocas en eso.

```ruby
extraction = client.v2.distill(
  urls: ["https://example.com/pricing"],
  schema: { plans: "list of plan names with monthly prices" },
  css_schema: {
    base_selector: ".plan-card",
    fields: [
      { name: "name",  type: "text", selector: "h3" },
      { name: "price", type: "text", selector: ".price" }
    ]
  }
)

item = extraction.results.first
puts item.data.inspect
puts item.extraction_tier   # "css", "llm", "mixed" o "none"
puts item.fields_from_css, item.fields_from_llm
```

El `css_schema` opcional se ejecuta primero y responde todo lo que puede solo con selectores. Únicamente los campos que se le escapan escalan al nivel del modelo de lenguaje, y por eso `fields_from_css` y `fields_from_llm` se informan por separado.

Descubrir y destilar en una sola llamada:

```ruby
client.v2.distill(
  discover_from: { url: "https://example.com", mode: "sitemap", max_pages: 10 },
  schema: { title: "page title", summary: "one-line summary" }
)
```

Los tipos de campo CSS son `text`, `attribute`, `html`, `regex`, `nested`, `list` y `nested_list`. Las listas de campos anidados se serializan de forma recursiva. Consulta [Distill](/es/docs/v2-distill).

### Ingest

Convierte un sitio entero, o un conjunto de documentos subidos, en JSONL troceado y listo para RAG mediante un único pipeline. Ingest siempre es asíncrono.

```ruby
job = client.v2.ingest(
  mode: "sitemap",           # urls, sitemap, crawl
  url: "https://docs.example.com",
  max_pages: 100,
  chunk: { max_words: 512, sentence_overlap: 1 },
  webhook_url: "https://my.app/hooks/enconvert"
)

status = client.v2.get_ingest_job(job.job_id)
puts status.status                                   # queued, discovering, processing, completed, failed, canceled
puts status.pages_processed, status.total_chunks
puts status.output_url if status.status == "completed"  # JSONL
```

El modo `"urls"` toma una lista `urls:` y rechaza `url:`. Cualquier otro modo toma una `url:` semilla y rechaza `urls:`. Ambas reglas se imponen localmente antes de enviar la solicitud.

Sube archivos en lugar de rastrear:

```ruby
file_job = client.v2.ingest_files(
  ["handbook.pdf", "notes.docx"],
  chunk: { max_words: 512, sentence_overlap: 1 }
)
```

`ingest_files` acepta PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, además de formatos ofimáticos heredados y ODF. Cada entrada puede ser una ruta, un objeto similar a IO o un Hash `{ data:, filename: }`.

Gestiona jobs y la entrega de webhooks:

```ruby
list = client.v2.list_ingest_jobs(limit: 20)
list.jobs.each { |j| puts "#{j.job_id}\t#{j.status}\t#{j.total_chunks}" }
puts list.has_more

client.v2.cancel_ingest_job(job.job_id)   # idempotente

secret = client.v2.get_webhook_secret
puts secret.secret, secret.signature_header, secret.timestamp_header,
     secret.signature_scheme, secret.replay_tolerance_seconds

client.v2.rotate_webhook_secret            # las firmas antiguas dejan de verificarse al instante
client.v2.retry_ingest_webhook(job.job_id) # reenvía el webhook de un job completado
```

`retry_ingest_webhook` devuelve un `WebhookRetryResult` con `delivered`, `attempts`, `status_code` y `detail`. Responde `409` cuando el job no está completado y `400` cuando el job no tiene webhook configurado. Consulta [Ingest](/es/docs/v2-ingest).

### Watch

Vuelve a renderizar una URL a una cadencia fija y te avisa cuando cambia.

```ruby
watcher = client.v2.create_watcher(
  "https://example.com/pricing",
  frequency_minutes: 60,     # mínimo de una hora
  diff_mode: "auto",         # auto, text, structured, tables, metadata
  webhook_url: "https://my.app/hooks/changes",
  notify_email: true
)

puts watcher.watcher_id, watcher.next_check_at

history = client.v2.get_watcher_snapshots(watcher.watcher_id, limit: 10)
history.snapshots.each do |snap|
  puts snap.checked_at, snap.has_changes, snap.similarity, snap.change_count, snap.changes.inspect
end

client.v2.list_watchers(limit: 20)
client.v2.get_watcher(watcher.watcher_id)
client.v2.update_watcher(watcher.watcher_id, status: "paused")
client.v2.update_watcher(watcher.watcher_id, webhook_url: "")  # borra el webhook
client.v2.delete_watcher(watcher.watcher_id)                   # borrado lógico, idempotente
```

`update_watcher` lanza `Enconvert::Error` si lo llamas sin campos que actualizar. `delete_watcher` devuelve el watcher marcado como eliminado con estado `"deleted"`, y un watcher eliminado se lee de vuelta como `404`. La semántica completa está en [Watch](/es/docs/v2-watch).

---

## Opciones de PDF

`pdf_options` es un Hash corriente con claves de símbolo, aceptado por `convert_url_to_pdf`, `convert_website_to_pdf`, `convert_document`, `convert_to_pdf` (solo escala de grises) y `client.v2.perceive` cuando `pdf` está entre las salidas. Solo se envían las claves que estableces.

```ruby
client.convert_url_to_pdf(
  "https://example.com",
  pdf_options: { page_size: "A4", orientation: "landscape", scale: 0.9,
                 margins: { top: 10, bottom: 10, left: 15, right: 15 } },
  save_to: "report.pdf"
)
```

| Clave | Tipo | Descripción |
|-------|------|-------------|
| `page_size` | `String` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"` y similares. |
| `page_width` | `Number` | Ancho de página explícito, como alternativa a `page_size`. |
| `page_height` | `Number` | Alto de página explícito. |
| `orientation` | `String` | `"portrait"` o `"landscape"`. |
| `margins` | `Hash` | `{ top:, bottom:, left:, right: }`, todos opcionales. |
| `scale` | `Number` | Escala de renderizado, por ejemplo `0.9` para el 90 por ciento. |
| `grayscale` | `Boolean` | Posprocesa el PDF a escala de grises. |
| `header` | `Hash` | Texto del encabezado por región de página. |
| `footer` | `Hash` | Texto del pie por región de página. |

---

## Manejo de errores

Todo fallo es un `Enconvert::Error` o una de sus subclases, así que las cláusulas `rescue` pueden ser tan amplias o tan estrechas como quieras. Los fallos de validación del lado del cliente, como un par de conversión no admitido o un `schema` ausente, lanzan el `Enconvert::Error` base antes de realizar ninguna solicitud HTTP.

```ruby
begin
  client.v2.perceive("https://example.com")
rescue Enconvert::AuthenticationError
  warn "Invalid or missing API key"
rescue Enconvert::QuotaError
  warn "Request was rejected with 402"
rescue Enconvert::RateLimitError
  warn "Too many requests, back off and retry"
rescue Enconvert::APIError => e
  warn "API error [#{e.status_code}]: #{e.message}"
rescue Enconvert::Error => e
  warn "Client-side validation failed: #{e.message}"
end
```

| Clase | Se lanza en | Código de estado |
|-------|-------------|------------------|
| `Enconvert::AuthenticationError` | Clave de API inválida, ausente o revocada | `401`, `403` |
| `Enconvert::QuotaError` | Se lanza ante un HTTP 402 | `402` |
| `Enconvert::RateLimitError` | Demasiadas solicitudes | `429` |
| `Enconvert::APIError` | Cualquier otra respuesta de 400 o superior | el código real |
| `Enconvert::Error` | Clase base, más todos los fallos de validación local | ninguno |

`APIError#status_code` te da el estado HTTP, y el mensaje se toma del campo `detail` o `error` del cuerpo de la respuesta cuando el cuerpo es JSON. El mapa completo de mensajes vive en [Códigos de error](/es/docs/error-codes).

---

## Recuperación de timeouts

Un renderizado largo de URL a PDF o una conversión de documento grande pueden sobrevivir al timeout del proxy inverso incluso cuando la conversión en sí tiene éxito en el servidor. El SDK se recupera de eso de forma transparente, sin código por tu parte.

1. Antes de cada solicitud de conversión, el cliente genera un id de job de 32 caracteres y lo envía como `job_id` en el cuerpo o en el formulario multiparte.
2. Si la solicitud vuelve con un estado de 500 o superior, el cliente pasa a sondear `GET /v1/convert/status/{job_id}` cada 3 segundos. Un `404` mientras la fila del job aún se está escribiendo se ignora y el sondeo continúa.
3. En cuanto el job se lee como `success`, se devuelve el resultado. En cuanto se lee como `failed`, se lanza `Enconvert::APIError` con estado `500` y el mensaje de error del servidor.
4. El plazo de sondeo es de 300 segundos. Pasado ese punto, el cliente lanza `Enconvert::APIError` con estado `504` y el mensaje `Conversion timed out`.

El `job_id` generado por el cliente también se fusiona en cada respuesta correcta, de modo que `result.job_id` siempre está relleno y puedes sondear `get_job_status` tú mismo más adelante si quieres.

<div class="alert alert-info">
<strong>Los envíos de lotes de sitios web quedan fuera.</strong> <code>convert_website_to_pdf</code> y <code>convert_website_to_screenshot</code> no crean una fila por job, así que un 5xx ahí significa que falló el propio envío y se expone de inmediato en lugar de sondearse.
</div>

---

## Configuración

```ruby
client = Enconvert::Client.new(
  api_key: ENV.fetch("ENCONVERT_API_KEY"),
  timeout: 300,
  base_url: "https://api.enconvert.com"
)
```

| Opción | Tipo | Por defecto | Descripción |
|--------|------|-------------|-------------|
| `api_key` | `String` | obligatorio | Tu clave de API privada. Un valor `nil` o en blanco lanza `Enconvert::Error` en la construcción. |
| `timeout` | `Integer` | `300` | Timeout de apertura y de lectura en **segundos**, aplicado a cada solicitud. |
| `base_url` | `String` | `"https://api.enconvert.com"` | URL base de la API. Las barras finales se eliminan. Sobrescríbela para un gateway autoalojado. |

Las solicitudes se autentican con el encabezado `X-API-Key`. Cuando pasas `save_to`, la descarga omite deliberadamente ese encabezado, porque la URL prefirmada de almacenamiento no debe recibir tu clave de API.

<div class="alert alert-warning">
<strong>Nunca incrustes la clave de API en el código.</strong> Léela desde una variable de entorno, desde las credenciales de Rails o desde tu gestor de secretos. Cualquiera que tenga tu clave privada puede ejecutar conversiones en tu nombre. Rótala desde el <a href="/es/dashboard">panel de control</a> si se filtra.
</div>

---

## Forma del resultado

Toda conversión de un solo archivo y de una sola página devuelve un `Enconvert::ConversionResult`:

```ruby
result = client.convert_document("report.docx", save_to: "report.pdf")

result.presigned_url            # URL firmada para la salida convertida
result.object_key               # clave del objeto en almacenamiento
result.filename                 # nombre de archivo del servidor
result.file_size                # bytes, o nil
result.conversion_time_seconds  # segundos, o nil
result.job_id                   # siempre rellenado por el cliente
```

Los demás Structs de V1 son `JobStatus` (`status`, `presigned_url`, `object_key`, `error`), `BatchSubmission` (`batch_id`, `status`, `url_count`, `total_discovered`, `discovery_method`, `output_format`), `BatchStatus` (`batch_id`, `status`, `total`, `completed`, `failed`, `in_progress`, `output_mode`, `zip_download_url`, `items`) y `BatchItem` (`source_url`, `status`, `download_url`, `output_file_size`, `duration`).

En el lado de V2, `PerceiveResult` es el que hay que conocer:

```ruby
op.operation_id                              # "per_..."
op.status                                    # queued, processing, completed, failed
op.url, op.url_final, op.content_hash
op.render_quality, op.cache_hit              # de 0.0 a 1.0 (o nil), y si vino de la caché
op.outputs                                   # Hash de nombre => V2OutputArtifact
op.structured, op.extraction_tier            # Hash (o nil), y cómo se extrajo
op.tokens.input, op.tokens.output, op.cost_cents, op.duration_ms
op.error, op.warnings                        # String o nil, y Array de String
op.status_code                               # estado HTTP del origen de la página
op.deductions, op.options_echo               # por qué bajó render_quality, y las opciones usadas
```

Un `V2OutputArtifact` lleva `url`, `object_key`, `size_bytes`, `content_type` y `expires_in` (900 segundos por defecto), así que las URL de artefacto firmadas son de vida corta. Vuelve a firmarlas con `get_perceive_operation`, o descarga los bytes con `download_perceive_artifact`. Las cargas útiles proporcionadas por el usuario, como schemas, datos extraídos, campos rastreados y cambios de diff, pasan intactas como Hashes de Ruby corrientes.

Las URL prefirmadas de las conversiones V1 también son temporales. Pasa `save_to` cuando quieras los bytes en disco, o cópialos a tu propio bucket para almacenamiento permanente.

---

## Código fuente e incidencias

- **RubyGems:** [enconvert](https://rubygems.org/gems/enconvert)
- **GitHub:** [conversionapi/ruby-sdk](https://github.com/conversionapi/ruby-sdk)
- **Ruby:** 3.0.0 o posterior, sin dependencias en tiempo de ejecución
- **Licencia:** MIT
- **Otros lenguajes:** [Todos los SDK](/es/docs/sdks)
- **Referencia de la API:** [Visión general de endpoints](/es/docs/endpoints-overview), [Autenticación](/es/docs/authentication)

---

## Preguntas frecuentes

### ¿Cómo convierto archivos en Ruby con una gem?

Ejecuta `gem install enconvert`, construye un cliente con `Enconvert::Client.new(api_key: ENV.fetch("ENCONVERT_API_KEY"))` y llama a un método como `convert_document`, `convert_image` o `convert_url_to_pdf`. Pasa `save_to:` y el SDK descarga el resultado a esa ruta por ti, creando los directorios padre que hagan falta.

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

`client.convert_document("report.docx", save_to: "report.pdf")`. El formato de salida es `"pdf"` por defecto, así que solo necesitas `output_format:` cuando quieres otra cosa. La misma llamada funciona con `.doc`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages` y `.numbers`.

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

`client.convert_url_to_pdf("https://example.com", save_to: "page.pdf")`. Establece `single_page: 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 y la escala. Para una página tras autenticación HTTP basic, añade `auth: { username: ..., password: ... }`.

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

`client.convert_image("photo.heic", output_format: "webp", save_to: "photo.webp")`. El formato de entrada procede de la extensión del nombre de archivo, y la gem convierte libremente entre `jpeg`, `png`, `svg`, `heic` y `webp`. También rasteriza un PDF a JPEG con `output_format: "jpeg"`.

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

Dos opciones. `client.convert_url_to_markdown(url, save_to: "article.md")` te da Markdown limpio con sabor GitHub y frontmatter YAML. `client.v2.perceive(url, outputs: %w[markdown])` te da el mismo contenido más una puntuación `render_quality`, avisos y extracción estructurada opcional, que es lo que quieres cuando un agente va a leer el resultado.

### ¿Qué significa render_quality y por qué debería comprobarlo?

Es una puntuación de 0.0 a 1.0 adjunta a cada lectura V2 que refleja con qué limpieza se renderizó realmente la página. Una página de desafío, un muro de cookies, una barrera de inicio de sesión, una página de error HTTP o un armazón vacío de JavaScript puntúan bajo. El contenido se sigue devolviendo para que puedas inspeccionarlo, con `warnings` y `deductions` que explican la puntuación. Ponle una condición antes de guardar el texto o de dárselo a un modelo.

### ¿Reintenta el SDK de Ruby cuando una conversión larga supera el timeout?

Sí. Cada solicitud de conversión lleva un `job_id` generado por el cliente y, si la solicitud falla con un estado de 500 o superior, el SDK sondea `GET /v1/convert/status/{job_id}` cada 3 segundos durante hasta 300 segundos. Devuelve el resultado en cuanto el job tiene éxito y lanza `Enconvert::APIError` con estado `504` si vence el plazo. Los envíos de lotes de sitios completos quedan excluidos, porque no tienen una fila por job.

### ¿Qué pasa si pido un par de conversión que la API no implementa?

La gem lanza `Enconvert::Error` localmente, antes de cualquier llamada de red, y el mensaje enumera las salidas válidas para esa entrada. Puedes consultar la misma tabla tú mismo con `Enconvert.valid_outputs_for("json")`, que devuelve `["csv", "toml", "xml", "yaml"]`.

### ¿Puedo usar la gem enconvert dentro de Rails o en un job en segundo plano?

Sí, y un job en segundo plano es el lugar adecuado para ella. La gem es `net/http` puro sin dependencias en tiempo de ejecución ni estado global, así que una instancia de `Enconvert::Client` se puede construir por job o memoizar por proceso con total seguridad. Las conversiones largas bloquean el hilo que llama hasta el `timeout` configurado, que es de 300 segundos por defecto, así que mantenlas fuera del ciclo de una petición web.

### ¿Qué versiones de Ruby admite el SDK?

Ruby 3.0.0 y posteriores, según declara `required_ruby_version` en el gemspec. No hay dependencias de gems en tiempo de ejecución, así que se instala en cualquier grupo de Bundler sin arrastrar un árbol de dependencias detrás.

### ¿Cómo verifico la firma de un webhook de ingest?

Llama a `client.v2.get_webhook_secret`, que crea el secreto de firma del proyecto en el primer uso y lo devuelve junto con `signature_header`, `timestamp_header`, `signature_scheme` y `replay_tolerance_seconds`. Calcula el HMAC sobre el cuerpo entregado y compáralo con el encabezado de firma. Usa `rotate_webhook_secret` para invalidar el secreto antiguo, y `retry_ingest_webhook(job_id)` para reenviar la notificación de un job completado.
