bulkconvert.io

How to add file conversion to your SaaS without owning the infrastructure

A practical walkthrough: when to bundle conversion locally, when to call an API, and how to wire a typical SaaS upload-and-convert flow.

·7 min read·dev · tutorial

Why this matters

Every SaaS eventually ships a feature that needs file conversion. Users upload a PDF and want a Word doc; they upload an MP3 and want it transcribed (which means decoding first); they upload HEIC photos from iPhones and your app needs JPGs. The instinct is to bundle a conversion library — ImageMagick for images, LibreOffice for documents, ffmpeg for media — but that path is more expensive than it looks.

This post is a practical comparison of the three options and a copy-paste integration for the third one.

Option 1: Bundle the conversion library in your app

The "obvious" answer. Install ImageMagick, LibreOffice, ffmpeg into your container, call the binary from your application.

When this is right:

  • You convert files all the time — every request, hot path
  • You already operate the right kind of infrastructure (a long-running VM or a beefy container with persistent disk)
  • You can absorb the dependency surface (each tool is hundreds of MB and has CVEs you'll patch quarterly)

When this is wrong:

  • Serverless runtime (Vercel functions, AWS Lambda, Cloudflare Workers) — most binaries don't fit in the deployment artifact size limit
  • You convert files occasionally — you'd be carrying 800 MB of dependencies for a feature 0.5% of users hit
  • Your security team doesn't want LibreOffice (CVE history) running with user-uploaded content

Option 2: Run a dedicated conversion microservice

You operate a separate small VM or container whose only job is conversion. Your main app POSTs files to it; it returns the converted output.

When this is right:

  • You have an ops team that already runs services this way
  • Conversion volume justifies a dedicated worker pool
  • Privacy or compliance demands that files never leave your network

When this is wrong:

  • One-person team — running a converter service is real ops work (queue handling, retry policies, scaling, format upgrades)
  • The team uses serverless everywhere else and a long-running VM is a mismatched primitive

Option 3: Call a third-party conversion API

A service like bulkconvert (or CloudConvert directly) exposes an HTTP endpoint: send a file URL + target format, receive a signed download URL.

When this is right:

  • Most SaaS use cases — file conversion is a feature, not the core
  • You're on serverless and want to keep deployments lean
  • You'd rather pay €0.02 per conversion than carry the dependency

When this is wrong:

  • Files are too sensitive for any third party to handle (regulated data)
  • Volume is so high that variable per-conversion cost exceeds the fixed cost of running your own

The reference integration

Here's the entire flow in a Next.js app, using bulkconvert as the conversion API:

Step 1: User uploads to your storage

You don't ship the file body through bulkconvert directly from the browser — you upload to your own storage first (S3, R2, Vercel Blob), then hand bulkconvert a signed URL. This keeps the file body in your control.

```ts // app/api/upload/route.ts import { put } from "@vercel/blob";

export async function POST(req: Request) { const formData = await req.formData(); const file = formData.get("file") as File; const blob = await put(file.name, file, { access: "public", // or "private" with signed URLs addRandomSuffix: true, }); return Response.json({ url: blob.url }); } ```

Step 2: POST to bulkconvert with the signed URL

```ts // app/api/convert/route.ts export async function POST(req: Request) { const { source_url, to } = await req.json(); const r = await fetch("https://www.bulkconvert.io/api/v1/convert", { method: "POST", headers: { Authorization: `Bearer ${process.env.BULKCONVERT_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ source_url, to }), }); const data = await r.json(); if (!r.ok) { return Response.json({ error: data.error }, { status: r.status }); } return Response.json({ download_url: data.download_url, filename: data.filename, size: data.size, }); } ```

Step 3: Stream the result to the user

Your app returns the signed `download_url` to the browser; the browser hits it directly. The converted file never transits your Vercel function — it goes from CloudConvert's storage straight to the user's browser, which keeps you well under the 4.5 MB Vercel function response limit and works for files up to 1 GB.

```tsx "use client"; const handleConvert = async (file: File, to: string) => { // 1. upload const fd = new FormData(); fd.append("file", file); const { url } = await fetch("/api/upload", { method: "POST", body: fd }).then(r => r.json());

// 2. convert const { download_url } = await fetch("/api/convert", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source_url: url, to }), }).then(r => r.json());

// 3. browser downloads directly window.location.href = download_url; }; ```

Error handling worth knowing

  • 402 monthly quota exhausted: bulkconvert returns a JSON `{ used, quota, reset_at }`. Surface a clear "you've hit your monthly limit" message; don't retry.
  • 429 daily rate limit: free-tier hit. Body includes `reset_at`. The next attempt after that timestamp will succeed.
  • 502 conversion failed: CloudConvert (the underlying engine) hit an internal error. Almost always transient — retry once.
  • 413 file too large: the source is bigger than your tier allows. Body tells you the exact size and the smallest tier that would accept it.

When to write a fallback

If the conversion fails for a reason that's bulkconvert-specific (502, network, rate limit), having a fallback inside your app is overkill unless you're at scale. For occasional users, "Please try again in a minute" is fine. At >1k conversions/day, queue the failed jobs to retry after the reset_at timestamp.

Cost math

Approximate per-conversion costs at the conversion-API layer:

| Provider | Per-conversion | Notes | |---|---|---| | CloudConvert | ~$0.02 | Credit-based pricing | | bulkconvert | €9 / 100 = €0.09 (Pro) | Includes the friendlier UX layer + landing pages | | Self-hosted (LibreOffice on €20/mo VM) | ~€0.0001 if you use it constantly | Plus ops time which dominates |

For most SaaS, the math says: use a third-party API until you're doing >10k conversions/month, then re-evaluate.

The honest read: don't ship LibreOffice in your container until you have to.