Changelog

New features, improvements, and fixes — everything we ship, as we ship it.

Subscribe via RSS
Clear Pick a year to narrow by month; a date range overrides both pickers.
Improved Fixed

Large files stream instead of being buffered #

The download proxy at /v1/convert/download streams from storage in 64 KB chunks instead of reading the whole object into memory first, so time to first byte drops on large files and several clients slowly pulling 150 MB files no longer risk an out of memory. Batch URL jobs, /v2/perceive zip bundles and /v2/ingest assembly write to a temp file as each result finishes and upload it as a stream, so at most one result is resident at a time. Multi-page PDF to JPEG archives are stored rather than deflated, which makes them marginally larger on the wire and quicker to produce, since JPEG does not deflate anyway. Image to PDF hands the image to the renderer as a file instead of a base64 data URI, which used to materialise about five copies of it. The S3 client is built once per process instead of once per call, which takes a TLS handshake off every upload, download, delete and health check.

Improved Security Fixed

Upload size limits measure the real file #

Fifty upload endpoints were reading the Content-Length header, which measures the whole multipart envelope, and a client that sends no Content-Length at all, which includes chunked and HTTP/2 uploads, was not size checked at all. They now measure the uploaded file's own bytes, so plan ceilings (5 MB Free, 15 MB Starter, 50 MB Pro, 150 MB Business) apply on every upload endpoint and the 413 boundary is exact. Admission control weighs bytes as well as request count: with more than 256 MB of conversions in flight the API answers 503 with Retry-After: 10 instead of running the box out of memory. That budget is tunable with MAX_PENDING_CONVERSION_BYTES.

Security Fixed

Images over 40 megapixels are rejected before decoding #

Fourteen image conversion entry points now check the declared canvas size right after reading the file header and return HTTP 400 before allocating a single pixel, with the message Image is too large to process: WxH (N pixels) exceeds the 40000000 pixel limit. A kilobyte upload can legally declare a canvas that decodes to several gigabytes, and Pillow's own bomb detector only fires around 358 MP. This is a real narrowing: a legitimate 50 MP camera photo that used to convert is now rejected. SVGs behave differently. An intrinsic canvas over 40 MP is scaled down with the aspect ratio preserved rather than refused, and an SVG whose intrinsic size cannot be read renders at 2048 px wide. The ceiling is tunable with IMAGE_MAX_PIXELS everywhere except compress-image, which keeps its own 40 MP limit and its own wording.

Fixed New

Watchers stop reporting changes that never happened #

A watch check is a diff against a stored baseline, so it now always captures with Chromium, the same engine that captured that baseline. Before this a check could fall through to the no-browser fetch path, see raw un-hydrated HTML, and fire a change webhook and email for a page that had not changed. The trade-off is that every watch check consumes a Chromium slot. Render quality scoring also recognises an un-hydrated single page app shell: a framework mount node holding fewer than 20 words on a page under 500 visible words scores 0.30 instead of 1.00, which is below the quality floor, so /v2/perceive escalates to a real browser render and a bad watch check is recorded for audit without being diffed or notified on.

Fixed Security

The no-browser fetch path works again #

The TLS fetch engine passed a resolve= argument that curl_cffi does not accept, so every request on that path raised a TypeError and fell through to a full Chromium render. It had been failing 100% of the time for about two weeks, and the raw Python error text was being appended to the warnings array of /v2/perceive responses. Eligible HTML-only pages now skip the browser, which leaves the single Chromium slot for pages that actually need it. The DNS pin that stops a redirect from rebinding to an internal address between validation and connect also takes effect for the first time. It is reapplied on every hop, cleared when a hop does not resolve, skipped for bare IP hosts, and IPv6 addresses are bracketed so the entry parses.

Fixed Improved Security

Pages with strict CSP render again #

The renderer injects a small stylesheet to fix print colors and hide modals. That injection raced Chromium's CSP error stream and threw whenever any frame logged a violation, measured at 9 of 20 renders on a page whose own main frame carries no policy at all, and it failed the entire conversion over a cosmetic style. Injection now runs inside the page and a failure is logged and skipped. CSP is also disabled per page over the DevTools protocol, which covers policies delivered by that no response rewrite can reach. The request interceptor no longer refetches and buffers every image, script and stylesheet just to strip a header that only matters on the main document. That removes 100 to 200 MB of copying per render on media heavy pages, and subresource requests now reach the SSRF re-validation check that never saw them before.

Fixed

Renders killed by a timeout or a disconnect get cleaned up #

When a request hit the 300 second gateway timeout or the client went away, the page being rendered stayed alive and kept executing the site's JavaScript, so every later conversion competed with it. A cancelled render now queues cleanup that waits for the conversion slot to free: a healthy browser gets its leftover pages parked, a browser that fails its health probe gets relaunched. Browser shutdown is bounded at 10 seconds, so a wedged Chromium can no longer hang recovery or leave the browser manager half torn down and failing every request after it. The client still gets its 504 immediately.

New

The EnConvert CLI: the whole API from your terminal #

Summary

EnConvert now has an official command-line interface. enconvert is a single, open-source (MIT) binary that covers the whole API from your terminal: file conversion across 40+ formats, URL and website rendering to PDF, screenshot or markdown, and the full v2 web-data surface (perceive, discover, lookup, distill, ingest). It installs natively on macOS, Linux and Windows, needs nothing but your existing secret API key, and sends no telemetry. Nothing in the API itself changes.

What changed

  • One command for every conversion. enconvert convert report.docx --to pdf infers the endpoint from the input extension and target format across all 46 file-conversion routes (documents, spreadsheets, presentations, images, data formats, compression). Batch globs, -O output directories, --skip-existing and per-file progress are built in.
  • URL and website rendering. enconvert url pdf|screenshot|markdown <url> exposes every render option (viewport, selectors, cookies, headers, basic auth, ad blocking, PDF geometry) 1:1 with the API, and enconvert site pdf|screenshot drives the async website crawls with --wait polling and ZIP downloads.
  • The v2 web-data verbs, first class. perceive (including batches with 200/202 handling), discover, lookup (with enrichment and answer synthesis), distill (schema, prompt and CSS-schema modes) and the complete ingest family including file uploads and webhook-secret management.
  • Scripting-grade plumbing. Stable documented exit codes, --json with the gateway's raw response, a bundled --jq filter (no jq install required), --jsonl streaming, paths-on-stdout output rules, jobs wait <id> for any job kind, and enconvert api — a gh-style passthrough that reaches every endpoint, including ones without a typed command.
  • Native installs on every platform. Homebrew (brew install enconvert/tap/enconvert), Scoop, Winget, a checksum-verifying curl -fsSL https://get.enconvert.com/install.sh | sh, and npm i -g @enconvert/cli. Standalone binaries for macOS (Intel and Apple Silicon), Linux (glibc and musl, x64 and arm64) and Windows.
  • Profiles, config and safe credentials. ~/.config/enconvert/config.toml profiles, project-level .enconvertrc.toml, a credential_helper hook for 1Password/pass/Vault, 0600-permission key storage, and automatic migration of keys already saved by npx @enconvert/mcp setup.
  • No telemetry. The CLI makes no requests other than the API calls you ask for and an optional once-daily version check that a single environment variable disables.

Scope

This is a new client only. No /v1/* or /v2/* endpoint behavior changed. The CLI covers every working v1 endpoint and every v2 endpoint except /v2/watch (watchers remain dashboard-managed for now; typed commands for them will follow). Source: https://github.com/enconvert/cli.

Docs

A new CLI documentation page was added in all five languages (en, fr, de, es, it), the CLI joined the Integrations section of the homepage with a dedicated /integrations/cli page, and the SDKs section of the docs sidebar now lists the CLI alongside the Node.js SDK and MCP server.

New Improved

Image Compression and SVG Sizing #

Summary

Two additions to the V1 conversion API, both synchronous multipart/form-data file conversions gated only by the global plan conversion limit. POST /v1/convert/compress-image compresses PNG, JPEG and WebP files without ever changing their format — lossless-first, with an optional target_size_kb budget met by aspect-ratio-locked downscaling. The three SVG rasterization endpoints (svg-to-png, svg-to-jpeg, svg-to-webp) gain optional width and height parameters that control the output dimensions in pixels; the playground exposes matching size inputs with an aspect-ratio lock derived from the uploaded SVG.

POST /v1/convert/compress-image

Accepted extensions (4): .png .jpg .jpeg .webp — magic-byte checked. The output keeps the input's extension and format; a file whose content does not match its extension is rejected with 400 rather than silently converted. Animated inputs (APNG, animated WebP) are rejected with 400 rather than silently flattened to their first frame.

Request: file (required). target_size_kb (optional; a zero/negative value is a 400 rejected before quota is burned, a non-numeric value is a 422 from request validation). output_filename (default: input basename; the input's extension is preserved). job_id (optional, enables status polling). direct_download (accepted for parity with the other file endpoints; responses currently always take the JSON path below, same as every V1 file conversion).

Behavior — stage 1, lossless (always runs): metadata (EXIF, XMP, PNG text chunks) is stripped; the ICC color profile and the EXIF orientation flag are preserved — orientation is re-emitted as a minimal single-tag EXIF block instead of being baked into pixels, which keeps the JPEG path free of extra quantization loss. PNG re-encodes at zlib level 9 + optimize, plus a palette candidate accepted only when the palette roundtrip is provably pixel-identical. JPEG re-encodes reusing the original quantization tables (quality='keep') with optimized progressive Huffman coding. WebP re-encodes as true lossless VP8L at maximum effort. The smallest of the original bytes and all candidates wins, so the output is never larger than the input.

Behavior — stage 2, dimension reduction (only when target_size_kb is set and stage 1 missed it): LANCZOS downscale with the aspect ratio locked; the scale factor is binary-searched (up to 8 iterations, minimum scale 1%) for the largest dimensions that fit the budget. Downscaled JPEG and lossy-source WebP re-encode at quality 85; PNG and lossless-source WebP stay lossless at the reduced size (lossless vs lossy WebP sources are told apart by walking the RIFF chunk list for VP8L). An unreachable target returns the smallest file achieved, not an error — check file_size / X-File-Size to see what was reached.

Response: 200 with JSON — presigned_url, object_key, filename (timestamped, input extension preserved, e.g. photo_20260717_101530123.png), file_size, conversion_time_seconds, job_id. The same values are mirrored on the X-Object-Key, X-File-Size, X-Conversion-Time, X-Filename headers.

Constraints:

  • The decoded canvas is capped at 40,000,000 pixels (e.g. 8000x5000), checked from the image header before any pixels are decoded, so a decompression bomb is rejected with 400 without allocating the full surface.
  • Whole-request size is validated against the plan's max file size via Content-Length (413).
  • The shared conversion concurrency gate applies: at capacity the endpoint returns 503 with Retry-After: 10.
  • WebP lossless encoding uses maximum effort (method=6) up to 4 MP and drops to method=4 above it, bounding CPU on large canvases; all requests are still bounded by the 300s gateway timeout (504, then poll with job_id).
  • CMYK JPEGs are re-encoded in CMYK (no mode change); 16-bit PNGs skip the palette candidate and only get the plain lossless re-encode.

width / height on svg-to-png, svg-to-jpeg, svg-to-webp

Request: optional width and height integer form fields, 110000 each. One dimension alone scales the render proportionally — the other is derived from the SVG's own aspect ratio (CairoSVG native behavior). Both together set the exact canvas size, which may change the aspect ratio. Omitting both keeps the previous behavior (the SVG's width/height/viewBox attributes decide).

Constraints:

  • Total output is capped at 25,000,000 pixels (400). For single-dimension requests the derived dimension is estimated server-side from the root <svg> width/height attributes (absolute units only) or viewBox, so an extreme-ratio SVG cannot request an unbounded render surface. If the ratio cannot be determined from those (e.g. the SVG sizes itself in em/ex/%, which CairoSVG can still resolve into a large canvas), a single-dimension request is rejected with 400 and asked to supply both width and height — the size must then be explicit and self-bounding.
  • Validation runs before quota is burned or an activity row is logged; out-of-range dimensions are pure client errors.
  • The converters' signatures stay backward-compatible — existing calls without width/height are byte-for-byte unaffected.

Playground: the three SVG conversions now show width/height inputs whose placeholders display the uploaded SVG's intrinsic size, plus a Lock aspect ratio toggle (on by default). The lock reads the SVG's width/height attributes client-side, falling back to the viewBox, and disables itself when neither yields a usable ratio. Blank fields mean "the SVG's own size". The controls are localized in all five languages.

Docs

Every affected page was updated in all five languages (en, fr, de, es, it): the three SVG endpoint pages now document width/height (parameter table, resolution notes, FAQs), a new compress-image endpoint page follows the standard skeleton, and the image-conversions category page, endpoints overview, parameters reference ("Image Options" section) and endpoint counts (48 → 49) were updated accordingly.

Improved New

Render Engine Ladder Added #

Summary

The V2 rendering endpoints — perceive, distill, ingest, and watch — now render each URL through an automatic multi-engine fallback instead of a single headless-browser pass. A page is first fetched over a real-browser TLS fingerprint; if that path is blocked or the page needs JavaScript, the request escalates to a headless-Chrome render, and a page that still looks blocked by anti-bot protection escalates once more to a stealth-hardened render. The result: more real-world pages return usable content, and simple pages come back faster. Nothing in your request changes — this is automatic, and every existing call behaves the same or better.

What changed

  • TLS-first fast path. Static and server-rendered pages are fetched over a real-browser TLS/HTTP-2 fingerprint with no headless browser involved. These pages return faster and free browser capacity for the pages that genuinely need it.
  • Automatic browser fallback. If the fast path is blocked, hits a bot wall, or the page renders empty (a client-side JavaScript app), the request transparently escalates to the headless-Chrome render — the same capture pipeline (cookie-banner dismissal, lazy-load scrolling, sticky-header handling, image waiting) as before.
  • Stealth escalation for anti-bot pages. A render that still looks blocked by anti-bot protection is retried once with additional browser-fingerprint hardening, recovering pages a plain render could not reach.
  • Same signals, same shape. The render_quality score and the blocked-page warning still tell a real render from a challenge page, and no response field changes. When every engine is still blocked, the best attempt is returned and flagged — exactly as before.

Scope

This applies to the V2 rendering endpoints only: perceive, distill, ingest, and watch. The V1 url-to-pdf, url-to-screenshot, and url-to-markdown endpoints are unchanged.