Errors
Every response that is not 2xx uses the same JSON envelope, on the public API and the dashboard API alike:
{
"error": {
"type": "unprocessable_audio",
"code": "insufficient_speech",
"message": "At least 3 seconds of speech are required (detected 1.8 s).",
"param": "file"
},
"request_id": "req_6d2e9b0a14f798e4eef1697e"
}| Field | Description |
|---|---|
error.type |
A broad category. Use it for general handling. |
error.code |
A specific snake_case code. Use it for program logic. New codes may be added; treat unknown codes by their type. |
error.message |
A human-readable explanation. Do not parse it; the wording can change. |
error.param |
The request field the error relates to, such as file or speaker_id, or null. |
request_id |
Identifier of the request. Also sent in the X-Request-Id header on every response, successful or not. Include it when you contact support. |
Failed requests are never charged. See Credits and billing.
Error types and HTTP status codes
error.type |
HTTP | Meaning |
|---|---|---|
invalid_request |
400 | A field is missing, malformed or out of range. |
authentication |
401 | The API key is missing, invalid or revoked. |
insufficient_credits |
402 | Not enough available credits. |
permission |
403 | The key lacks a scope, or the account cannot use this feature yet. |
not_found |
404 | The resource does not exist in your account. |
conflict |
409 | The request conflicts with the current state, such as an operation still running. |
payload_too_large |
413 | The upload is larger than the limit. |
unsupported_media |
415 | The file format is not supported. |
unprocessable_audio |
422 | The audio was decoded but cannot be used (length, silence, speech). |
rate_limited |
429 | Too many requests. Wait for Retry-After seconds. |
internal |
500, 502, 504 | A server-side failure, including model failure or timeout. |
overloaded |
503 | The model service is at capacity. Wait for Retry-After seconds. |
Error codes
error.code |
error.type |
HTTP | Meaning | Retry |
|---|---|---|---|---|
missing_api_key |
authentication |
401 | No Authorization: Bearer header was sent. |
No |
invalid_api_key |
authentication |
401 | The key or the Authorization header is malformed, or the key does not exist. |
No |
api_key_in_url |
invalid_request |
400 | An api_key query parameter was sent. Keys go only in the Authorization header. Rotate a key that was sent in a URL. |
No |
revoked_api_key |
authentication |
401 | The key was revoked or rotated out. | No |
insufficient_scope |
permission |
403 | The key does not have a scope this request needs. The message names the missing scopes. | No |
email_not_verified |
permission |
403 | The account's email address is not verified. | No |
account_suspended |
permission |
403 | The account is suspended. | No |
account_deleted |
permission |
403 | The account was deleted. | No |
session_required |
permission |
403 | The endpoint is only available to signed-in dashboard sessions, not API keys. | No |
invalid_speaker_id |
invalid_request |
400 | speaker_id does not match ^[A-Za-z0-9_.:-]{1,64}$. |
No |
invalid_limit |
invalid_request |
400 | limit is not an integer in the allowed range. |
No |
invalid_cursor |
invalid_request |
400 | The pagination cursor is not one the API returned. |
No |
invalid_display_name |
invalid_request |
400 | display_name is longer than 128 characters. |
No |
missing_file |
invalid_request |
400 | The multipart body has no file field. |
No |
invalid_multipart |
invalid_request |
400 | The multipart body could not be parsed. | No |
unknown_parameter, duplicate_parameter, invalid_parameter |
invalid_request |
400 | A form field is not accepted by this endpoint, appears twice, or has an invalid value (for example detect_deepfake that is not true or false). |
No |
unsupported_content_type |
unsupported_media |
415 | The request is not multipart/form-data. |
No |
request_too_large |
payload_too_large |
413 | A request body other than an upload is too large. | No |
invalid_idempotency_key |
invalid_request |
400 | Idempotency-Key is not 1 to 128 printable ASCII characters. |
No |
invalid_operation, invalid_status |
invalid_request |
400 | A GET /v1/usage filter has an unknown value. |
No |
speaker_not_found |
not_found |
404 | The speaker_id is not enrolled in your account. |
No |
operation_not_found |
not_found |
404 | The operation_id does not exist in your account. |
No |
operation_in_progress |
conflict |
409 | A request with the same Idempotency-Key is still running. The body includes its operation_id; poll it. |
Later |
operation_superseded |
conflict |
409 | A newer attempt with the same Idempotency-Key took over this operation. Use the newer attempt's result. |
No |
idempotency_result_unavailable |
conflict |
409 | The stored result for this Idempotency-Key is no longer available. Use a new key. |
No |
too_many_samples |
conflict |
409 | The speaker already has the maximum of 5 active samples. | No |
speaker_reenrollment_required |
conflict |
409 | The speaker has no samples for the current speaker model version. Enroll again. | No |
idempotency_key_reuse |
invalid_request |
422 | The Idempotency-Key was already used for a different request. |
No |
insufficient_credits |
insufficient_credits |
402 | Available credits are below the cost of the request. | No |
file_too_large |
payload_too_large |
413 | The upload is over 20 MB. | No |
unsupported_format |
unsupported_media |
415 | The file is not an audio format the server can decode. | No |
corrupt_audio |
unprocessable_audio |
422 | The file is empty, truncated or could not be decoded. | No |
audio_decode_timeout |
unprocessable_audio |
422 | Decoding took longer than 20 seconds. Send a shorter or simpler file. | No |
audio_too_short |
unprocessable_audio |
422 | The decoded audio is shorter than the minimum for this operation. | No |
audio_too_long |
unprocessable_audio |
422 | The decoded audio is longer than the maximum for this operation. | No |
silent_audio |
unprocessable_audio |
422 | The recording contains no signal. | No |
insufficient_speech |
unprocessable_audio |
422 | Too little speech was detected (3 s to enroll, 2 s to verify). | No |
rate_limited |
rate_limited |
429 | Too many requests for this key, account or IP address. | After Retry-After |
concurrency_limited |
rate_limited |
429 | Too many model operations running at once for this account. | After Retry-After |
inference_overloaded |
overloaded |
503 | The model queue is full. | After Retry-After |
inference_failed |
internal |
502 | The model service returned an error. | Yes, with backoff |
inference_timeout |
internal |
504 | The model did not finish in time. | Yes, with backoff |
operation_interrupted |
internal |
500 | The operation was interrupted, for example by a server restart. Nothing was charged. | Yes, with backoff |
internal_error |
internal |
500 | An unexpected server error. Nothing was charged. | Yes, with backoff |
Other malformed requests return 400 with type invalid_request, a specific error.code, and error.param naming the field.
Some errors add fields next to error. 402 insufficient_credits includes the amounts:
{
"error": {
"type": "insufficient_credits",
"code": "insufficient_credits",
"message": "This operation needs 100 credits but only 50 are available.",
"param": null
},
"request_id": "req_a81c0f3e5b2734048a57188e",
"credits": { "available": 50, "required": 100 }
}Handling errors
# -s hides progress, -w prints the status code after the body
curl -s -w "\nHTTP %{http_code}\n" https://platform.detectif.ai/v1/speakers/unknown-id \
-H "Authorization: Bearer $DETECTIF_API_KEY"import os
import requests
class DetectifError(Exception):
def __init__(self, status, body):
err = body.get("error", {})
self.status = status
self.type = err.get("type")
self.code = err.get("code")
self.param = err.get("param")
self.request_id = body.get("request_id")
super().__init__(f"{status} {self.code}: {err.get('message')} (request {self.request_id})")
def call(method, path, **kwargs):
resp = requests.request(
method,
f"https://platform.detectif.ai/v1{path}",
headers={"Authorization": f"Bearer {os.environ['DETECTIF_API_KEY']}"},
timeout=60,
**kwargs,
)
body = resp.json()
if not resp.ok:
raise DetectifError(resp.status_code, body)
return body
try:
call("GET", "/speakers/unknown-id")
except DetectifError as e:
if e.code == "speaker_not_found":
print("Not enrolled yet. Request ID:", e.request_id)
else:
raiseclass DetectifError extends Error {
constructor(status, body) {
const err = body.error ?? {};
super(`${status} ${err.code}: ${err.message} (request ${body.request_id})`);
Object.assign(this, { status, type: err.type, code: err.code, param: err.param, requestId: body.request_id });
}
}
async function call(method, path, init = {}) {
const res = await fetch(`https://platform.detectif.ai/v1${path}`, {
...init,
method,
headers: { Authorization: `Bearer ${process.env.DETECTIF_API_KEY}`, ...init.headers },
});
const body = await res.json();
if (!res.ok) throw new DetectifError(res.status, body);
return body;
}
try {
await call("GET", "/speakers/unknown-id");
} catch (e) {
if (e instanceof DetectifError && e.code === "speaker_not_found") {
console.log("Not enrolled yet. Request ID:", e.requestId);
} else {
throw e;
}
}See Idempotency and retries for which errors are safe to retry and how.