Overview
Every request is JSON over HTTPS. Bytes never pass through our control plane: sources upload straight to storage with a presigned URL, results come back as a short-lived signed link. The API is included with the AI Pro and Business plans.
Authentication
Authenticate with an API key in the Authorization header. Create and revoke keys on your account page. The full key is shown once, store it securely; we keep only a hash. Use bm_test_… keys for integration and bm_live_… for production.
Authorization: Bearer bm_live_xxxxxxxxxxxxxxxxxxxxxxxxQuickstart
Create a job and get the result. Fast tools finish inline when you send Prefer: wait; otherwise poll the job id.
curl -X POST https://api.bitmorph.io/v1/jobs \
-H "Authorization: Bearer $BITMORPH_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: wait=20" \
-d '{
"tool": "text-to-qr",
"input": { "type": "inline", "data": "https://bitmorph.io" }
}'
# → { "id": "job_…", "status": "completed",
# "output": { "url": "https://…signed", "content_type": "image/png", ... } }Jobs
A job runs one tool on one input. It resolves to queued → processing → completed / failed.
/jobs, create a job| tool | string | The conversion slug, e.g. "pdf-to-word". See GET /tools. |
| input | object | One of: {type:"upload", file}, {type:"url", url}, {type:"inline", data}, or {type:"multi", files:[]}. |
| params | object? | Tool-specific options (e.g. OCR language). See each tool's contract. |
Headers: Prefer: wait=<0–55> long-polls for a fast job; Idempotency-Key makes retries safe.
/jobs/{id}, retrieve a job (poll for the result)/jobs, list your recent jobs (cursor-paginated)/jobs/{id}, cancel a still-queued job (best-effort)# 1. presign an upload, PUT the bytes (see Uploads), then reference the handle:
curl -X POST https://api.bitmorph.io/v1/jobs \
-H "Authorization: Bearer $BITMORPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "tool": "pdf-to-word", "input": { "type": "upload", "file": "file_…" } }'
# → 202 { "id": "job_abc", "status": "queued" }
# 2. poll until done
curl https://api.bitmorph.io/v1/jobs/job_abc -H "Authorization: Bearer $BITMORPH_API_KEY"Uploads
For files, mint a presigned upload, PUT the bytes straight to storage, then reference the returned file handle in a job. Nothing transits our servers.
/uploads, create a presigned upload target# 1. get a presigned PUT
curl -X POST https://api.bitmorph.io/v1/uploads \
-H "Authorization: Bearer $BITMORPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "filename": "report.pdf", "size": 482113 }'
# → { "file": "file_…", "upload_url": "https://…", "method": "PUT" }
# 2. PUT the bytes to upload_url
curl -X PUT "$UPLOAD_URL" --data-binary @report.pdf
# 3. reference "file" in POST /jobs (input.type = "upload")Merge and other multi-input tools take several handles: { "type": "multi", "files": ["file_a", "file_b"] }.
Tools
150 conversions are available. Each tool declares its input mode, params, and output type, fetch the catalogue to discover them.
/tools, list every tool (public)/tools/{slug}, one tool’s contract (params, input mode, output)curl https://api.bitmorph.io/v1/toolsWebhooks
Register an endpoint to receive job.completed and job.failed events instead of polling. Each delivery is signed so you can verify it came from BitMorph.
/webhook_endpoints, register an endpoint (secret returned once)/webhook_endpoints, list your endpoints/webhook_endpoints/{id}, delete an endpointVerifying a signature
The Bitmorph-Signature header is t=<unix>,v1=<hmac>. Recompute the HMAC over {t}.{raw_body} with your endpoint secret and compare.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(t + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Errors
Errors use a stable envelope with a request_id for support.
{ "error": {
"type": "unprocessable",
"code": "wrong_password",
"message": "That password didn't open the file.",
"request_id": "req_…"
} }| 400 | invalid_request | Malformed request or bad parameters. |
| 401 | authentication | Missing, invalid, or revoked API key. |
| 402 | quota | Monthly conversion / AI / video allowance exhausted. |
| 403 | permission | Key lacks the scope, or the plan doesn't include the API. |
| 404 | not_found | No such job, tool, or endpoint. |
| 413 | invalid_request | File larger than your plan allows. |
| 422 | unprocessable | The file couldn't be processed (wrong password, corrupt, no text). |
| 429 | rate_limit | Per-minute rate limit or in-flight concurrency limit hit. |
| 5xx | engine / unavailable | A transient engine or service error, safe to retry. |
Rate limits & quotas
Three independent limits keep the platform fair. Every response carries X-RateLimit-* headers.
| Rate limit | per minute | Burst protection per key. Over limit → 429 with Retry-After. |
| Concurrency | in-flight | Max simultaneous jobs per key. Over limit → 429. |
| Monthly allowance | conversions | Your plan's included API conversions. Exhausted → 402. Heavy tools (OCR, large PDF→Office, ebook) count as more than one. |
Ordinary conversions are unlimited in the app; the API meters conversions so machine-paced volume stays fair. AI and video also draw your monthly AI-page and video-minute allowances.
Ready to build?
Grab a key on your account page, or read the machine-readable OpenAPI spec.
