API reference · v1
Bulk Convert HTTP API
Convert files programmatically with a single POST. Send a URL to the source file plus the target format; receive a signed download URL. No SDK required.
Quickstart
The API has one endpoint: POST /api/v1/convert. Authenticate with a bearer token, send a JSON body with two fields, and receive a signed URL pointing at the converted file. A typical conversion finishes in 2–10 seconds; the request blocks until the file is ready (up to 3 minutes), so you don't need to poll anything yourself.
curl -X POST https://bulkconvert.io/api/v1/convert \
-H "Authorization: Bearer bc_live_xxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://example.com/report.pdf",
"to": "docx"
}'source_url. CloudConvert pulls the file server-side, so local paths, localhost, and credentials-required URLs won't work. Signed S3 / R2 / Vercel Blob URLs are ideal.Authentication
Every request needs an Authorization: Bearer <api_key> header. API keys look like bc_live_ followed by 26 random characters.
Keys are emailed to you the moment your API-tier Stripe subscription activates. Cancelling the subscription revokes the key within seconds— every request re-checks the key against our KV store, so there's no grace period and no caching. Treat the key like a password: store it as an environment variable, never commit it.
Pro and Bulk plans also produce bc_live_ keys, but those work only on the web converter. Calling /api/v1/convert with a Pro or Bulk key returns 403 — API access requires the API plan.
Base URL & versioning
https://bulkconvert.io/api/v1
The /v1 in the path is the API contract version. We version on breaking changes only. Any new optional field — or a new conversion pair — is added without bumping the version. When we do ship a v2, v1 stays live for at least 12 months and you get an email 90 days before sunset.
POST /api/v1/convert
Synchronous conversion. Blocks until the file is ready.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
source_url | string | yes | Publicly fetchable URL of the source file. Must end in a file extension (or include one in the path) so we can detect the source format. |
to | string | yes | Target format, lowercase, no dot. e.g. "docx", "mp3", "webp". See Supported conversions. |
Success response — 200
{
"ok": true,
"download_url": "https://eu-central.storage.cloudconvert.com/tasks/.../report.docx?...",
"filename": "report.docx",
"size": 184320,
"job_id": "b4d8ded1-4b10-4956-a5c3-83732c29459d",
"tier": "api",
"usage": {
"used": 385,
"quota": 10000,
"remaining": 9615,
"reset_at": "2026-06-01T00:00:00.000Z"
}
}| Field | Description |
|---|---|
download_url | Signed URL to the converted file. Expires after 24 hours. |
filename | Suggested filename for the output. |
size | Output file size in bytes. |
job_id | Internal job ID — quote this if you open a support ticket. |
tier | Always "api" — confirms the key tier that processed this request. |
Error response
{
"error": "conversion pdf → xyz not supported"
}Errors always include an error string. Some also include detail with the upstream message, or hints like upgrade_url. The HTTP status is meaningful — see the table.
| Status | error | When this happens |
|---|---|---|
400 | invalid json body | Request body didn't parse as JSON. |
400 | source_url and `to` are required | Missing one of the required fields. |
400 | couldn't detect source format from URL | The URL doesn't end in a file extension. Append `.pdf`, `.mp4` etc. or use a `?ext=pdf` style query string the server can strip. |
400 | conversion <from> → <to> not supported | The {from, to} pair isn't in the catalog. See Supported conversions below. |
401 | missing Authorization: Bearer <api_key> | No bearer token in the request. |
401 | invalid or revoked api key | The key was never valid, or the subscription was cancelled. |
402 | monthly quota exhausted | You've used your tier's monthly conversion allowance. Response body contains `used`, `quota`, and `reset_at`. Counter resets at 00:00 UTC on the 1st. |
403 | API access requires the API plan | Your key is valid but on a Pro/Bulk tier. Upgrade at /pricing#api. |
502 | conversion failed | CloudConvert returned an error mid-job. The `detail` field has the upstream message; retrying with the same source usually works. We don't bill failures — the usage counter rolls back. |
503 | conversion backend not yet configured | Only happens during a brief outage if our CloudConvert key is missing or revoked. Retry in a minute. |
Examples
cURL
curl -X POST https://bulkconvert.io/api/v1/convert \
-H "Authorization: Bearer bc_live_xxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"source_url": "https://example.com/report.pdf",
"to": "docx"
}'Node.js
// Node 18+ (built-in fetch)
const res = await fetch("https://bulkconvert.io/api/v1/convert", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BULK_CONVERT_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
source_url: "https://example.com/report.pdf",
to: "docx",
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(`${res.status}: ${err.error}`);
}
const { download_url, filename, size, job_id } = await res.json();
console.log(`Converted ${filename} (${size} bytes) — job ${job_id}`);
console.log("Download from:", download_url);Python
import os
import requests
resp = requests.post(
"https://bulkconvert.io/api/v1/convert",
headers={"Authorization": f"Bearer {os.environ['BULK_CONVERT_KEY']}"},
json={
"source_url": "https://example.com/report.pdf",
"to": "docx",
},
timeout=300, # conversions can take up to 3 minutes
)
resp.raise_for_status()
data = resp.json()
print(f"Converted {data['filename']} ({data['size']} bytes)")
print("Download:", data["download_url"])GET /api/v1/usage
Read the calling key's current-month conversion count without performing a conversion. Free to hit as often as you want — no rate limit, no CloudConvert call.
curl https://bulkconvert.io/api/v1/usage \
-H "Authorization: Bearer bc_live_xxxxxxxxxxxxxxxxxxxxxxxxxx"Response
{
"tier": "api",
"used": 384,
"quota": 10000,
"remaining": 9616,
"reset_at": "2026-06-01T00:00:00.000Z",
"key_prefix": "bc_live_xxxxxx"
}| Field | Description |
|---|---|
tier | Your subscription tier — pro, bulk, or api. |
used | Conversions consumed this UTC month. |
quota | Monthly cap, or null when the tier is unlimited (Bulk). |
remaining | max(0, quota − used), or null when quota is null. |
reset_at | ISO timestamp of the next month boundary (UTC). Counter zeros at that moment. |
key_prefix | First 16 chars of your key — handy for support tickets without leaking the secret. |
POST /api/v1/convert response also includes the same usageobject inline, so most clients won't need to call /usage at all — only hit it for dashboards or pre-flight checks.Supported conversions
33 conversion pairs across six categories. Pass the to column as the to field in your request; the source format is detected from the source_url extension.
document
| from | to | Description |
|---|---|---|
pdf | docx | PDF → Word |
docx | pdf | Word → PDF |
pdf | html | PDF → HTML |
html | pdf | HTML → PDF |
pdf | md | PDF → Markdown |
md | html | Markdown → HTML |
md | pdf | Markdown → PDF |
html | md | HTML → Markdown |
csv | xlsx | CSV → Excel |
xlsx | csv | Excel → CSV |
csv | json | CSV → JSON |
audio
| from | to | Description |
|---|---|---|
mp3 | wav | MP3 → WAV |
wav | mp3 | WAV → MP3 |
mp3 | m4a | MP3 → M4A |
mp3 | flac | MP3 → FLAC |
flac | mp3 | FLAC → MP3 |
mp4 | mp3 | MP4 (audio) → MP3 |
video
| from | to | Description |
|---|---|---|
mp4 | mov | MP4 → MOV |
mov | mp4 | MOV → MP4 |
mp4 | webm | MP4 → WebM |
webm | mp4 | WebM → MP4 |
mp4 | gif | MP4 → GIF |
image
| from | to | Description |
|---|---|---|
png | jpg | PNG → JPG |
jpg | png | JPG → PNG |
png | webp | PNG → WebP |
webp | png | WebP → PNG |
heic | jpg | HEIC → JPG |
png | svg | PNG → SVG |
archive
| from | to | Description |
|---|---|---|
zip | tar | ZIP → TAR |
tar | zip | TAR → ZIP |
7z | zip | 7Z → ZIP |
ebook
| from | to | Description |
|---|---|---|
epub | pdf | EPUB → PDF |
epub | mobi | EPUB → MOBI |
Limits & quotas
Per request
- Max file size: 5 GB on the API tier
- Max conversion time: 3 minutes. After that the request returns 502 and you can retry.
- Download URL lifetime: 24 hours from when the job finishes. Mirror to your own storage if you need it longer.
Concurrency
No per-account concurrency cap on the API tier — we forward to CloudConvert and they queue. In practice expect ~10 simultaneous jobs to all start processing immediately.
Per tier — all plans
| Tier | Price | Conversions / day | Max file size | API access |
|---|---|---|---|---|
| Free | Free | 3 | 10 MB | No |
| Pro | €9/mo | 100 | 100 MB | No |
| Bulk | €19/mo | Unlimited | 1 GB | No |
| API | €49/mo | Unlimited | 5 GB | Yes |
Best practices
Hosting the source file
The fastest pattern is to upload to S3 / R2 / Vercel Blob with a short-lived signed GET URL (5–15 minutes is plenty), then send that URL as source_url. CloudConvert pulls it once, within the request, so the signed URL only needs to outlive that single fetch.
If the source URL doesn't have a file extension at the end of the path, the request will fail with a 400. The cleanest fix is to include the extension in your signed-URL filename — e.g. https://your-bucket/abc123.pdf?X-Amz-.... Any query string after the extension is fine; we strip it before detection.
Idempotency & retries
Conversions aren't idempotent — each call creates a new CloudConvert job and costs credits. If a request fails with a 5xx, retry safely; CloudConvert deduplicates upload-only failures on their side, so worst case you'll get a fresh job_id for the second attempt.
For long-tail flakiness — usually transient CloudConvert engine errors — a single retry with the same body fixes the problem 95% of the time. We recommend at most 3 retries with exponential backoff (1s, 4s, 16s).
Downloading the result
The download_urlpoints at CloudConvert's object storage, not at us. It's a regular signed HTTPS URL — no auth header needed, just stream it down with the HTTP client of your choice. If you need to store it long-term, copy it to your own bucket before the 24 h expiry.
Webhooks (not yet available)
We don't support webhook callbacks yet. The endpoint is synchronous — the response only returns once the file is ready. For very large files (1 GB+), set your HTTP client's read timeout to at least 300 seconds.
Help & support
Email support@bulkconvert.io with your job_id for the failing request. Replies usually come within one business day. Status page and changelog are coming soon.