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 #
- Generate a unique key per logical request — typically a UUID or a hash of
(customer_id, request_id). - Pass it as the
Idempotency-Keyheader onPOST /v1/convertorPOST /v1/jobs. - If the request succeeds, we cache the response for 24 hours.
- If you retry with the same key + same body, we replay the cached response.
- If you retry with the same key but different body, we return
422. - 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| Constraint | Limit |
|---|---|
| Max key length | 128 chars |
| Allowed characters | A-Z a-z 0-9 . _ - : + / |
| Replay window | 24 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 #
| HTTP | When it fires |
|---|---|
400 Bad Request | Key violates the format constraints (length, charset). |
409 Conflict | A request with this key is still in-flight. Retry after a few seconds. |
422 Unprocessable Entity | Same 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.