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.

RubyGems: enconvert · Fuente: conversionapi/ruby-sdk · Ruby: 3.0.0 o posterior · Dependencias en tiempo de ejecución: ninguna

Instalación#

gem install enconvert

O añádela a tu Gemfile:

gem "enconvert"
bundle install

Inicio rápido#

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.

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.
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.
No combines auth con un encabezado Authorization. La API rechaza el conflicto, así que elige uno u otro.

convert_url_to_screenshot#

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

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.

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.

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

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

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.

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

Aquí solo se respeta pdf_options[:grayscale]. 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 convert_document o convert_url_to_pdf.

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.

batch = client.convert_website_to_pdf(
  "https://example.com",
  crawl_mode: "sitemap",
  exclude_patterns: ["/tag/"],
  notification_email: "[email protected]"
)

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.

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.

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.

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

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:

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:

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:

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 y Parámetros y opciones.

Discover#

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

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

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.

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.

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:

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.

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.

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:

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:

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.

Watch#

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

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.


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.

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.

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.


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.

Los envíos de lotes de sitios web quedan fuera. convert_website_to_pdf y convert_website_to_screenshot 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.

Configuración#

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.

Nunca incrustes la clave de API en el código. 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 panel de control si se filtra.

Forma del resultado#

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

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:

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#


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.