Concepts

Errors

Every error code, what triggers it, and how to handle it.

Every error response uses the same envelope:

{
  "error": {
    "code": "quota_exceeded",
    "message": "Monthly conversion quota exceeded.",
    "details": { "limit": 1000, "used": 1000 }
  }
}
FieldDescription
codeMachine-readable, stable across versions. Always present.
messageHuman-readable. May change wording over time — do not pattern-match.
detailsOptional structured context (e.g. limit, used, retry_after, route).

Error codes #

HTTPcodeMeaningRecovery
400bad_requestMissing field, malformed multipart, invalid JSON.Fix the request.
401invalid_api_keyHeader missing or revoked key.Re-issue from the dashboard.
404not_foundJob ID doesn't exist or has expired (7 day retention).
409bad_requestIdempotency-Key request is still in-flight.Retry after Retry-After.
413file_too_largeFile exceeds the active plan's max size.Upgrade or use POST /v1/jobs for >100 MB.
422unsupported_routeThis source→target combo isn't in our routing table.Check Formats.
422bad_requestSame Idempotency-Key reused with a different payload.Issue a new key.
429rate_limitedPer-minute throttle exceeded.Honor Retry-After.
429quota_exceededMonthly quota hit.Wait for month rollover or upgrade.
502service_unavailableConversion engine temporarily unavailable.Safe to retry with back-off.
401invalid_signatureStripe webhook signature mismatch (internal endpoint).

Branch on the HTTP status (or, when present, error.code):

Python
import requests, time

KEY = "ctf_sk_..."
H = {"Authorization": f"Bearer {KEY}"}

def convert(path: str, target: str) -> bytes:
    with open(path, "rb") as f:
        r = requests.post(
            "https://changethisfile.com/v1/convert",
            headers=H, files={"file": f}, data={"target": target},
        )
    if r.status_code == 200:
        return r.content
    err = (r.json().get("error") or {}) if r.headers.get("Content-Type", "").startswith("application/json") else {}
    code = err.get("code", "unknown")
    if code == "quota_exceeded":
        raise RuntimeError(f"Out of quota: {err.get('details')}")
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 60)))
        return convert(path, target)        # retry once
    if r.status_code == 413:
        raise RuntimeError("File too large for current plan")
    if r.status_code == 422:
        raise RuntimeError(f"Unsupported route: {err.get('message')}")
    if r.status_code in (502, 503):
        time.sleep(2)
        return convert(path, target)        # engine briefly down
    if r.status_code == 401:
        raise RuntimeError("Bad CTF API key")
    raise RuntimeError(f"[{code}] {err.get('message')} (CF-Ray={r.headers.get('CF-Ray')})")
JavaScript
async function convert(path, target) {
  const { readFileSync } = await import('node:fs');
  const form = new FormData();
  form.append('file', new Blob([readFileSync(path)]), path);
  form.append('target', target);

  const r = await fetch('https://changethisfile.com/v1/convert', {
    method: 'POST',
    headers: { Authorization: 'Bearer ctf_sk_...' },
    body: form,
  });
  if (r.ok) return new Uint8Array(await r.arrayBuffer());

  const err = (await r.json().catch(() => ({}))).error || {};
  if (err.code === 'quota_exceeded') throw new Error(`Out of quota: ${JSON.stringify(err.details)}`);
  if (r.status === 429) {
    await new Promise(rs => setTimeout(rs, (parseInt(r.headers.get('Retry-After')) || 60) * 1000));
    return convert(path, target);
  }
  if (r.status === 413) throw new Error('File too large');
  if (r.status === 422) throw new Error(`Unsupported route: ${err.message}`);
  if (r.status === 502 || r.status === 503) {
    await new Promise(rs => setTimeout(rs, 2000));
    return convert(path, target);
  }
  if (r.status === 401) throw new Error('Bad CTF API key');
  throw new Error(`[${err.code}] ${err.message} (CF-Ray=${r.headers.get('CF-Ray')})`);
}

Request IDs #

Every response includes either an X-Request-Id (synthetic) or a CF-Ray (Cloudflare-issued) header. Include it when contacting support — it lets us pinpoint your specific call in seconds.