Speaker enrollment and verification

Eidos · Speaker verification answers one question: is this the same speaker who was enrolled? You enroll a speaker with one or more recordings, then compare new recordings against that enrollment.

Endpoint Scope Cost
POST /v1/speakers/enroll speakers:enroll 50 credits
POST /v1/speakers/verify speakers:verify 50 credits
POST /v1/speakers/verify with detect_deepfake=true speakers:verify and deepfake:detect 100 credits (50 + 50)
GET /v1/speakers speakers:read free
GET /v1/speakers/{speaker_id} speakers:read free
DELETE /v1/speakers/{speaker_id} speakers:delete free

The model

The model is eidos-redimnet2, based on ReDimNet2. The current version is redimnet2-b6-vox2-lm-v1.0.0. The version can change; each response names the version that produced it in model.version, so read it from there rather than hard-coding it.

Phone-call audio. Verification is less reliable on telephone audio: 8 kHz narrowband recordings and audio that has passed through lossy voice codecs. Expect more wrong decisions on that audio than on clean wideband recordings, and plan a fallback, such as a second factor or human review, for calls where the decision matters.

Speaker IDs

speaker_id is your identifier for a person, such as a customer number. It must match ^[A-Za-z0-9_.:-]{1,64}$ and is unique within your account. Use an opaque identifier rather than a name, email address or phone number.

Speakers are private to your account. An ID that does not exist in your account returns 404 speaker_not_found, whether or not another account uses the same ID.

Enroll a speaker

Example (HTTP)
POST /v1/speakers/enroll

Multipart fields:

Field Required Description
file yes One recording of the speaker: 5 to 60 seconds after decoding, with at least 3 seconds of detected speech. We recommend 10 seconds or more of clean speech.
speaker_id yes Your identifier for the speaker.
display_name no A label shown in the dashboard.

The first call for a speaker_id creates the speaker. Each later call adds a sample, up to 5 active samples per speaker. The speaker's voice template is the normalised mean of the sample embeddings for the current model version. Recordings from different sessions, devices or days give the template more variety than one long recording.

Example (cURL)
curl https://platform.detectif.ai/v1/speakers/enroll \
  -H "Authorization: Bearer $DETECTIF_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "speaker_id=customer-1042" \
  -F "display_name=Customer 1042" \
  -F "file=@enrollment-1.wav"

Example response (values are illustrative):

Example (JSON)
{
  "request_id": "req_71c0e2a4f9d31a170e74983d",
  "operation_id": "op_3e9d4a02b7c127b87803b74dd556a75b",
  "object": "speaker.enrollment",
  "speaker": {
    "speaker_id": "customer-1042",
    "display_name": "Customer 1042",
    "samples": 1,
    "created_at": "2026-10-12T09:14:03Z",
    "updated_at": "2026-10-12T09:14:03Z"
  },
  "quality": { "speech_seconds": 11.8, "duration_seconds": 14.2 },
  "model": { "name": "eidos-redimnet2", "version": "redimnet2-b6-vox2-lm-v1.0.0" },
  "credits": { "charged": 50, "balance": 99850 },
  "latency_ms": 296.4,
  "idempotent_replay": false
}

quality.speech_seconds is the amount of speech detected in the recording. Enrollment needs at least 3 seconds; 10 seconds or more of clean speech is recommended. Enroll 2 to 3 samples per speaker.

Verify a speaker

Example (HTTP)
POST /v1/speakers/verify

Multipart fields:

Field Required Description
file yes The recording to check: 3 to 60 seconds after decoding, with at least 2 seconds of detected speech.
speaker_id yes The enrolled speaker to compare against.
detect_deepfake no true to also run deepfake detection on the same recording. Costs 50 more credits and needs the deepfake:detect scope.
Example (cURL)
curl https://platform.detectif.ai/v1/speakers/verify \
  -H "Authorization: Bearer $DETECTIF_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "speaker_id=customer-1042" \
  -F "detect_deepfake=true" \
  -F "file=@caller.wav"

Example response with detect_deepfake=true (values are illustrative):

Example (JSON)
{
  "request_id": "req_0b8e5d13c6fac3aa890b2a12",
  "operation_id": "op_c41a7e9f2d054febd9b806806d7caada",
  "object": "speaker.verification",
  "speaker_id": "customer-1042",
  "result": {
    "decision": "match",
    "similarity": 0.71,
    "threshold": 0.53,
    "similarity_semantics": "cosine similarity between embeddings; not a probability",
    "duration_seconds": 6.3
  },
  "deepfake": {
    "label": "genuine",
    "is_synthetic": false,
    "score": 0.05,
    "threshold": 0.62,
    "score_semantics": "model output probability of synthetic speech; not calibrated to your traffic",
    "duration_seconds": 6.3,
    "segments": [
      { "start": 0.0, "end": 4.0, "score": 0.04 },
      { "start": 2.0, "end": 6.3, "score": 0.06 }
    ],
    "operation_id": "op_f7b2093ac1e4ae025abcf34a4243019e",
    "credits_charged": 50,
    "model": { "name": "aletheia-wavlm", "version": "wavlm-finetune-n-m43-moredata-ep063" }
  },
  "model": { "name": "eidos-redimnet2", "version": "redimnet2-b6-vox2-lm-v1.0.0" },
  "credits": { "charged": 100, "balance": 99750 },
  "latency_ms": 534.9,
  "idempotent_replay": false
}
Field Description
result.similarity Cosine similarity between the recording's voice embedding and the speaker's template, from -1 to 1. Higher means more similar.
result.threshold The cosine similarity threshold applied for this request.
result.decision match when similarity reaches threshold, otherwise no_match.
result.similarity_semantics A plain statement of what the similarity means.
deepfake null without detect_deepfake=true. Otherwise the deepfake result (same fields as deepfake detection's result, plus its own operation_id, credits_charged and model), or, if that part failed, {"error": {...}, "operation_id": "...", "credits_charged": 0}.
model The speaker model and version used. Templates belong to a model version.
credits.charged 50, or 100 when the deepfake part also succeeded. 0 for an idempotent replay, which also has credits.original_charge (the amount charged for the original request).

Reading the result

  • Similarity is not a probability. A similarity of 0.71 does not mean a 71% chance that it is the same person. It is a cosine similarity between two embeddings. Compare it with the threshold returned in the same response.
  • Read the threshold and model version from each response. The default is 0.53 and can change when the model changes. Do not hard-code it. Store model.version with each decision. See The decision threshold.
  • Telephone audio is harder. On 8 kHz or codec-compressed phone audio, verification is less reliable (team-reported). Enroll and verify with audio from the same kind of channel when you can.
  • A match is a separate finding from authenticity. A cloned voice can be similar to the enrolled speaker. Use detect_deepfake=true, or a separate detection call, when that matters.
  • no_match is a result, not an error. It is charged like match. Decide in your own flow what happens next, for example a second factor or human review.

The decision threshold

result.decision is match when result.similarity is at or above result.threshold. The default threshold is 0.53 (cosine similarity). It was chosen for a low false-accept rate on a held-out split of Svarah (Indian-accented English), where the team measured:

Default threshold 0.53 on Svarah (held-out split) Rate
False accepts (a different speaker decided as match) 0.12%
False rejects (the same speaker decided as no_match) 2.75%

The trade-off: a threshold set for few false accepts rejects more genuine speakers when the conditions differ. When the enrollment and the verification recordings come from different sessions and conditions, false rejects rise. In the team's VoxCeleb test they were about 10% for full utterances and about 22% for 3 second clips. On phone and codec-compressed audio, verification is less reliable still.

To reduce false rejects:

  • enroll 2 to 3 samples per speaker, each with 10 seconds or more of clean speech, from different sessions;
  • verify with longer recordings where you can;
  • treat no_match as a reason for another check, not as proof of a different speaker.

You can also apply your own threshold to result.similarity. A lower threshold rejects fewer genuine speakers and accepts more impostors; a higher one does the opposite. Measure on recordings from your own channels before you change it.

The combined verify and deepfake option

With detect_deepfake=true, one request runs two operations on the same recording: a speaker verification and a deepfake detection. They are accounted independently:

  • Each operation reserves 50 credits, so you need at least 100 available credits. With fewer, the request fails with 402 insufficient_credits and nothing is charged.
  • It counts as two model operations toward the per-account rate limit.
  • If both succeed, you are charged 100 credits.
  • If the deepfake part fails (for example a timeout), deepfake contains an error object with a code and message, its reservation is released, and you are charged 50 credits for the verification.
  • Each part has its own operation_id. Both stored results can be fetched with GET /v1/operations/{operation_id}.

List, read and delete speakers

These calls are free.

Example (cURL)
# List speakers, 50 at a time
curl "https://platform.detectif.ai/v1/speakers?limit=50" \
  -H "Authorization: Bearer $DETECTIF_API_KEY"

# Read one speaker
curl https://platform.detectif.ai/v1/speakers/customer-1042 \
  -H "Authorization: Bearer $DETECTIF_API_KEY"

# Delete a speaker and its stored voice embeddings
curl -X DELETE https://platform.detectif.ai/v1/speakers/customer-1042 \
  -H "Authorization: Bearer $DETECTIF_API_KEY"

GET /v1/speakers returns speakers newest first as {"object": "list", "data": [...], "has_more": bool, "next_cursor": string | null}. limit defaults to 20 and can be 1 to 100. Pass next_cursor back as cursor to read the next page. Each speaker has speaker_id, display_name, samples (active samples for the current model version), created_at and updated_at.

Deleting a speaker removes its stored voice embeddings and returns {"object": "speaker.deleted", "speaker_id": "...", "deleted": true}. To enroll the same speaker_id again later, start a new enrollment.

Errors

Requests that fail are not charged. See Errors for the full list.

HTTP error.code What to do
400 invalid_speaker_id Use 1 to 64 characters from A-Z a-z 0-9 _ . : -.
404 speaker_not_found The speaker_id is not enrolled in your account.
409 too_many_samples The speaker already has 5 active samples. Delete the speaker and enroll again to replace them.
409 speaker_reenrollment_required The speaker has no samples for the current speaker model version. Enroll the speaker again.
422 audio_too_short, audio_too_long Enrollment: 5 to 60 seconds. Verification: 3 to 60 seconds.
422 insufficient_speech Too little speech was detected: at least 3 seconds to enroll, 2 seconds to verify.
422 silent_audio The recording contains no signal.
403 insufficient_scope With detect_deepfake=true the key needs both speakers:verify and deepfake:detect.
402 insufficient_credits Not enough available credits for the operations requested.