A user taps "upload" on an iPhone and your server receives IMG_4211.HEIC. Your image CDN does not render it, the PDF library you use for invoices rejects it, and Chrome on Windows shows a broken icon. Meanwhile your marketing site is full of PNG screenshots weighing 900 KB each, and a designer has just handed you the new logo as an SVG for a mobile app that only accepts raster icons.
These are the same problem: a file in format A has to become format B, often at a different size. The conversion itself is the easy part. The judgment is in choosing B, sending the file in a way the API accepts, and not losing the output afterwards.
01Choose the target format before you write any code
Most format mistakes are made before the first request: someone converts everything to PNG "to be safe" and ships pages three times heavier than they need to be, or converts a logo to JPEG and gets grey fringes around the text. Pick the target from what the image is and where it is going.
| Format | Good for | Weak at | Notes |
|---|---|---|---|
| JPEG | Photos, anything with gradients and noise | Sharp edges, text, transparency | Lossy. Every re-save loses a little more. Universally supported. |
| PNG | Screenshots, UI, logos, anything needing transparency | Photos (files get huge) | Lossless. Safe for text and flat colour. |
| WebP | Photos and graphics on the web | Old software, some email clients, office tools | Lossy or lossless, usually much smaller than JPEG or PNG at similar quality. |
| AVIF | Web photos where size matters most | Tooling support, encoding speed | Often smaller still than WebP. |
| HEIC/HEIF | Storage on Apple devices | Almost everywhere else | What iPhones save by default. Convert on arrival. |
| GIF | Legacy animations | Everything else | 256 colours per frame. |
| SVG | Logos, icons, diagrams | Photos; anything that must be a bitmap | Vector, scales without blur, but it is code, not pixels. |
Two rules cover most real decisions. First, photographs go lossy, graphics go lossless: JPEG or WebP for a camera image, PNG for a screenshot or a logo with transparency. Second, the destination decides, not the source. A web page can take WebP; an email to an unknown client, a PDF generator, or a government upload form usually wants JPEG or PNG.
One catalog fact shapes the table: the converters read AVIF but do not write it. The output formats on Image Converter are png, jpg, webp, gif, tiff, bmp and pdf. If you need AVIF on the way out, WebP is the closest format you can produce here.
A support form accepts screenshots that users annotate with red arrows and text. Your app stores them for agents to read. Which target format is the right default?
02Four endpoints, one engine
The catalog has four converters. They share one engine, the same parameters, the same response shape and the same price (10 credits a call). What differs is which input files each one accepts.
| API | Accepts | Writes |
|---|---|---|
imageconverter | HEIC, HEIF, AVIF, WebP, PNG, JPG, GIF, TIFF, BMP, PDF, SVG | png, jpg, webp, gif, tiff, bmp, pdf |
heicconverter | HEIC, HEIF | png, jpg, webp, gif, tiff |
webpconverter | WebP, PNG, JPG, GIF | png, jpg, webp, gif, tiff |
svgconverter | SVG | png, jpg, webp |
If your upload form takes anything a user might throw at it, call imageconverter and stop thinking about it. The narrow ones earn their place when you want the restriction: an "upload your iPhone photo" path that should reject anything that is not HEIC, or a logo pipeline that must only ever receive SVG. Letting the API refuse the wrong input type is one less validation branch in your own code.
The parameters, from the definition:
image(required): the file, as a multipart upload.outputFormat(required): one of the formats above.quality(default90): applies tojpgandwebpoutput.maxWidth,maxHeight: shrink to fit while keeping the aspect ratio.
The response carries inputFormat, outputFormat, expires and downloadURL on every plan. inputSize and outputSize are premium on imageconverter, so do not build a "you saved 64%" message that depends on them unless your plan includes them.
03Upload the file, do not send JSON
These four endpoints only take a file. Send JSON and you get a 400 that says the API requires a file upload. The request must be multipart/form-data, with the file in a field named image and the other parameters as ordinary form fields.
Two details catch people out.
Let your HTTP client write the Content-Type header. A multipart body needs a boundary string, and the header has to name it. If you set Content-Type: multipart/form-data by hand, the boundary is missing and a request that worked yesterday starts returning 400.
Send a real filename with the right extension. The engine reads the extension of the uploaded filename to work out what it has been given. A buffer appended as blob with no name gives it less to go on. Pass the original name, or at least upload.heic.
// Node 18+, ESM. FormData and Blob are globals.
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
const KEY = process.env.APIVERVE_API_KEY;
export async function convertImage(filePath, outputFormat, opts = {}) {
const bytes = await readFile(filePath);
const form = new FormData();
form.append('image', new Blob([bytes]), path.basename(filePath));
form.append('outputFormat', outputFormat);
if (opts.quality) form.append('quality', String(opts.quality));
if (opts.maxWidth) form.append('maxWidth', String(opts.maxWidth));
if (opts.maxHeight) form.append('maxHeight', String(opts.maxHeight));
const res = await fetch('https://api.apiverve.com/v1/imageconverter', {
method: 'POST',
headers: { 'x-api-key': KEY }, // no Content-Type: fetch adds it with the boundary
body: form,
signal: AbortSignal.timeout(30_000),
});
const body = await res.json();
if (!res.ok || body.status !== 'ok') {
throw new Error(`convert failed (${res.status}): ${body.error}`);
}
return body.data;
}
const data = await convertImage('IMG_4211.HEIC', 'jpg', { quality: 82, maxWidth: 2048 });
console.log(data.inputFormat, '->', data.outputFormat);
// The link expires; copy the bytes somewhere you control now.
const img = await fetch(data.downloadURL);
await writeFile('IMG_4211.jpg', Buffer.from(await img.arrayBuffer()));import os
import requests
KEY = os.environ["APIVERVE_API_KEY"]
def convert_image(file_path, output_format, quality=None, max_width=None, max_height=None):
fields = {"outputFormat": output_format}
if quality:
fields["quality"] = str(quality)
if max_width:
fields["maxWidth"] = str(max_width)
if max_height:
fields["maxHeight"] = str(max_height)
with open(file_path, "rb") as f:
res = requests.post(
"https://api.apiverve.com/v1/imageconverter",
headers={"x-api-key": KEY}, # requests builds the multipart header
files={"image": (os.path.basename(file_path), f)},
data=fields,
timeout=30,
)
body = res.json()
if not res.ok or body.get("status") != "ok":
raise RuntimeError(f"convert failed ({res.status_code}): {body.get('error')}")
return body["data"]
data = convert_image("IMG_4211.HEIC", "jpg", quality=82, max_width=2048)
print(data["inputFormat"], "->", data["outputFormat"])
img = requests.get(data["downloadURL"], timeout=30)
img.raise_for_status()
with open("IMG_4211.jpg", "wb") as out:
out.write(img.content)Set an explicit timeout of around 30 seconds. Conversion is real work, and the platform documentation recommends that ceiling for rendering and extraction endpoints rather than the 10 to 15 seconds you would use for a lookup.
04Converting HEIC from phones
The most common reason anyone looks for a converter is an iPhone. Apple devices save photos as HEIC by default, and while iOS often converts to JPEG when sharing to a website, it does not always: native apps, some file pickers, and anything going through a cloud drive can hand you the original.
The robust approach is to normalise at the edge of your system. Every upload that arrives gets converted to one storage format, once, before anything else touches it. Downstream code (thumbnails, PDFs, emails, the admin panel) then only ever sees JPEG or WebP.
// npm install express@4 multer@1
import express from 'express';
import multer from 'multer';
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
const app = express();
const KEY = process.env.APIVERVE_API_KEY;
function needsConversion(file) {
const ext = file.originalname.split('.').pop().toLowerCase();
return ['heic', 'heif'].includes(ext) || file.mimetype === 'image/heic' || file.mimetype === 'image/heif';
}
app.post('/photos', upload.single('photo'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'no file' });
if (!needsConversion(req.file)) {
// Already a web format: store req.file.buffer as-is.
return res.json({ stored: req.file.originalname, converted: false });
}
const form = new FormData();
form.append('image', new Blob([req.file.buffer]), req.file.originalname);
form.append('outputFormat', 'jpg');
form.append('quality', '82');
form.append('maxWidth', '2560');
const r = await fetch('https://api.apiverve.com/v1/heicconverter', {
method: 'POST',
headers: { 'x-api-key': KEY },
body: form,
signal: AbortSignal.timeout(30_000),
});
const body = await r.json();
if (!r.ok) return res.status(502).json({ error: body.error ?? 'conversion failed' });
const jpg = Buffer.from(await (await fetch(body.data.downloadURL)).arrayBuffer());
// Save `jpg` to your own storage here (S3, GCS, disk), then record that path.
res.json({ stored: req.file.originalname.replace(/\.hei[cf]$/i, '.jpg'), converted: true, bytes: jpg.length });
});
app.listen(3000);A few decisions in that route are worth copying.
JPEG, not PNG, for photos. Converting a 3 MB HEIC photo to PNG can produce a file several times larger, because PNG stores every pixel losslessly, including the sensor noise. Quality 80 to 85 is visually indistinguishable from the original at normal viewing sizes.
Resize while you convert. maxWidth: 2560 costs nothing extra and stops a 4032-pixel original from being served to a 400-pixel avatar slot. Both maxWidth and maxHeight keep the aspect ratio, so you never get a squashed image.
Decide by extension and MIME type, but do not trust either completely. Browsers report HEIC inconsistently. If you get it wrong in the "already a web format" direction, your downstream code fails later; sending everything through imageconverter instead removes that branch at the cost of 10 credits per already-fine upload.
Your HEIC upload route works perfectly in development, then fails for most real users with a 400 naming a size limit. What is the most likely cause?
05Rasterising SVG for places that need pixels
SVG is the right master format for logos and icons, and the wrong delivery format surprisingly often: app stores want PNG icons at fixed sizes, many email clients will not display SVG, and Open Graph images must be raster.
There is also a security reason to rasterise. An SVG file is XML that a browser executes, and it can contain scripts and external references. If users can upload SVG avatars or logos and you serve those files from your own domain, you are serving them code. Converting user-supplied SVG to PNG on upload, and serving only the PNG, turns a document into pixels and closes that hole.
svgconverter accepts only SVG and writes png, jpg or webp. Use maxWidth to set the output size: vector input has no "real" pixel size, so pick the largest size you will display and let smaller sizes scale down from that.
The catalog also shows the vector-or-raster choice from the other side. Country Flags hands you a flag as either format. Ask for svg and you get a file that stays sharp at any size; omit format and you get PNG.
curl "https://api.apiverve.com/v1/countryflags?country=JP&format=svg" \
-H "x-api-key: YOUR_API_KEY"data.format is svg and downloadUrl ends in .svg. Choosing the format is a premium option; without it you get png.
If you are drawing a flag in a 24-pixel table cell, the PNG is simpler. If the same flag appears on a retina hero banner and in a printed PDF, the SVG is one file that serves both, and you rasterise it yourself only for destinations that cannot take vectors.
06Quality, size and the link that expires
quality (1 to 100, default 90) only matters for jpg and webp. Ninety is a conservative default that favours fidelity. For photos displayed on the web, 75 to 85 usually halves the file again with no difference anyone will notice. For product photos where customers zoom in, stay near 90. Do not reconvert a JPEG to JPEG repeatedly; each pass loses detail. Convert from the original every time you need a new size.
If you need to know how much a setting saves and outputSize is not on your plan, measure it yourself: the byte length of what you download from downloadURL is the real number.
Every successful conversion returns downloadURL and expires. The link is temporary. On these endpoints expires is a Unix timestamp in milliseconds, about two days after the call. Treat it as a pickup slip, not storage:
- Download the bytes immediately after the call returns.
- Write them to storage you own.
- Store your own URL in the database, never the
downloadURL.
A downloadURL saved to a database works in every test and then breaks every image in production two days later. That is the single most common bug in image pipelines built on any conversion service.
You store data.downloadURL in your products table as the image URL. What happens?
07What this costs and when to skip the API
Each conversion is 10 credits. That is cheap per image and adds up across a bulk backfill, so a few habits keep it sensible: convert once on upload, never on every request; key your stored output on the source file's hash so a re-upload of the same photo reuses the existing result; and skip the call entirely when the input is already in the target format and within the size you need.
Formats are now under control: you can take anything a phone or a designer sends you and turn it into what your pages, PDFs and emails need. Next you will go the other way and create images that never existed as files at all, by rendering HTML and Markdown into social cards and receipts: Render HTML and Markdown to images.