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.
More from the blog
Automate file conversion in your n8n workflows
Our n8n community node is live. Convert documents, images, audio and video inside any n8n automation, EU-hosted, with one API key. Here is how it works.
Is it safe to convert files online?
The short answer is 'it depends on the service'. Here's what to actually check before uploading a file you care about.
Where does Smallpdf actually send your files?
Smallpdf is Swiss-branded, but the data path is more complicated than the front page suggests. Here's what we could verify from their public docs.