Concepts

Idempotency

Make conversions safe to retry without duplicating charges.

Conversions can be expensive — both for our infrastructure and your quota. Network failures and client retries can accidentally double-charge your account. The Idempotency-Key header solves this by letting you mark each logical request with a unique ID we'll cache for 24 hours.

How it works #

  1. Generate a unique key per logical request — typically a UUID or a hash of (customer_id, request_id).
  2. Pass it as the Idempotency-Key header on POST /v1/convert or POST /v1/jobs.
  3. If the request succeeds, we cache the response for 24 hours.
  4. If you retry with the same key + same body, we replay the cached response.
  5. If you retry with the same key but different body, we return 422.
  6. If the original is still in flight, we return 409 Conflict.
curl -X POST https://changethisfile.com/v1/convert \
     -H "Authorization: Bearer ctf_sk_..." \
     -H "Idempotency-Key: cust-123-attempt-1" \
     -F "file=@photo.png" \
     -F "target=jpg" \
     --output photo.jpg
ConstraintLimit
Max key length128 chars
Allowed charactersA-Z a-z 0-9 . _ - : + /
Replay window24 hours

Code samples #

KEY=$(uuidgen)
for attempt in 1 2 3; do
  curl -fsS -X POST https://changethisfile.com/v1/convert \
       -H "Authorization: Bearer ctf_sk_..." \
       -H "Idempotency-Key: $KEY" \
       -F "file=@photo.png" \
       -F "target=jpg" \
       --output photo.jpg && break
  sleep $((attempt * 2))
done
import requests, time, uuid

KEY = str(uuid.uuid4())
H = {"Authorization": "Bearer ctf_sk_...", "Idempotency-Key": KEY}

for attempt in range(3):
    with open("photo.png", "rb") as f:
        r = requests.post(
            "https://changethisfile.com/v1/convert",
            headers=H, files={"file": f}, data={"target": "jpg"},
        )
    if r.ok:
        open("photo.jpg", "wb").write(r.content)
        break
    if r.status_code == 429:
        time.sleep(int(r.headers.get("Retry-After", 60)))
        continue
    raise RuntimeError(r.text)
import { readFileSync, writeFileSync } from 'node:fs';

const KEY = crypto.randomUUID();
const H = { Authorization: 'Bearer ctf_sk_...', 'Idempotency-Key': KEY };

for (let attempt = 0; attempt < 3; attempt++) {
  const form = new FormData();
  form.append('file', new Blob([readFileSync('photo.png')]), 'photo.png');
  form.append('target', 'jpg');

  const r = await fetch('https://changethisfile.com/v1/convert',
    { method: 'POST', headers: H, body: form });
  if (r.ok) {
    writeFileSync('photo.jpg', Buffer.from(await r.arrayBuffer()));
    break;
  }
  if (r.status === 429) {
    await new Promise(rs => setTimeout(rs, (parseInt(r.headers.get('Retry-After')) || 60) * 1000));
    continue;
  }
  throw new Error(await r.text());
}

Replay vs retry — what counts as the "same body"? #

We hash a stable fingerprint of your request to detect mismatches:

  • JSON requests: SHA-256 of the raw body.
  • Multipart uploads: SHA-256 of (path, content-type, content-length). We deliberately don't hash file bytes — that would force buffering the entire upload, defeating the streaming model.

Practical implication: if you reuse a key with the same source / target and a file of the same byte length, we treat it as a replay. If you swap the file for one with a different size, we return 422.

Replayed responses #

When we replay a stored response, we add an Idempotent-Replayed: true header so you can distinguish a fresh result from a cache hit.

HTTP/1.1 200 OK
Content-Type: image/jpeg
Idempotent-Replayed: true
…

For binary /v1/convert responses we can't replay the converted bytes (some files are gigabytes — too expensive to cache). A replay of a binary response returns a JSON sentinel pointing you to re-issue without the key. For /v1/jobs (always JSON) replays are byte-perfect.

Errors #

HTTPWhen it fires
400 Bad RequestKey violates the format constraints (length, charset).
409 ConflictA request with this key is still in-flight. Retry after a few seconds.
422 Unprocessable EntitySame key, different body. Issue a new key.

When to use it #

  • Always for any non-idempotent retry path (e.g. queue workers, webhooks back to your service).
  • Always for billed customers who'd be charged twice on a duplicate conversion.
  • Skip for casual one-off conversions where double-charging isn't a concern.