Ruby File Conversion SDK#
enconvert is the official Ruby gem for the EnConvert API. It converts URLs, images, and documents across 43 implemented conversion endpoints, and it exposes a second namespace, client.v2, that reads live web pages into agent-ready Markdown, JSON, screenshots, and PDFs. Every V2 read carries a render_quality score, so a blocked page or an empty SPA shell never passes as real content. The gem targets Ruby 3.0+ and ships with zero runtime dependencies, built on net/http, json, and securerandom from the standard library. Responses come back as Structs with snake_case readers.
enconvert · Source: conversionapi/ruby-sdk · Ruby: 3.0.0 or newer · Runtime dependencies: none
Install#
gem install enconvert
Or add it to your Gemfile:
gem "enconvert"
bundle install
Quick start#
require "enconvert"
client = Enconvert::Client.new(api_key: ENV.fetch("ENCONVERT_API_KEY"))
# Convert a live page to PDF and stream it to disk.
result = client.convert_url_to_pdf("https://example.com", save_to: "page.pdf")
puts result.presigned_url
# Read the same page the way an agent should, with a quality score attached.
op = client.v2.perceive("https://example.com", outputs: %w[markdown structured])
puts op.outputs["markdown"].url, op.render_quality # e.g. 0.93
The gem is server-side only. Your private API key must stay on the server, so read it from an environment variable or a secret manager and never ship it to a browser or a mobile binary.
What the client exposes#
Enconvert::Client carries the file conversion surface directly, and the whole web intelligence surface hangs off client.v2.
| Group | Methods | Returns |
|---|---|---|
| Single URL | convert_url_to_pdf, convert_url_to_screenshot, convert_url_to_markdown |
ConversionResult |
| Uploaded file | convert_image, convert_document, convert_to_markdown, convert_to_pdf |
ConversionResult |
| Whole site batch | convert_website_to_pdf, convert_website_to_screenshot |
BatchSubmission |
| Status and polling | get_job_status, get_batch_status, wait_for_batch |
JobStatus, BatchStatus |
| Web intelligence | client.v2.*, 23 methods across six capabilities |
V2 Structs |
| Format helpers | Enconvert.valid_outputs_for, Enconvert::IMPLEMENTED_CONVERSIONS |
Array, Set |
Every response type is a Struct created with keyword_init: true, so you read fields as ordinary methods: result.presigned_url, op.render_quality, job.total_chunks.
File conversion#
convert_url_to_pdf#
Render any public URL to 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
| Option | Type | Default | Description |
|---|---|---|---|
save_to |
String |
nil |
Local path to download the PDF to. Parent directories are created for you. |
single_page |
Boolean |
true |
true produces one continuous page. false paginates using pdf_options[:page_size]. |
pdf_options |
Hash |
nil |
Page geometry. See PDF options. |
viewport_width |
Integer |
1920 |
Browser viewport width in pixels. |
viewport_height |
Integer |
1080 |
Browser viewport height in pixels. |
load_media |
Boolean |
true |
Wait for images and video before capture. |
enable_scroll |
Boolean |
true |
Scroll top to bottom so lazy loaders fire. |
output_filename |
String |
nil |
Override the generated filename. |
auth |
Hash |
nil |
HTTP basic credentials, for example { username: "user", password: "pass" }. |
cookies, headers |
Array, Hash |
nil |
Cookies to set and extra request headers. All three browser-access fields are passed through unchanged. |
auth with an Authorization header. The API rejects the conflict, so pick one or the other.
convert_url_to_screenshot#
Capture a full-page PNG of any URL.
client.convert_url_to_screenshot("https://example.com", viewport_width: 1440, save_to: "shot.png")
Accepts the same viewport, media, scroll, filename, and browser-access options as convert_url_to_pdf. It does not take single_page or pdf_options.
convert_url_to_markdown#
Extract clean GitHub-Flavored Markdown from any URL. The converter strips navigation, footers, ads, and scripts, keeps the main article body, and prepends YAML frontmatter with the title, description, url, links, and images.
client.convert_url_to_markdown("https://example.com/article", save_to: "article.md")
Useful for feeding a RAG pipeline, importing third-party content into a CMS, or building a training corpus.
convert_image#
Convert between jpeg, png, svg, heic, and webp in any direction, or rasterize a PDF to JPEG.
# From a path on disk.
client.convert_image("photo.heic", output_format: "webp", save_to: "photo.webp")
# From bytes you already hold.
bytes = File.binread("photo.heic")
client.convert_image({ data: bytes, filename: "photo.heic" }, output_format: "webp", save_to: "photo.webp")
# Rasterize the first page of a PDF.
client.convert_image("invoice.pdf", output_format: "jpeg", save_to: "invoice.jpg")
The input format is resolved from the filename extension, so a raw IO without a filename cannot be used here. Pass a path or a { data:, filename: } Hash instead.
| Option | Type | Required | Description |
|---|---|---|---|
output_format |
String |
Yes | Target format. Aliases jpg, yml, htm, and md are normalized for you. |
save_to |
String |
No | Local path to download the result to. |
output_filename |
String |
No | Override the generated filename. |
convert_document#
Convert documents and data formats. output_format defaults to "pdf".
# docx to pdf
client.convert_document("report.docx", save_to: "report.pdf")
# json to yaml
client.convert_document("data.json", output_format: "yaml", save_to: "data.yaml")
# markdown to pdf with custom page setup
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"
)
Recognized input extensions: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.
EPUB has no dedicated document pair. Send .epub files through convert_to_pdf or convert_to_markdown instead.
| Option | Type | Default | Description |
|---|---|---|---|
output_format |
String |
"pdf" |
Target format. |
save_to |
String |
nil |
Local path to download the result to. |
output_filename |
String |
nil |
Override the generated filename. |
pdf_options |
Hash |
nil |
Page setup, honored when the output is PDF. |
convert_to_markdown#
Convert an uploaded file of almost any document format to clean Markdown. The format is detected on the server.
client.convert_to_markdown("handbook.docx", save_to: "handbook.md")
Accepted inputs: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats. Images are not supported on this endpoint.
The output is a single heading-aware .md file, which makes it a good building block for RAG ingestion: a semantic chunker can split on the document's own heading hierarchy instead of arbitrary character counts. The only options are save_to: and output_filename:.
convert_to_pdf#
Convert an uploaded file of almost any format to PDF.
# pptx to pdf
client.convert_to_pdf("slides.pptx", save_to: "slides.pdf")
# pdf passthrough, normalized to grayscale
client.convert_to_pdf("scan.pdf", pdf_options: { grayscale: true }, save_to: "gray.pdf")
Accepted inputs: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, plain text, raster images, SVG, EPUB, or an existing PDF that is passed through and normalized.
pdf_options[:grayscale] is honored here. The other page geometry fields are ignored on this endpoint. When you need full control over page size, orientation, and margins, use convert_document or convert_url_to_pdf.
Options are save_to:, output_filename:, and pdf_options: (grayscale only).
convert_website_to_pdf and convert_website_to_screenshot#
Discover every page of a website, convert each one in the background, and collect the results as a single ZIP. Both methods are asynchronous and return a BatchSubmission immediately.
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 works identically and produces a ZIP of PNGs.
| Option | Type | Description |
|---|---|---|
crawl_mode |
String |
How pages are discovered, for example "sitemap". |
include_patterns, exclude_patterns |
Array |
Only crawl, or skip, URLs matching these patterns. |
notification_email |
String |
Email address to notify when the batch finishes. |
callback_url |
String |
Webhook called on completion. |
output_filename |
String |
Override the generated ZIP filename. |
viewport_width, viewport_height, load_media, enable_scroll |
Integer, Boolean |
Per-page render behavior. Sent only when you set them, otherwise the gateway applies its own defaults. |
auth, cookies, headers |
Hash, Array, Hash |
Browser access for protected pages. |
single_page, pdf_options |
Boolean, Hash |
PDF batches only. |
wait_for_batch(batch_id, interval: 5, timeout: 1800, save_to: nil) polls until the batch leaves "processing", then returns the final BatchStatus. With save_to it also downloads the ZIP. It raises Enconvert::APIError with status 504 when the timeout is reached, and status 500 when a batch finishes without a ZIP to save.
get_job_status#
Poll a single conversion job by 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
Returns a JobStatus with status, presigned_url, object_key, and error. You rarely need to call it yourself, because the SDK already polls it on your behalf. See Timeout recovery.
Supported conversions#
The gem carries the full table of implemented {input}-to-{output} endpoints and validates every pair locally, so an unsupported pair raises Enconvert::Error before a request leaves your process.
Enconvert.valid_outputs_for("json") # => ["csv", "toml", "xml", "yaml"]
Enconvert.valid_outputs_for("pdf") # => ["jpeg"]
Enconvert::IMPLEMENTED_CONVERSIONS.size # => 43
| Input | Outputs |
|---|---|
json |
csv, toml, xml, yaml |
xml |
csv, json |
yaml |
json |
csv |
json, xml |
toml |
json |
markdown |
html, pdf |
html |
pdf |
doc, excel, ppt, odt, ods, odp, ots, pages, numbers |
pdf |
jpeg, png, svg, heic, webp |
each other, all 20 pairs |
pdf |
jpeg |
That is 43 endpoints: 13 structured text, 9 document, and 21 image conversions.
Web intelligence (V2)#
client.v2 turns live web pages into agent-ready data: render, enumerate, search, extract, ingest, and monitor. Every render carries render_quality, a score from 0.0 to 1.0 attached to every read. A low score means the page did not render cleanly, typically a challenge page, a cookie wall, a login gate, or an empty JavaScript shell. The content is still returned, but flagged, along with a warnings array and a deductions Hash that says which checks fired. Read the score before you put the content anywhere, and a bad read will never quietly enter your agent's context.
All V2 endpoints require a private API key. See V2 overview for the endpoint-level reference.
Perceive#
Render one URL into the artifacts you ask for. perceive is synchronous and returns the completed operation with freshly signed artifact URLs.
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_...", then 0.0 to 1.0
puts op.outputs["markdown"].url # signed artifact URL
puts op.outputs["markdown"].expires_in # seconds until it stops working
puts op.structured, op.warnings.inspect # plain Ruby Hash, then Array of String
Re-sign the artifact URLs of an earlier operation at any time:
again = client.v2.get_perceive_operation(op.operation_id)
Perceive up to 1000 URLs with one shared options block. Small batches run inline and come back completed. Larger ones come back with status "queued", so poll them by 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
Skip the signed-URL round trip entirely with perceive_direct, where the HTTP response body is the artifact itself:
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)
# Re-download a stored artifact from an earlier operation.
raw = client.v2.download_perceive_artifact(op.operation_id, output: "markdown")
perceive_direct needs exactly one artifact-producing output and raises Enconvert::Error locally otherwise. structured may ride along, because it stays inline server-side and is never streamed. download_perceive_artifact takes an optional output:, which you can omit when the operation produced exactly one artifact and must name when it produced more than one, and it returns 410 once an artifact is past its retention window.
| Option | Type | Description |
|---|---|---|
outputs |
Array |
Any of markdown, html_cleaned, html_raw, screenshot, screenshot_full_page, pdf, links, images, structured. Defaults server-side to ["markdown", "structured"]. |
extract |
Array |
Any of tables, prices, contacts, metadata, main_content, headings, structured_data, technologies, all. |
schema |
Hash |
Field descriptions for structured extraction. |
wait_for, wait_timeout_ms |
String, Integer |
CSS selector to wait for before capture, and the cap on that wait. |
js_code |
String |
JavaScript to run in the page before capture. |
viewport, mobile |
Hash, Boolean |
Viewport dimensions, and whether to render with a mobile profile. |
headers, cookies, auth |
Hash, Array, Hash |
Browser access for protected pages. |
proxy_url |
String |
Route the render through your own proxy. |
geolocation |
Hash |
Spoofed geolocation for the browser context. |
action_chain |
Array |
Scripted clicks, scrolls, and inputs before capture. |
cache_mode |
String |
enabled, bypass, or refresh. |
pdf_options |
Hash |
Page geometry when pdf is among the outputs. |
block_resources |
Array |
Resource types to block, for example image, font, script. |
respect_robots |
Boolean |
Honor robots.txt. |
only_main_content |
Boolean |
Strip navigation and boilerplate from the Markdown output. |
direct_download |
Boolean |
Accepted by perceive only. perceive_batch rejects it. |
Full parameter semantics live in Perceive and Parameters and options.
Discover#
Enumerate a site's URLs without rendering anything. No browser is involved, so it is fast and cheap.
found = client.v2.discover(
"https://example.com",
mode: "hybrid", # "sitemap", "crawl", or "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 when max_urls capped the result
puts found.sources.inspect # where each URL came from
See Discover for mode-by-mode behavior.
Lookup#
Run a categorized web search, and optionally perceive the top results in the same call.
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 # present for auto-perceived hits
end
puts search.answer_box.inspect, search.knowledge_graph.inspect
With perceive_top: 3, the first three result URLs are rendered and carry a full PerceiveResult inline on hit.perceive. Their operation ids are also collected in search.perceive_operation_ids. More in Lookup.
Distill#
Schema-driven structured extraction. Provide exactly one of urls: or discover_from:, and schema: is always required. The SDK raises Enconvert::Error locally if you get that wrong.
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", or "none"
puts item.fields_from_css, item.fields_from_llm
The optional css_schema runs first and answers whatever it can from selectors alone. Only the fields it misses escalate to the language-model tier, which is why fields_from_css and fields_from_llm are reported separately.
Discover and distill in one call:
client.v2.distill(
discover_from: { url: "https://example.com", mode: "sitemap", max_pages: 10 },
schema: { title: "page title", summary: "one-line summary" }
)
CSS field types are text, attribute, html, regex, nested, list, and nested_list. Nested field lists are serialized recursively. See Distill.
Ingest#
Turn a whole site, or a set of uploaded documents, into chunked RAG-ready JSONL through one pipeline. Ingest is always asynchronous.
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
Mode "urls" takes a urls: list and rejects url:. Every other mode takes a seed url: and rejects urls:. Both rules are enforced locally before the request is sent.
Upload files instead of crawling:
file_job = client.v2.ingest_files(
["handbook.pdf", "notes.docx"],
chunk: { max_words: 512, sentence_overlap: 1 }
)
ingest_files accepts PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT and MD, plus legacy and ODF office formats. Each entry can be a path, an IO-like object, or a { data:, filename: } Hash.
Manage jobs and webhook delivery:
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) # idempotent
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 # old signatures stop verifying at once
client.v2.retry_ingest_webhook(job.job_id) # re-deliver a completed job's webhook
retry_ingest_webhook returns a WebhookRetryResult with delivered, attempts, status_code, and detail. It responds 409 when the job is not completed and 400 when the job has no webhook configured. See Ingest.
Watch#
Re-render a URL on a fixed cadence and get notified when it changes.
watcher = client.v2.create_watcher(
"https://example.com/pricing",
frequency_minutes: 60, # hourly floor
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: "") # clears the webhook
client.v2.delete_watcher(watcher.watcher_id) # soft delete, idempotent
update_watcher raises Enconvert::Error if you call it with no fields to update. delete_watcher returns the tombstoned watcher with status "deleted", and a deleted watcher reads back as 404. Full semantics in Watch.
PDF options#
pdf_options is a plain Hash with symbol keys, accepted by convert_url_to_pdf, convert_website_to_pdf, convert_document, convert_to_pdf (grayscale only), and client.v2.perceive when pdf is among the outputs. Only the keys you set are sent on the wire.
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"
)
| Key | Type | Description |
|---|---|---|
page_size |
String |
"A4", "A3", "Letter", "Legal", and similar. |
page_width |
Number |
Explicit page width, as an alternative to page_size. |
page_height |
Number |
Explicit page height. |
orientation |
String |
"portrait" or "landscape". |
margins |
Hash |
{ top:, bottom:, left:, right: }, all optional. |
scale |
Number |
Render scale, for example 0.9 for 90 percent. |
grayscale |
Boolean |
Post-process the PDF to grayscale. |
header |
Hash |
Header text per page region. |
footer |
Hash |
Footer text per page region. |
Error handling#
Every failure is an Enconvert::Error or one of its subclasses, so rescue clauses can be as broad or as narrow as you want. Client-side validation failures, such as an unsupported conversion pair or a missing schema, raise the base Enconvert::Error before any HTTP request is made.
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
| Class | Raised on | Status code |
|---|---|---|
Enconvert::AuthenticationError |
Invalid, missing, or revoked API key | 401, 403 |
Enconvert::QuotaError |
Raised on HTTP 402 | 402 |
Enconvert::RateLimitError |
Too many requests | 429 |
Enconvert::APIError |
Any other response of 400 or above | the actual code |
Enconvert::Error |
Base class, plus all local validation failures | none |
APIError#status_code gives you the HTTP status, and the message is pulled from the response body's detail or error field when the body is JSON. The full message map lives in Error codes.
Timeout recovery#
A long URL-to-PDF render or a large document conversion can outlive the reverse-proxy timeout even when the conversion itself succeeds on the server. The SDK recovers from that transparently, with no code on your side.
- Before each conversion request the client generates a 32-character job id and sends it as
job_idin the body or the multipart form. - If the request comes back with a status of 500 or above, the client switches to polling
GET /v1/convert/status/{job_id}every 3 seconds. A404while the job row is still being written is ignored, and polling continues. - As soon as the job reads
success, the result is returned. As soon as it readsfailed,Enconvert::APIErroris raised with status500and the server's error message. - The polling deadline is 300 seconds. Past that, the client raises
Enconvert::APIErrorwith status504and the messageConversion timed out.
The client-generated job_id is also merged into every successful response, so result.job_id is always populated and you can poll get_job_status yourself later if you want to.
convert_website_to_pdf and convert_website_to_screenshot do not create a per-job row, so a 5xx there means the submission itself failed and is surfaced immediately rather than polled.
Configuration#
client = Enconvert::Client.new(
api_key: ENV.fetch("ENCONVERT_API_KEY"),
timeout: 300,
base_url: "https://api.enconvert.com"
)
| Option | Type | Default | Description |
|---|---|---|---|
api_key |
String |
required | Your private API key. A nil or blank value raises Enconvert::Error at construction. |
timeout |
Integer |
300 |
Open and read timeout in seconds, applied to every request. |
base_url |
String |
"https://api.enconvert.com" |
API base URL. Trailing slashes are stripped. Override it for a self-hosted gateway. |
Requests authenticate with the X-API-Key header. When you pass save_to, the download deliberately bypasses that header, because the presigned storage URL must not receive your API key.
Result shape#
Every single-file and single-page conversion returns an Enconvert::ConversionResult:
result = client.convert_document("report.docx", save_to: "report.pdf")
result.presigned_url # signed URL for the converted output
result.object_key # storage object key
result.filename # server-side filename
result.file_size # bytes, or nil
result.conversion_time_seconds # seconds, or nil
result.job_id # always populated by the client
The other V1 Structs are 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), and BatchItem (source_url, status, download_url, output_file_size, duration).
On the V2 side, PerceiveResult is the one to know:
op.operation_id # "per_..."
op.status # queued, processing, completed, failed
op.url, op.url_final, op.content_hash
op.render_quality, op.cache_hit # 0.0 to 1.0 (or nil), and whether it came from cache
op.outputs # Hash of name => V2OutputArtifact
op.structured, op.extraction_tier # Hash (or nil), and how it was extracted
op.tokens.input, op.tokens.output, op.cost_cents, op.duration_ms
op.error, op.warnings # String or nil, and Array of String
op.status_code # upstream HTTP status of the page
op.deductions, op.options_echo # why render_quality dropped, and the options used
A V2OutputArtifact carries url, object_key, size_bytes, content_type, and expires_in (900 seconds by default), so signed artifact URLs are short-lived. Re-sign them with get_perceive_operation, or download the bytes with download_perceive_artifact. User-supplied payloads such as schemas, extracted data, tracked fields, and diff changes pass through untouched as plain Ruby Hashes.
Presigned URLs from V1 conversions are also temporary. Pass save_to when you want the bytes on disk, or copy them into your own bucket for permanent storage.
Source and issues#
- RubyGems: enconvert
- GitHub: conversionapi/ruby-sdk
- Ruby: 3.0.0 or newer, no runtime dependencies
- License: MIT
- Other languages: All SDKs
- API reference: Endpoints overview, Authentication
Frequently asked questions#
How do I convert files in Ruby with a gem?#
Run gem install enconvert, build a client with Enconvert::Client.new(api_key: ENV.fetch("ENCONVERT_API_KEY")), and call a method such as convert_document, convert_image, or convert_url_to_pdf. Pass save_to: and the SDK downloads the result to that path for you, creating parent directories as needed.
How do I convert DOCX to PDF in Ruby?#
client.convert_document("report.docx", save_to: "report.pdf"). The output format defaults to "pdf", so you only need output_format: when you want something else. The same call works for .doc, .xls, .xlsx, .ppt, .pptx, .odt, .ods, .odp, .ots, .pages, and .numbers.
How do I convert a URL to PDF in Ruby?#
client.convert_url_to_pdf("https://example.com", save_to: "page.pdf"). Set single_page: false to paginate instead of producing one continuous page, and pass pdf_options: for page size, orientation, margins, and scale. For a page behind HTTP basic auth, add auth: { username: ..., password: ... }.
How do I convert HEIC to WebP in Ruby?#
client.convert_image("photo.heic", output_format: "webp", save_to: "photo.webp"). The input format comes from the filename extension, and the gem converts freely between jpeg, png, svg, heic, and webp. It also rasterizes a PDF to JPEG with output_format: "jpeg".
How do I scrape a web page into Markdown from Ruby?#
Two options. client.convert_url_to_markdown(url, save_to: "article.md") gives you clean GitHub-Flavored Markdown with YAML frontmatter. client.v2.perceive(url, outputs: %w[markdown]) gives you the same content plus a render_quality score, warnings, and optional structured extraction, which is what you want when an agent is going to read the result.
What does render_quality mean and why should I check it?#
It is a score from 0.0 to 1.0 attached to every V2 read, reflecting how cleanly the page actually rendered. A challenge page, a cookie wall, a login gate, an HTTP error page, or an empty JavaScript shell all score low. The content is still returned so you can inspect it, with warnings and deductions explaining the score. Gate on it before you store the text or feed it to a model.
Does the Ruby SDK retry when a long conversion times out?#
Yes. Every conversion request carries a client-generated job_id, and if the request fails with a status of 500 or above the SDK polls GET /v1/convert/status/{job_id} every 3 seconds for up to 300 seconds. It returns the result once the job succeeds and raises Enconvert::APIError with status 504 if the deadline passes. Whole-site batch submissions are excluded, because they have no per-job row.
What happens if I request a conversion pair the API does not implement?#
The gem raises Enconvert::Error locally, before any network call, and the message lists the valid outputs for that input. You can check the same table yourself with Enconvert.valid_outputs_for("json"), which returns ["csv", "toml", "xml", "yaml"].
Can I use the enconvert gem inside Rails or a background job?#
Yes, and a background job is the right place for it. The gem is plain net/http with no runtime dependencies and no global state, so an Enconvert::Client instance is safe to build per job or memoize per process. Long conversions block the calling thread up to the configured timeout, which defaults to 300 seconds, so keep them out of a web request cycle.
Which Ruby versions does the SDK support?#
Ruby 3.0.0 and newer, as declared by required_ruby_version in the gemspec. There are no runtime gem dependencies, so it installs into any Bundler group without pulling a dependency tree behind it.
How do I verify an ingest webhook signature?#
Call client.v2.get_webhook_secret, which creates the project's signing secret on first use and returns it alongside signature_header, timestamp_header, signature_scheme, and replay_tolerance_seconds. Compute the HMAC over the delivered body and compare it against the signature header. Use rotate_webhook_secret to invalidate the old secret, and retry_ingest_webhook(job_id) to re-deliver a completed job's notification.