# The Converterrr API

> Convert and compress files between 142 formats from your own code with one HTTP request. Send a URL or upload a file, then poll for the result or receive a signed webhook. Included in every paid plan.

URL: https://www.converterrr.com/developers

- Base URL: `https://www.converterrr.com/api/v1`
- OpenAPI 3.1 spec: https://www.converterrr.com/api/v1/openapi.json
- Endpoint reference: https://www.converterrr.com/developers/reference.md
- Access: The API is included in every paid plan only, from $14/month (Starter). Free accounts can't create API keys, and /api/v1 returns `402 plan_required` for accounts without an active paid plan.
- Coverage: 3,053 conversions (every conversion with a cloud route) plus compression. `GET /formats` lists them without authentication.
- Engines: FFmpeg, ImageMagick, Ghostscript, librsvg, Potrace, Poppler, LibreOffice, Pandoc, Calibre, 7-Zip, fontTools, each job in its own isolated, single-use sandbox.
- Automation: Automating the web converter (scripts, headless browsers, browser-automation tools, or calling the web app's internal /api endpoints) is not allowed and is blocked. Use the API at /api/v1 for automation.

## Quickstart

Create an API key in the [dashboard](https://www.converterrr.com/dashboard/api-keys) (requires a paid plan and a verified email), then create a job. The base URL is `https://www.converterrr.com/api/v1`.

```bash
curl https://www.converterrr.com/api/v1/jobs \
  -H "Authorization: Bearer $CONVERTERRR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "url": "https://example.com/slides.pptx" }, "output_format": "pdf" }'
```

```ts
const headers = { Authorization: `Bearer ${process.env.CONVERTERRR_API_KEY}` };

let job = await fetch("https://www.converterrr.com/api/v1/jobs", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ input: { url: "https://example.com/slides.pptx" }, output_format: "pdf" }),
}).then((r) => r.json());

while (job.status === "queued" || job.status === "processing") {
  await new Promise((r) => setTimeout(r, 2000));
  job = await fetch(`https://www.converterrr.com/api/v1/jobs/${job.id}`, { headers }).then((r) => r.json());
}

console.log(job.output?.download_url ?? job.error);
```

```python
import os, time, requests

headers = {"Authorization": f"Bearer {os.environ['CONVERTERRR_API_KEY']}"}
job = requests.post("https://www.converterrr.com/api/v1/jobs", headers=headers, json={
    "input": {"url": "https://example.com/slides.pptx"},
    "output_format": "pdf",
}).json()

while job["status"] in ("queued", "processing"):
    time.sleep(2)
    job = requests.get(f"https://www.converterrr.com/api/v1/jobs/{job['id']}", headers=headers).json()

print(job["output"]["download_url"] if job["output"] else job["error"])
```

A job moves from `queued` to `processing` to `completed` (or `failed`). Completed jobs include an `output.download_url` that stays valid until `expires_at`.

## Authentication

Send your key in the `Authorization` header as a bearer token (or in `x-api-key`). Keys start with `cvr_live_`, are shown once when created, and can be revoked at any time. Keep them server-side; never ship them in a browser or mobile app.

```http
Authorization: Bearer cvr_live_xxxxxxxxxxxxxxxxxxxxxxxx
```

API access requires a verified email address and an active paid plan. Requests without a valid key return `401 unauthorized`; accounts without a paid plan get `402 plan_required`.

## Uploading files

There are three ways to provide the input file:

| Input | Body | Use it for |
| --- | --- | --- |
| `input.url` | JSON | Any public http(s) URL, including presigned S3/GCS links. We download it. |
| `file` field | multipart/form-data | Small files sent together with `output_format` in a single request. |
| `input.upload_id` | JSON | Large files: create a presigned upload, PUT the bytes, then reference the upload. |

```bash
# 1. Ask for a presigned URL
curl https://www.converterrr.com/api/v1/uploads -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "filename": "interview.mov" }'
# → { "id": "upl_…", "upload_url": "https://…", "method": "PUT", "max_size": …, "expires_at": "…" }

# 2. Upload the raw bytes (any HTTP client works)
curl -X PUT --upload-file interview.mov "$UPLOAD_URL"

# 3. Convert it
curl https://www.converterrr.com/api/v1/jobs -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "input": { "upload_id": "upl_…" }, "output_format": "mp4" }'
```

## Compression

Send `"operation": "compress"` to make a file smaller in the same format (no `output_format` needed). Levels: `light`, `balanced`, `strong` (default `balanced`). Or pass `target_size` in bytes or as "500kb" / "2mb" (decimal units) to land at or just under a size; lossless formats can't hit a target. If the file is already as small as it gets, the original comes back unchanged.

Formats the API can compress: JPG, PNG, WebP, AVIF, GIF, TIFF, HEIC, MP4, MOV, MKV, WebM, AVI, MP3, M4A, AAC, OGG, Opus, FLAC, PDF, DOCX, PPTX, XLSX, ODT, ODP, ODS, EPUB, ZIP, 7Z, TAR.GZ, TAR.BZ2, TAR.XZ.

```bash
curl https://www.converterrr.com/api/v1/jobs \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "url": "https://example.com/report.pdf" }, "operation": "compress", "target_size": "2mb" }'
```

## Webhooks

Pass `webhook_url` when creating a job and we `POST` the job to it when it finishes or fails. Failed deliveries (network errors or 5xx responses) are retried with exponential backoff.

```json
{
  "type": "job.finished",
  "data": {
    "id": "cm3x9q2f80001",
    "status": "completed",
    "conversion": { "from": "pptx", "to": "pdf", "label": "PPTX → PDF" },
    "output": { "name": "slides.pdf", "size": 482113, "download_url": "https://…?download=1" },
    "credits": 2
  }
}
```

Every request carries a `Converterrr-Signature` header like `t=1730822400,v1=…`. Verify it with your webhook signing secret (shown on the API keys page): HMAC-SHA256 of `<t>.<raw body>`.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
```

## Errors

Errors use standard HTTP status codes and a consistent JSON body:

```json
{ "error": { "code": "insufficient_credits", "message": "You don't have enough credits for this conversion." } }
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | The body or parameters are invalid. The message names the field. |
| 400 | `invalid_url` | `input.url` isn't a public http(s) URL. |
| 400 | `unsupported_format` | Unknown format or file extension; pass `input_format` if it can't be detected. |
| 400 | `unsupported_conversion` | The pair has no cloud route (some conversions only run in the browser). |
| 400 | `unsupported_compression` | The format can't be compressed in the cloud. |
| 400 | `missing_output_format` | `output_format` is required for conversions. |
| 400 | `invalid_target_size` | target_size must be bytes or a size like "500kb" or "2mb". |
| 401 | `unauthorized` | Missing, invalid or revoked API key. |
| 402 | `plan_required` | The account has no active paid plan. The API is included in every paid plan. |
| 402 | `insufficient_credits` | Not enough credits for this job. |
| 403 | `email_unverified` | Verify your email address to use the API. |
| 404 | `not_found` | The job or upload doesn't exist (or isn't yours). |
| 409 | `upload_incomplete` | The presigned upload hasn't finished; PUT the file to `upload_url` first. |
| 409 | `in_progress` | `DELETE /jobs/:id` on a job that is already processing. |
| 413 | `file_too_large` | The file is larger than your plan allows. |
| 429 | `rate_limited` | Too many requests for this API key. Back off and retry. |
| 500 | `internal_error` | Something went wrong on our side. |
| 503 | `cloud_unavailable` | Cloud conversion is temporarily unavailable. |

## Credits and limits

| Plan | Price | Cloud credits a month | Largest file | Parallel jobs | Results kept |
| --- | --- | --- | --- | --- | --- |
| Starter | $14/month | 1,500 | 1 GB | 3 | 24 hours |
| Pro | $29/month | 5,000 | 4 GB | 10 | 3 days |
| Business | $79/month | 20,000 | 10 GB | 25 | 7 days |

- A cloud conversion (web or API) costs 1 credit for up to a minute of processing, plus 1 credit for each additional full minute. Heavier engines (LibreOffice, Calibre and Poppler: Office documents, eBooks, PDF to Word) cost 2 credits.
- Failed jobs are refunded automatically.
- Subscription credits reset at the start of each billing period. Package credits never expire and are used after subscription credits.
- Compression jobs cost 1 credit, plus 1 for each additional full minute of processing.
- Credit packages add credits that never expire, but API access comes with a subscription.
- Each API key is rate limited; a `429 rate_limited` response means back off and retry.
- Check remaining credits and limits with `GET /account`.

See [pricing](https://www.converterrr.com/pricing) for plans and credit packages.
