> 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.

# Enroll a voice sample

`POST /v1/speakers/enroll`

Saves a voice sample for a speaker so that later recordings can be verified against it. The speaker is created on first use. Send a recording of 15-60 seconds with at least 10 seconds of clear speech from that one person. Up to 5 samples per speaker; 2-3 samples from different sessions make verification more reliable. Main failures: too short, too long or too little speech (422, the message says what is needed), the speaker already has 5 samples (409), not enough credits (402). Requires scope `speakers:enroll`.

## 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}$`) |
| `display_name` | string | no | Optional label for the speaker, shown in the dashboard. (at most 128 characters) |

## Response

`200`: The sample was saved.

| Field | Type | Required | Description |
|---|---|---|---|
| `request_id` | string | yes |  |
| `operation_id` | string | yes |  |
| `object` | string | yes | (always `"speaker.enrollment"`) |
| `speaker` | object | yes |  |
| `speaker.speaker_id` | string | yes | (pattern `^[A-Za-z0-9_.:-]{1,64}$`) |
| `speaker.display_name` | string or null | yes |  |
| `speaker.samples` | integer | yes | Active enrollment samples for the current speaker model (max 5). |
| `speaker.created_at` | string | yes |  |
| `speaker.updated_at` | string | yes |  |
| `quality` | object | yes |  |
| `quality.speech_seconds` | number | yes |  |
| `quality.duration_seconds` | number | yes |  |
| `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.enrollment",
  "speaker": {
    "speaker_id": "customer-4711",
    "display_name": "Ada Lovelace",
    "samples": 1,
    "created_at": "2026-10-13T09:30:00Z",
    "updated_at": "2026-10-13T09:30:00Z"
  },
  "quality": {
    "speech_seconds": 17.9,
    "duration_seconds": 19.2
  },
  "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. |
| `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/enroll" \
  -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/enroll",
        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/enroll", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DETECTIF_API_KEY}`,
    "Idempotency-Key": randomUUID(),
  },
  body: form,
});
console.log(res.status, await res.json());
```
