> For AI agents: the documentation index is at https://platform.detectif.ai/llms.txt. Every page is also available as Markdown by adding `.md` to its path.

# Verify a speaker

`POST /v1/speakers/verify`

Checks whether a recording is the voice of an enrolled speaker. Send a recording of 3-60 seconds with at least 2 seconds of speech. The result has a `decision` (`match` or `no_match`), a `similarity` from -1 to 1 (higher means more alike) and the `threshold` used for the decision. Main failures: unknown speaker (404), too short or too little speech (422), not enough credits (402). Requires scope `speakers:verify`.

With `detect_deepfake=true` the same recording is also checked for AI-generated speech (needs scope `deepfake:detect`; that check needs a recording longer than 4 seconds, otherwise only it fails). The two checks are counted separately, and only completed checks use credits:
* if the synthetic-speech check fails, you still get the verification, with `deepfake.error`;
* if the verification fails but the synthetic-speech check completed, you get the verification's error together with `partial` (the synthetic-speech result);
* if both fail, you get the error and no credits are used.

A retry with the same `Idempotency-Key` returns the same outcome again without running or counting the checks twice.

## Authentication

Send your API key as `Authorization: Bearer <your API key>`.

## Headers

| Name | Type | Required | Description |
|---|---|---|---|
| `Authorization` | string | yes | `Bearer <your API key>`. Keys are created in the dashboard; see [Authentication](https://platform.detectif.ai/docs/authentication/). |
| `Idempotency-Key` | string or null | no | Optional (recommended) key that makes retries safe for 24 h: a retry with the same key and the same request returns the stored response without charging again. (1 to 128 characters) |

## Request body

Send the request as `multipart/form-data`. Upload the audio file in the `file` field (up to 100 MB); send the other fields as form fields.

| Field | Type | Required | Description |
|---|---|---|---|
| `file` | file | yes | The audio file: WAV, MP3, FLAC, OGG/Opus, WebM, M4A/AAC or AIFF, at most 100 MB. |
| `speaker_id` | string | yes | Your identifier for the speaker (1-64 characters: letters, digits, _ . : -). (1 to 64 characters; pattern `^[A-Za-z0-9_.:-]{1,64}$`) |
| `detect_deepfake` | boolean | no | Also check the same recording for AI-generated speech (needs scope deepfake:detect). (default `false`) |

## Response

`200`: The check completed.

| Field | Type | Required | Description |
|---|---|---|---|
| `request_id` | string | yes |  |
| `operation_id` | string | yes |  |
| `object` | string | yes | (always `"speaker.verification"`) |
| `speaker_id` | string | yes |  |
| `result` | object | yes |  |
| `result.decision` | string | yes | `match` when `similarity` is at or above `threshold`. (one of `match`, `no_match`) |
| `result.similarity` | number | yes | How alike the two voices are, from -1 to 1 (higher is more alike). (-1 to 1) |
| `result.threshold` | number | yes | The similarity from which the decision is `match`. |
| `result.similarity_semantics` | string | yes | A short reminder of what `similarity` means. |
| `result.duration_seconds` | number | yes | Length of the checked audio, in seconds. |
| `deepfake` | object or null | yes | null unless detect_deepfake=true; then either the synthetic-speech result or its error. |
| `deepfake.(option 1 (EmbeddedDeepfake))` | object | no |  |
| `deepfake.verdict` | string | yes | The decision: `real`, `fake` (AI-generated), or `not_sure` (try a longer or cleaner recording). (one of `real`, `fake`, `not_sure`) |
| `deepfake.label` | string | yes | The same decision in the older wording: `synthetic` (fake), `genuine` (real), `uncertain` (not sure). (one of `synthetic`, `genuine`, `uncertain`) |
| `deepfake.is_synthetic` | boolean | yes | true only when the verdict is `fake`. |
| `deepfake.score` | number | yes | How likely the recording is AI-generated, from 0 to 1. (0 to 1) |
| `deepfake.threshold` | number | yes | The main model's threshold (0.62). Informational: decide with `verdict`. |
| `deepfake.score_semantics` | string | yes | A short reminder of what `score` means. |
| `deepfake.duration_seconds` | number | yes | Length of the checked audio, in seconds. |
| `deepfake.segments` | array of object | yes | Scores for consecutive, overlapping parts of the recording. |
| `deepfake.segments.start` | number | yes | Start of this part of the recording, in seconds. |
| `deepfake.segments.end` | number | yes | End of this part of the recording, in seconds. |
| `deepfake.segments.score` | number | yes | How likely this part is synthetic (0 to 1). (0 to 1) |
| `deepfake.operation_id` | string | yes |  |
| `deepfake.credits_charged` | integer | yes |  |
| `deepfake.model` | object | yes |  |
| `deepfake.model.name` | string | yes | Model family: `aletheia` (deepfake detection) or `eidos` (speaker verification). |
| `deepfake.model.version` | string | yes | Opaque model version; it changes whenever the model is updated. |
| `deepfake.(option 2 (EmbeddedDeepfakeError))` | object | no |  |
| `deepfake.error` | object | yes |  |
| `deepfake.error.type` | string | yes | (one of `invalid_request`, `authentication`, `permission`, `not_found`, `conflict`, `rate_limited`, `insufficient_credits`, `payload_too_large`, `unsupported_media`, `unprocessable_audio`, `overloaded`, `internal`) |
| `deepfake.error.code` | string | yes | Stable snake_case error code. |
| `deepfake.error.message` | string | yes | Human-readable explanation. Do not parse. |
| `deepfake.error.param` | string or null | no | The request field the error refers to, if any. |
| `deepfake.operation_id` | string | yes |  |
| `deepfake.credits_charged` | integer | no | (default `0`) |
| `model` | object | yes |  |
| `model.name` | string | yes | Model family: `aletheia` (deepfake detection) or `eidos` (speaker verification). |
| `model.version` | string | yes | Opaque model version; it changes whenever the model is updated. |
| `credits` | object | yes |  |
| `credits.charged` | integer | yes | Credits used by this request. A repeated request with the same Idempotency-Key uses none: it reports 0 here and what the original request used in `original_charge`. |
| `credits.balance` | integer or null | yes | Your available credits after this request; null on the unlimited plan. |
| `credits.original_charge` | integer or null | no | Only on repeated requests (`charged` is then 0): the credits the original request used. |
| `credits.unlimited` | boolean or null | no | Present (true) only for accounts on the unlimited plan, where checks use no credits. |
| `latency_ms` | number | yes |  |
| `idempotent_replay` | boolean | yes |  |

### Example response

```json
{
  "request_id": "req_8f14e45fceea167a5a36dedd",
  "operation_id": "op_6f1ed002ab5595859014ebf0951522d9",
  "object": "speaker.verification",
  "speaker_id": "customer-4711",
  "result": {
    "decision": "match",
    "similarity": 0.7412,
    "threshold": 0.53,
    "similarity_semantics": "Higher means the two voices are more alike. Not a percentage.",
    "duration_seconds": 4.1
  },
  "idempotent_replay": false
}
```

## Errors

| Status | Meaning |
|---|---|
| `400` | The request is invalid, for example a missing or malformed field. |
| `401` | The API key is missing, invalid or revoked. |
| `402` | Not enough credits are available for this operation. |
| `403` | The key lacks the required scope, the email address is not verified, or the account is suspended. |
| `404` | Not found. Also returned for resources that belong to another account. |
| `408` | The upload did not arrive in time (the whole file within 120 seconds, at least 64 KB per second). |
| `409` | Conflict, for example a request with the same Idempotency-Key is still running, or the speaker already has the maximum number of samples. |
| `413` | The upload is larger than 100 MB. |
| `415` | Not a supported audio format, or the request is not multipart/form-data. |
| `422` | The audio cannot be used (damaged, too short, too long, silent or too little speech; the message says what is needed), or the Idempotency-Key was already used for a different request. |
| `429` | Too many requests or too many operations at the same time. Wait for the number of seconds in Retry-After. |
| `500` | Something went wrong on our side. No credits were used. |
| `502` | The check failed on our side. No credits were used; retry later. |
| `503` | The service is busy, for example with many large uploads. Retry after the Retry-After seconds. |
| `504` | The check took too long. No credits were used; retry later. |

Error bodies use the shape described in [Errors](https://platform.detectif.ai/docs/errors/), with a stable `error.code`.

## Code samples

```bash
curl "https://platform.detectif.ai/v1/speakers/verify" \
  -H "Authorization: Bearer $DETECTIF_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "speaker_id=alice" \
  -F "file=@clip.wav"
```

```python
import os
import uuid

import requests

with open("clip.wav", "rb") as f:
    resp = requests.post(
        "https://platform.detectif.ai/v1/speakers/verify",
        headers={"Authorization": f"Bearer {os.environ['DETECTIF_API_KEY']}", "Idempotency-Key": str(uuid.uuid4())},
        data={"speaker_id": "alice"},
        files={"file": f},
        timeout=300,
    )
print(resp.status_code, resp.json())
```

```javascript
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const form = new FormData();
form.append("speaker_id", "alice");
form.append("file", new Blob([await readFile("clip.wav")]), "clip.wav");

const res = await fetch("https://platform.detectif.ai/v1/speakers/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DETECTIF_API_KEY}`,
    "Idempotency-Key": randomUUID(),
  },
  body: form,
});
console.log(res.status, await res.json());
```
