Idempotency and retries

Networks fail. A request can time out on your side after the server has already run the model. The Idempotency-Key header lets you retry such a request without running the model twice and without paying twice.

The Idempotency-Key header

Send a unique key with each billable POST (/v1/deepfake/detect, /v1/speakers/enroll, /v1/speakers/verify). It is optional, and recommended.

Example (HTTP)
Idempotency-Key: 3f1c9a8e-4b2d-4c7a-9e51-0d6f2b8a7c13
  • 1 to 128 printable ASCII characters. A random UUID per logical request works well. Other values are rejected with 400 invalid_idempotency_key.
  • Generate the key once, before the first attempt, and reuse it for every retry of that same request.
  • Use a new key for each new request, even if the audio is the same.

A key is scoped to your account and the operation type, so the same key on /v1/deepfake/detect and /v1/speakers/verify refers to two separate records.

What the server does with a repeated key

The server stores a fingerprint of each request: the operation type, the normalised parameters (such as speaker_id and detect_deepfake) and a SHA-256 hash of the uploaded file.

Situation Response Charged
First request with this key Runs normally. 50 (or 100) on success
Same key, same request, previous attempt succeeded The stored result with "idempotent_replay": true, a new request_id, header Idempotent-Replayed: true and "credits": {"charged": 0, "balance": <current balance>, "original_charge": 50}. The model does not run again. 0
Same key, same request, previous attempt still running 409 operation_in_progress, with the operation_id to poll. 0
Same key, same request, previous attempt failed (and was not charged) Runs again under the same record. 50 (or 100) on success
Same key, different request (other file or parameters) 422 idempotency_key_reuse. 0

Keys are kept for 24 hours for replay. Stored results stay available with GET /v1/operations/{operation_id} for 30 days.

In a replay, credits.charged is 0 because nothing was charged for this request, credits.balance is your balance now, and credits.original_charge is what the original request was charged (50, or 100 for a verification with detect_deepfake=true). Example:

Example (JSON)
{
  "request_id": "req_5e0b7c2a91d4f3e8a6b1c0d9",
  "operation_id": "op_9a0f3b6c1e72d8e6725b71fcb5f62c89",
  "object": "deepfake.detection",
  "result": {
    "label": "genuine",
    "is_synthetic": false,
    "score": 0.08,
    "threshold": 0.62,
    "score_semantics": "model output probability of synthetic speech; not calibrated to your traffic",
    "duration_seconds": 3.0,
    "segments": [{ "start": 0.0, "end": 3.0, "score": 0.08 }]
  },
  "model": { "name": "aletheia-wavlm", "version": "wavlm-finetune-n-m43-moredata-ep063" },
  "credits": { "charged": 0, "balance": 99850, "original_charge": 50 },
  "latency_ms": 412.6,
  "idempotent_replay": true
}

Which errors to retry

Response Retry?
Network error or client timeout, no response Yes, with the same Idempotency-Key.
429 rate_limited, 429 concurrency_limited Yes, after Retry-After seconds.
503 inference_overloaded Yes, after Retry-After seconds.
502 inference_failed, 504 inference_timeout, 500 internal Yes, with backoff. Stop after a few attempts.
409 operation_in_progress Do not resend. Poll GET /v1/operations/{operation_id} or wait and retry with the same key.
400, 401, 403, 404, 413, 415, 422 No. Fix the request first.
402 insufficient_credits No. Retrying will not succeed until credits are available.

Use exponential backoff with jitter, honour Retry-After when present, and cap the number of attempts.

Retry helpers

cURL retries 429, 500, 502, 503 and 504 with --retry, and honours Retry-After. Fix the key in a variable so every attempt sends the same one:

Example (cURL)
KEY=$(uuidgen)
curl https://platform.detectif.ai/v1/deepfake/detect \
  --retry 4 --retry-max-time 120 \
  -H "Authorization: Bearer $DETECTIF_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -F "file=@call-recording.wav"

Polling a running operation

409 operation_in_progress means an earlier attempt with the same key is still running. The error body carries its operation_id next to error. Poll the operation instead of sending the audio again. Polling is free.

GET /v1/operations/{operation_id} returns object: "operation" with type, status (pending, reserved, running, succeeded or failed), source (api, dashboard or mcp), credits_charged, created_at, completed_at, the stored result (the same JSON as the original response, kept 30 days) and, for a failed operation, an error object.

Example (cURL)
curl https://platform.detectif.ai/v1/operations/op_9a0f3b6c1e72d8e6725b71fcb5f62c89 \
  -H "Authorization: Bearer $DETECTIF_API_KEY"