Idempotency: retries never double-bill
Send an Idempotency-Key header on a detection and the API remembers the outcome under that key. A retry with the same key and the same payload returns the stored result, with the same id, and is not metered again. Timeouts, dropped connections and crashed batch runs become safe to resume.
How it works
- The key is any string up to 255 characters, unique per API key and endpoint. Derive it from your own identity of the image (camera id plus file name, an import row id) rather than a random value, so a resumed run reuses the same keys.
- The first request with a key runs normally and stores its result. Every later request with the same key and an identical payload (same image bytes and parameters) returns that result: HTTP 200 with the original body. While the first request is still running a replay answers 202 with the request state.
- The same key with a different payload answers
409 IDEMPOTENCY_CONFLICT. That is a bug in your key derivation; do not retry it. - Replays are free: a stored result is served without a model run, without drawing from the free quota and without a metered image.
- Replay window: 365 days, the same window as
GET /detect/{id}. A key that refers to an expired request answers 409 with the expiry indetails; use a new key. - A failed first request (for example a 503 from the model) is stored too: the replay returns the stored error with status 200 and
status: "error". Send a new key to run the image again.
Per endpoint
POST /detect, /detect-urban, /filter, /filter-urbanYesPOST /detect-urlYesPOST /scan-image-text, /scan-image-text-urlYesPOST /jobsYes (recommended for every job)POST /jobs/{id}/submitNot needed: submitting twice returns the current job stateGET endpointsNot needed: reads are safe to repeatExamples
cURL
curl -X POST "https://api.animaldetect.com/v1/detect" \
-H "Authorization: Bearer $ANIMAL_DETECT_API_KEY" \
-H "User-Agent: MyApp/1.0 (ops@example.com)" \
-H "Idempotency-Key: camera-a/2026-06-17/img_001" \
-F "image=@img_001.jpg" \
-F "threshold=0.2"Python
import os
import uuid
import requests
# Derive the key from YOUR identity of the image, so a crashed run can
# resume with the same keys and never pays twice.
idempotency_key = "camera-a/2026-06-17/img_001"
response = requests.post(
"https://api.animaldetect.com/v1/detect",
headers={
"Authorization": "Bearer " + os.environ["ANIMAL_DETECT_API_KEY"],
"User-Agent": "MyApp/1.0 (ops@example.com)",
"Idempotency-Key": idempotency_key,
},
files={"image": open("img_001.jpg", "rb")},
data={"threshold": "0.2"},
)
if response.status_code == 409:
# Same key, different payload: fix the key derivation, do not retry.
raise RuntimeError(response.json()["error"])
response.raise_for_status()
result = response.json() # identical on every replay, same "id"409 on a payload mismatch
{
"error": "Idempotency-Key was already used with a different request payload.",
"code": "IDEMPOTENCY_CONFLICT",
"details": {
"request_id": "1d6492ab-f29e-480e-ae56-2133f3fb5429",
"endpoint": "/api/v1/detect"
},
"type": "idempotency_error",
"request_id": "7c1b0d2e-5e5a-4d0b-9f0e-3f9a4c1e8b21",
"doc_url": "https://www.animaldetect.com/doc/api/errors#IDEMPOTENCY_CONFLICT"
}Related
Every response carries an X-Request-Id; see Errors and request ids. For high-volume pipelines, Detection jobs take the same header on job creation.