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.
Free tier: 3 conversions/day, 10 MB max — same quota across the web UI and the API. The API plan unlocks 10,000/month and 5 GB max file size.
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. We fetch 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://www.bulkconvert.io/api/v1/files/9e2f7a4c-3b1e-4d9f-a8c2-1f6d3b87e1ab",
"filename": "report.docx",
"size": 184320,
"job_id": "9e2f7a4c-3b1e-4d9f-a8c2-1f6d3b87e1ab",
"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 | The conversion engine returned an error mid-job. The `detail` field has a scrubbed upstream message. Retrying with the same source usually works. We don't bill failures — the usage counter rolls back. |
503 | conversion service unavailable | Only happens during a brief outage on the conversion backend. 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 conversion 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 — incoming jobs are queued by our conversion infrastructure. 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. We pull 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 conversion job. If a request fails with a 5xx, retry safely. Worst case you'll get a fresh job_idfor the second attempt and we don't bill for the failed one.
For long-tail flakiness — usually transient upstream 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_url points at bulkconvert.io/api/v1/files/<id>. It's a simple HTTPS URL — no auth header needed, just stream it down with the HTTP client of your choice. The URL is valid for 24 hours from when the conversion finishes. If you need to store the result long-term, copy it to your own bucket before then.
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.